refactor: Update to Dart 3.10 with . shorthands

This commit is contained in:
Christian Kußowski 2025-11-30 12:54:06 +01:00
commit 1ea649f01e
No known key found for this signature in database
GPG key ID: E067ECD60F1A0652
167 changed files with 3351 additions and 3912 deletions

View file

@ -43,18 +43,16 @@ abstract class AppRoutes {
static FutureOr<String?> loggedInRedirect( static FutureOr<String?> loggedInRedirect(
BuildContext context, BuildContext context,
GoRouterState state, GoRouterState state,
) => ) => Matrix.of(context).widget.clients.any((client) => client.isLogged())
Matrix.of(context).widget.clients.any((client) => client.isLogged()) ? '/rooms'
? '/rooms' : null;
: null;
static FutureOr<String?> loggedOutRedirect( static FutureOr<String?> loggedOutRedirect(
BuildContext context, BuildContext context,
GoRouterState state, GoRouterState state,
) => ) => Matrix.of(context).widget.clients.any((client) => client.isLogged())
Matrix.of(context).widget.clients.any((client) => client.isLogged()) ? null
? null : '/home';
: '/home';
AppRoutes(); AppRoutes();
@ -63,8 +61,8 @@ abstract class AppRoutes {
path: '/', path: '/',
redirect: (context, state) => redirect: (context, state) =>
Matrix.of(context).widget.clients.any((client) => client.isLogged()) Matrix.of(context).widget.clients.any((client) => client.isLogged())
? '/rooms' ? '/rooms'
: '/home', : '/home',
), ),
GoRoute( GoRoute(
path: '/home', path: '/home',
@ -88,19 +86,13 @@ abstract class AppRoutes {
), ),
GoRoute( GoRoute(
path: '/logs', path: '/logs',
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) =>
context, defaultPageBuilder(context, state, const LogViewer()),
state,
const LogViewer(),
),
), ),
GoRoute( GoRoute(
path: '/configs', path: '/configs',
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) =>
context, defaultPageBuilder(context, state, const ConfigViewer()),
state,
const ConfigViewer(),
),
), ),
GoRoute( GoRoute(
path: '/backup', path: '/backup',
@ -108,9 +100,7 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) => defaultPageBuilder(
context, context,
state, state,
BootstrapDialog( BootstrapDialog(wipe: state.uri.queryParameters['wipe'] == 'true'),
wipe: state.uri.queryParameters['wipe'] == 'true',
),
), ),
), ),
ShellRoute( ShellRoute(
@ -150,11 +140,8 @@ abstract class AppRoutes {
routes: [ routes: [
GoRoute( GoRoute(
path: 'archive', path: 'archive',
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) =>
context, defaultPageBuilder(context, state, const Archive()),
state,
const Archive(),
),
routes: [ routes: [
GoRoute( GoRoute(
path: ':roomid', path: ':roomid',
@ -173,20 +160,14 @@ abstract class AppRoutes {
), ),
GoRoute( GoRoute(
path: 'newprivatechat', path: 'newprivatechat',
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) =>
context, defaultPageBuilder(context, state, const NewPrivateChat()),
state,
const NewPrivateChat(),
),
redirect: loggedOutRedirect, redirect: loggedOutRedirect,
), ),
GoRoute( GoRoute(
path: 'newgroup', path: 'newgroup',
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) =>
context, defaultPageBuilder(context, state, const NewGroup()),
state,
const NewGroup(),
),
redirect: loggedOutRedirect, redirect: loggedOutRedirect,
), ),
GoRoute( GoRoute(
@ -376,9 +357,7 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) => defaultPageBuilder(
context, context,
state, state,
ChatSearchPage( ChatSearchPage(roomId: state.pathParameters['roomid']!),
roomId: state.pathParameters['roomid']!,
),
), ),
redirect: loggedOutRedirect, redirect: loggedOutRedirect,
), ),
@ -407,9 +386,7 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) => defaultPageBuilder(
context, context,
state, state,
ChatDetails( ChatDetails(roomId: state.pathParameters['roomid']!),
roomId: state.pathParameters['roomid']!,
),
), ),
routes: [ routes: [
GoRoute( GoRoute(
@ -459,9 +436,7 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) => defaultPageBuilder(
context, context,
state, state,
EmotesSettings( EmotesSettings(roomId: state.pathParameters['roomid']),
roomId: state.pathParameters['roomid'],
),
), ),
redirect: loggedOutRedirect, redirect: loggedOutRedirect,
), ),
@ -480,23 +455,21 @@ abstract class AppRoutes {
BuildContext context, BuildContext context,
GoRouterState state, GoRouterState state,
Widget child, Widget child,
) => ) => NoTransitionPage(
NoTransitionPage( key: state.pageKey,
key: state.pageKey, restorationId: state.pageKey.value,
restorationId: state.pageKey.value, child: child,
child: child, );
);
static Page defaultPageBuilder( static Page defaultPageBuilder(
BuildContext context, BuildContext context,
GoRouterState state, GoRouterState state,
Widget child, Widget child,
) => ) => FluffyThemes.isColumnMode(context)
FluffyThemes.isColumnMode(context) ? noTransitionPageBuilder(context, state, child)
? noTransitionPageBuilder(context, state, child) : MaterialPage(
: MaterialPage( key: state.pageKey,
key: state.pageKey, restorationId: state.pageKey.value,
restorationId: state.pageKey.value, child: child,
child: child, );
);
} }

View file

@ -46,18 +46,12 @@ enum AppSettings<T> {
'chat.fluffy.no_encryption_warning_shown', 'chat.fluffy.no_encryption_warning_shown',
false, false,
), ),
displayChatDetailsColumn( displayChatDetailsColumn('chat.fluffy.display_chat_details_column', false),
'chat.fluffy.display_chat_details_column',
false,
),
// AppConfig-mirrored settings // AppConfig-mirrored settings
applicationName<String>('chat.fluffy.application_name', 'FluffyChat'), applicationName<String>('chat.fluffy.application_name', 'FluffyChat'),
defaultHomeserver<String>('chat.fluffy.default_homeserver', 'matrix.org'), defaultHomeserver<String>('chat.fluffy.default_homeserver', 'matrix.org'),
// colorSchemeSeed stored as ARGB int // colorSchemeSeed stored as ARGB int
colorSchemeSeedInt<int>( colorSchemeSeedInt<int>('chat.fluffy.color_scheme_seed', 0xFF5625BA),
'chat.fluffy.color_scheme_seed',
0xFF5625BA,
),
emojiSuggestionLocale<String>('emoji_suggestion_locale', ''), emojiSuggestionLocale<String>('emoji_suggestion_locale', ''),
enableSoftLogout<bool>('chat.fluffy.enable_soft_logout', false); enableSoftLogout<bool>('chat.fluffy.enable_soft_logout', false);
@ -75,10 +69,9 @@ enum AppSettings<T> {
final store = AppSettings._store = await SharedPreferences.getInstance(); final store = AppSettings._store = await SharedPreferences.getInstance();
// Migrate wrong datatype for fontSizeFactor // Migrate wrong datatype for fontSizeFactor
final fontSizeFactorString = final fontSizeFactorString = Result(
Result(() => store.getString(AppSettings.fontSizeFactor.key)) () => store.getString(AppSettings.fontSizeFactor.key),
.asValue ).asValue?.value;
?.value;
if (fontSizeFactorString != null) { if (fontSizeFactorString != null) {
Logs().i('Migrate wrong datatype for fontSizeFactor!'); Logs().i('Migrate wrong datatype for fontSizeFactor!');
await store.remove(AppSettings.fontSizeFactor.key); await store.remove(AppSettings.fontSizeFactor.key);
@ -93,8 +86,9 @@ enum AppSettings<T> {
} }
if (kIsWeb && loadWebConfigFile) { if (kIsWeb && loadWebConfigFile) {
try { try {
final configJsonString = final configJsonString = utf8.decode(
utf8.decode((await http.get(Uri.parse('config.json'))).bodyBytes); (await http.get(Uri.parse('config.json'))).bodyBytes,
);
final configJson = final configJson =
json.decode(configJsonString) as Map<String, Object?>; json.decode(configJsonString) as Map<String, Object?>;
for (final setting in AppSettings.values) { for (final setting in AppSettings.values) {

View file

@ -20,10 +20,7 @@ abstract class FluffyThemes {
static bool isThreeColumnMode(BuildContext context) => static bool isThreeColumnMode(BuildContext context) =>
MediaQuery.sizeOf(context).width > FluffyThemes.columnWidth * 3.5; MediaQuery.sizeOf(context).width > FluffyThemes.columnWidth * 3.5;
static LinearGradient backgroundGradient( static LinearGradient backgroundGradient(BuildContext context, int alpha) {
BuildContext context,
int alpha,
) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
return LinearGradient( return LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
@ -91,12 +88,14 @@ abstract class FluffyThemes {
), ),
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
toolbarHeight: isColumnMode ? 72 : 56, toolbarHeight: isColumnMode ? 72 : 56,
shadowColor: shadowColor: isColumnMode
isColumnMode ? colorScheme.surfaceContainer.withAlpha(128) : null, ? colorScheme.surfaceContainer.withAlpha(128)
: null,
surfaceTintColor: isColumnMode ? colorScheme.surface : null, surfaceTintColor: isColumnMode ? colorScheme.surface : null,
backgroundColor: isColumnMode ? colorScheme.surface : null, backgroundColor: isColumnMode ? colorScheme.surface : null,
actionsPadding: actionsPadding: isColumnMode
isColumnMode ? const EdgeInsets.symmetric(horizontal: 16.0) : null, ? const EdgeInsets.symmetric(horizontal: 16.0)
: null,
systemOverlayStyle: SystemUiOverlayStyle( systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
statusBarIconBrightness: brightness.reversed, statusBarIconBrightness: brightness.reversed,
@ -107,10 +106,7 @@ abstract class FluffyThemes {
), ),
outlinedButtonTheme: OutlinedButtonThemeData( outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: BorderSide( side: BorderSide(width: 1, color: colorScheme.primary),
width: 1,
color: colorScheme.primary,
),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: BorderSide(color: colorScheme.primary), side: BorderSide(color: colorScheme.primary),
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
@ -157,8 +153,8 @@ extension BubbleColorTheme on ThemeData {
: colorScheme.onPrimaryContainer; : colorScheme.onPrimaryContainer;
Color get secondaryBubbleColor => HSLColor.fromColor( Color get secondaryBubbleColor => HSLColor.fromColor(
brightness == Brightness.light brightness == Brightness.light
? colorScheme.tertiary ? colorScheme.tertiary
: colorScheme.tertiaryContainer, : colorScheme.tertiaryContainer,
).withSaturation(0.5).toColor(); ).withSaturation(0.5).toColor();
} }

View file

@ -78,8 +78,9 @@ Future<void> startGui(List<Client> clients, SharedPreferences store) async {
String? pin; String? pin;
if (PlatformInfos.isMobile) { if (PlatformInfos.isMobile) {
try { try {
pin = pin = await const FlutterSecureStorage().read(
await const FlutterSecureStorage().read(key: 'chat.fluffy.app_lock'); key: 'chat.fluffy.app_lock',
);
} catch (e, s) { } catch (e, s) {
Logs().d('Unable to read PIN from Secure storage', e, s); Logs().d('Unable to read PIN from Secure storage', e, s);
} }

View file

@ -60,8 +60,9 @@ class ArchiveView extends StatelessWidget {
itemBuilder: (BuildContext context, int i) => ChatListItem( itemBuilder: (BuildContext context, int i) => ChatListItem(
controller.archive[i], controller.archive[i],
onForget: () => controller.forgetRoomAction(i), onForget: () => controller.forgetRoomAction(i),
onTap: () => context onTap: () => context.go(
.go('/rooms/archive/${controller.archive[i].id}'), '/rooms/archive/${controller.archive[i].id}',
),
), ),
); );
} }

View file

@ -23,10 +23,7 @@ import '../key_verification/key_verification_dialog.dart';
class BootstrapDialog extends StatefulWidget { class BootstrapDialog extends StatefulWidget {
final bool wipe; final bool wipe;
const BootstrapDialog({ const BootstrapDialog({super.key, this.wipe = false});
super.key,
this.wipe = false,
});
@override @override
BootstrapDialogState createState() => BootstrapDialogState(); BootstrapDialogState createState() => BootstrapDialogState();
@ -148,7 +145,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
builder: (context, snapshot) { builder: (context, snapshot) {
final status = snapshot.data; final status = snapshot.data;
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
CircularProgressIndicator.adaptive(value: status?.progress), CircularProgressIndicator.adaptive(value: status?.progress),
if (status != null) Text(status.calcLocalizedString(context)), if (status != null) Text(status.calcLocalizedString(context)),
@ -177,8 +174,9 @@ class BootstrapDialogState extends State<BootstrapDialog> {
), ),
body: Center( body: Center(
child: ConstrainedBox( child: ConstrainedBox(
constraints: constraints: const BoxConstraints(
const BoxConstraints(maxWidth: FluffyThemes.columnWidth * 1.5), maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: ListView( child: ListView(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
children: [ children: [
@ -193,10 +191,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
), ),
subtitle: Text(L10n.of(context).chatBackupDescription), subtitle: Text(L10n.of(context).chatBackupDescription),
), ),
const Divider( const Divider(height: 32, thickness: 1),
height: 32,
thickness: 1,
),
TextField( TextField(
minLines: 2, minLines: 2,
maxLines: 4, maxLines: 4,
@ -220,8 +215,9 @@ class BootstrapDialogState extends State<BootstrapDialog> {
}); });
}, },
title: Text(_getSecureStorageLocalizedName()), title: Text(_getSecureStorageLocalizedName()),
subtitle: subtitle: Text(
Text(L10n.of(context).storeInSecureStorageDescription), L10n.of(context).storeInSecureStorageDescription,
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
CheckboxListTile.adaptive( CheckboxListTile.adaptive(
@ -241,16 +237,16 @@ class BootstrapDialogState extends State<BootstrapDialog> {
label: Text(L10n.of(context).next), label: Text(L10n.of(context).next),
onPressed: onPressed:
(_recoveryKeyCopied || _storeInSecureStorage == true) (_recoveryKeyCopied || _storeInSecureStorage == true)
? () { ? () {
if (_storeInSecureStorage == true) { if (_storeInSecureStorage == true) {
const FlutterSecureStorage().write( const FlutterSecureStorage().write(
key: _secureStorageKey, key: _secureStorageKey,
value: key, value: key,
); );
} }
setState(() => _recoveryKeyStored = true); setState(() => _recoveryKeyStored = true);
} }
: null, : null,
), ),
], ],
), ),
@ -303,8 +299,9 @@ class BootstrapDialogState extends State<BootstrapDialog> {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
children: [ children: [
ListTile( ListTile(
contentPadding: contentPadding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 8.0), horizontal: 8.0,
),
trailing: Icon( trailing: Icon(
Icons.info_outlined, Icons.info_outlined,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@ -370,7 +367,9 @@ class BootstrapDialogState extends State<BootstrapDialog> {
); );
try { try {
await bootstrap await bootstrap
.client.encryption!.crossSigning .client
.encryption!
.crossSigning
.selfSign(recoveryKey: key); .selfSign(recoveryKey: key);
Logs().d('Successful selfsigned'); Logs().d('Successful selfsigned');
} catch (e, s) { } catch (e, s) {
@ -383,13 +382,14 @@ class BootstrapDialogState extends State<BootstrapDialog> {
} }
} on InvalidPassphraseException catch (e) { } on InvalidPassphraseException catch (e) {
setState( setState(
() => _recoveryKeyInputError = () => _recoveryKeyInputError = e
e.toLocalizedString(context), .toLocalizedString(context),
); );
} on FormatException catch (_) { } on FormatException catch (_) {
setState( setState(
() => _recoveryKeyInputError = () => _recoveryKeyInputError = L10n.of(
L10n.of(context).wrongRecoveryKey, context,
).wrongRecoveryKey,
); );
} catch (e, s) { } catch (e, s) {
ErrorReporter( ErrorReporter(
@ -397,8 +397,8 @@ class BootstrapDialogState extends State<BootstrapDialog> {
'Unable to open SSSS with recovery key', 'Unable to open SSSS with recovery key',
).onErrorCallback(e, s); ).onErrorCallback(e, s);
setState( setState(
() => _recoveryKeyInputError = () => _recoveryKeyInputError = e
e.toLocalizedString(context), .toLocalizedString(context),
); );
} finally { } finally {
setState( setState(
@ -428,8 +428,9 @@ class BootstrapDialogState extends State<BootstrapDialog> {
final consent = await showOkCancelAlertDialog( final consent = await showOkCancelAlertDialog(
context: context, context: context,
title: L10n.of(context).verifyOtherDevice, title: L10n.of(context).verifyOtherDevice,
message: L10n.of(context) message: L10n.of(
.verifyOtherDeviceDescription, context,
).verifyOtherDeviceDescription,
okLabel: L10n.of(context).ok, okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel, cancelLabel: L10n.of(context).cancel,
); );
@ -452,17 +453,18 @@ class BootstrapDialogState extends State<BootstrapDialog> {
final waitForSecret = Completer(); final waitForSecret = Completer();
final secretsSub = client final secretsSub = client
.encryption!.ssss.onSecretStored.stream .encryption!
.listen(( .ssss
event, .onSecretStored
) async { .stream
if (await client.encryption!.keyManager .listen((event) async {
.isCached() && if (await client.encryption!.keyManager
await client.encryption!.crossSigning .isCached() &&
.isCached()) { await client.encryption!.crossSigning
waitForSecret.complete(); .isCached()) {
} waitForSecret.complete();
}); }
});
final result = await showFutureLoadingDialog( final result = await showFutureLoadingDialog(
context: context, context: context,
@ -542,7 +544,7 @@ class BootstrapDialogState extends State<BootstrapDialog> {
case BootstrapState.done: case BootstrapState.done:
titleText = L10n.of(context).everythingReady; titleText = L10n.of(context).everythingReady;
body = Column( body = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon( const Icon(
Icons.check_circle_rounded, Icons.check_circle_rounded,
@ -576,13 +578,9 @@ class BootstrapDialogState extends State<BootstrapDialog> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: .stretch,
children: [ children: [body, const SizedBox(height: 8), ...buttons],
body,
const SizedBox(height: 8),
...buttons,
],
), ),
), ),
), ),

View file

@ -19,20 +19,21 @@ class AddWidgetTileView extends StatelessWidget {
CupertinoSegmentedControl( CupertinoSegmentedControl(
groupValue: controller.widgetType, groupValue: controller.widgetType,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
children: { children:
'm.etherpad': Text(L10n.of(context).widgetEtherpad), {
'm.jitsi': Text(L10n.of(context).widgetJitsi), 'm.etherpad': Text(L10n.of(context).widgetEtherpad),
'm.video': Text(L10n.of(context).widgetVideo), 'm.jitsi': Text(L10n.of(context).widgetJitsi),
'm.custom': Text(L10n.of(context).widgetCustom), 'm.video': Text(L10n.of(context).widgetVideo),
}.map( 'm.custom': Text(L10n.of(context).widgetCustom),
(key, value) => MapEntry( }.map(
key, (key, value) => MapEntry(
Padding( key,
padding: const EdgeInsets.symmetric(horizontal: 4.0), Padding(
child: value, padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: value,
),
),
), ),
),
),
onValueChanged: controller.setWidgetType, onValueChanged: controller.setWidgetType,
), ),
Padding( Padding(

View file

@ -182,14 +182,14 @@ class ChatController extends State<ChatPageWithRoom>
} }
void enterThread(String eventId) => setState(() { void enterThread(String eventId) => setState(() {
activeThreadId = eventId; activeThreadId = eventId;
selectedEvents.clear(); selectedEvents.clear();
}); });
void closeThread() => setState(() { void closeThread() => setState(() {
activeThreadId = null; activeThreadId = null;
selectedEvents.clear(); selectedEvents.clear();
}); });
void recreateChat() async { void recreateChat() async {
final room = this.room; final room = this.room;
@ -266,9 +266,7 @@ class ChatController extends State<ChatPageWithRoom>
closeIconColor: theme.colorScheme.onErrorContainer, closeIconColor: theme.colorScheme.onErrorContainer,
content: Text( content: Text(
L10n.of(context).otherPartyNotLoggedIn, L10n.of(context).otherPartyNotLoggedIn,
style: TextStyle( style: TextStyle(color: theme.colorScheme.onErrorContainer),
color: theme.colorScheme.onErrorContainer,
),
), ),
showCloseIcon: true, showCloseIcon: true,
), ),
@ -306,11 +304,9 @@ class ChatController extends State<ChatPageWithRoom>
} }
return KeyEventResult.handled; return KeyEventResult.handled;
} else if (evt.logicalKey.keyLabel == 'Enter' && evt is KeyDownEvent) { } else if (evt.logicalKey.keyLabel == 'Enter' && evt is KeyDownEvent) {
final currentLineNum = sendController.text final currentLineNum =
.substring( sendController.text
0, .substring(0, sendController.selection.baseOffset)
sendController.selection.baseOffset,
)
.split('\n') .split('\n')
.length - .length -
1; 1;
@ -357,10 +353,11 @@ class ChatController extends State<ChatPageWithRoom>
sendingClient = Matrix.of(context).client; sendingClient = Matrix.of(context).client;
final lastEventThreadId = final lastEventThreadId =
room.lastEvent?.relationshipType == RelationshipTypes.thread room.lastEvent?.relationshipType == RelationshipTypes.thread
? room.lastEvent?.relationshipEventId ? room.lastEvent?.relationshipEventId
: null; : null;
readMarkerEventId = readMarkerEventId = room.hasNewMessages
room.hasNewMessages ? lastEventThreadId ?? room.fullyRead : ''; ? lastEventThreadId ?? room.fullyRead
: '';
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_tryLoadTimeline(); _tryLoadTimeline();
if (kIsWeb) { if (kIsWeb) {
@ -402,11 +399,11 @@ class ChatController extends State<ChatPageWithRoom>
var readMarkerEventIndex = readMarkerEventId.isEmpty var readMarkerEventIndex = readMarkerEventId.isEmpty
? -1 ? -1
: timeline!.events : timeline!.events
.filterByVisibleInGui( .filterByVisibleInGui(
exceptionEventId: readMarkerEventId, exceptionEventId: readMarkerEventId,
threadId: activeThreadId, threadId: activeThreadId,
) )
.indexWhere((e) => e.eventId == readMarkerEventId); .indexWhere((e) => e.eventId == readMarkerEventId);
// Read marker is existing but not found in first events. Try a single // Read marker is existing but not found in first events. Try a single
// requestHistory call before opening timeline on event context: // requestHistory call before opening timeline on event context:
@ -441,12 +438,12 @@ class ChatController extends State<ChatPageWithRoom>
String? scrollUpBannerEventId; String? scrollUpBannerEventId;
void discardScrollUpBannerEventId() => setState(() { void discardScrollUpBannerEventId() => setState(() {
scrollUpBannerEventId = null; scrollUpBannerEventId = null;
}); });
void _showScrollUpMaterialBanner(String eventId) => setState(() { void _showScrollUpMaterialBanner(String eventId) => setState(() {
scrollUpBannerEventId = eventId; scrollUpBannerEventId = eventId;
}); });
void updateView() { void updateView() {
if (!mounted) return; if (!mounted) return;
@ -463,9 +460,7 @@ class ChatController extends State<ChatPageWithRoom>
animateInEventIndex = i; animateInEventIndex = i;
} }
Future<void> _getTimeline({ Future<void> _getTimeline({String? eventContextId}) async {
String? eventContextId,
}) async {
await Matrix.of(context).client.roomsLoading; await Matrix.of(context).client.roomsLoading;
await Matrix.of(context).client.accountDataLoading; await Matrix.of(context).client.accountDataLoading;
if (eventContextId != null && if (eventContextId != null &&
@ -534,12 +529,12 @@ class ChatController extends State<ChatPageWithRoom>
// ignore: unawaited_futures // ignore: unawaited_futures
_setReadMarkerFuture = timeline _setReadMarkerFuture = timeline
.setReadMarker( .setReadMarker(
eventId: eventId, eventId: eventId,
public: AppSettings.sendPublicReadReceipts.value, public: AppSettings.sendPublicReadReceipts.value,
) )
.then((_) { .then((_) {
_setReadMarkerFuture = null; _setReadMarkerFuture = null;
}); });
if (eventId == null || eventId == timeline.room.lastEvent?.eventId) { if (eventId == null || eventId == timeline.room.lastEvent?.eventId) {
Matrix.of(context).backgroundPush?.cancelNotification(roomId); Matrix.of(context).backgroundPush?.cancelNotification(roomId);
} }
@ -579,8 +574,8 @@ class ChatController extends State<ChatPageWithRoom>
} }
void setActiveClient(Client c) => setState(() { void setActiveClient(Client c) => setState(() {
Matrix.of(context).setActiveClient(c); Matrix.of(context).setActiveClient(c);
}); });
Future<void> send() async { Future<void> send() async {
if (sendController.text.trim().isEmpty) return; if (sendController.text.trim().isEmpty) return;
@ -627,11 +622,7 @@ class ChatController extends State<ChatPageWithRoom>
} }
void sendFileAction({FileSelectorType type = FileSelectorType.any}) async { void sendFileAction({FileSelectorType type = FileSelectorType.any}) async {
final files = await selectFiles( final files = await selectFiles(context, allowMultiple: true, type: type);
context,
allowMultiple: true,
type: type,
);
if (files.isEmpty) return; if (files.isEmpty) return;
await showAdaptiveDialog( await showAdaptiveDialog(
context: context, context: context,
@ -722,31 +713,26 @@ class ChatController extends State<ChatPageWithRoom>
setState(() { setState(() {
replyEvent = null; replyEvent = null;
}); });
room.sendFileEvent( room
file, .sendFileEvent(
inReplyTo: replyEvent, file,
threadRootEventId: activeThreadId, inReplyTo: replyEvent,
extraContent: { threadRootEventId: activeThreadId,
'info': { extraContent: {
...file.info, 'info': {...file.info, 'duration': duration},
'duration': duration, 'org.matrix.msc3245.voice': {},
}, 'org.matrix.msc1767.audio': {
'org.matrix.msc3245.voice': {}, 'duration': duration,
'org.matrix.msc1767.audio': { 'waveform': waveform,
'duration': duration, },
'waveform': waveform, },
}, )
}, .catchError((e) {
).catchError((e) { scaffoldMessenger.showSnackBar(
scaffoldMessenger.showSnackBar( SnackBar(content: Text((e as Object).toLocalizedString(context))),
SnackBar( );
content: Text( return null;
(e as Object).toLocalizedString(context), });
),
),
);
return null;
});
return; return;
} }
@ -785,7 +771,9 @@ class ChatController extends State<ChatPageWithRoom>
} }
for (final event in selectedEvents) { for (final event in selectedEvents) {
if (copyString.isNotEmpty) copyString += '\n\n'; if (copyString.isNotEmpty) copyString += '\n\n';
copyString += event.getDisplayEvent(timeline!).calcLocalizedBodyFallback( copyString += event
.getDisplayEvent(timeline!)
.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)), MatrixLocals(L10n.of(context)),
withSenderNamePrefix: true, withSenderNamePrefix: true,
); );
@ -813,14 +801,8 @@ class ChatController extends State<ChatPageWithRoom>
value: -100, value: -100,
label: L10n.of(context).extremeOffensive, label: L10n.of(context).extremeOffensive,
), ),
AdaptiveModalAction( AdaptiveModalAction(value: -50, label: L10n.of(context).offensive),
value: -50, AdaptiveModalAction(value: 0, label: L10n.of(context).inoffensive),
label: L10n.of(context).offensive,
),
AdaptiveModalAction(
value: 0,
label: L10n.of(context).inoffensive,
),
], ],
); );
if (score == null) return; if (score == null) return;
@ -835,11 +817,11 @@ class ChatController extends State<ChatPageWithRoom>
final result = await showFutureLoadingDialog( final result = await showFutureLoadingDialog(
context: context, context: context,
future: () => Matrix.of(context).client.reportEvent( future: () => Matrix.of(context).client.reportEvent(
event.roomId!, event.roomId!,
event.eventId, event.eventId,
reason: reason, reason: reason,
score: score, score: score,
), ),
); );
if (result.error != null) return; if (result.error != null) return;
setState(() { setState(() {
@ -905,9 +887,10 @@ class ChatController extends State<ChatPageWithRoom>
return; return;
} }
final room = client.getRoomById(roomId)!; final room = client.getRoomById(roomId)!;
await Event.fromJson(event.toJson(), room).redactEvent( await Event.fromJson(
reason: reason, event.toJson(),
); room,
).redactEvent(reason: reason);
} }
} else { } else {
await event.cancelSend(); await event.cancelSend();
@ -957,8 +940,9 @@ class ChatController extends State<ChatPageWithRoom>
!selectedEvents.first.status.isSent) { !selectedEvents.first.status.isSent) {
return false; return false;
} }
return currentRoomBundle return currentRoomBundle.any(
.any((cl) => selectedEvents.first.senderId == cl!.userID); (cl) => selectedEvents.first.senderId == cl!.userID,
);
} }
void forwardEventsAction() async { void forwardEventsAction() async {
@ -966,9 +950,9 @@ class ChatController extends State<ChatPageWithRoom>
final timeline = this.timeline; final timeline = this.timeline;
if (timeline == null) return; if (timeline == null) return;
final forwardEvents = List<Event>.from(selectedEvents) final forwardEvents = List<Event>.from(
.map((event) => event.getDisplayEvent(timeline)) selectedEvents,
.toList(); ).map((event) => event.getDisplayEvent(timeline)).toList();
await showScaffoldDialog( await showScaffoldDialog(
context: context, context: context,
@ -1004,29 +988,29 @@ class ChatController extends State<ChatPageWithRoom>
inputFocus.requestFocus(); inputFocus.requestFocus();
} }
void scrollToEventId( void scrollToEventId(String eventId, {bool highlightEvent = true}) async {
String eventId, { final foundEvent = timeline!.events.firstWhereOrNull(
bool highlightEvent = true, (event) => event.eventId == eventId,
}) async { );
final foundEvent =
timeline!.events.firstWhereOrNull((event) => event.eventId == eventId);
final eventIndex = foundEvent == null final eventIndex = foundEvent == null
? -1 ? -1
: timeline!.events : timeline!.events
.filterByVisibleInGui( .filterByVisibleInGui(
exceptionEventId: eventId, exceptionEventId: eventId,
threadId: activeThreadId, threadId: activeThreadId,
) )
.indexOf(foundEvent); .indexOf(foundEvent);
if (eventIndex == -1) { if (eventIndex == -1) {
setState(() { setState(() {
timeline = null; timeline = null;
_scrolledUp = false; _scrolledUp = false;
loadTimelineFuture = _getTimeline(eventContextId: eventId).onError( loadTimelineFuture = _getTimeline(eventContextId: eventId).onError(
ErrorReporter(context, 'Unable to load timeline after scroll to ID') ErrorReporter(
.onErrorCallback, context,
'Unable to load timeline after scroll to ID',
).onErrorCallback,
); );
}); });
await loadTimelineFuture; await loadTimelineFuture;
@ -1054,8 +1038,10 @@ class ChatController extends State<ChatPageWithRoom>
timeline = null; timeline = null;
_scrolledUp = false; _scrolledUp = false;
loadTimelineFuture = _getTimeline().onError( loadTimelineFuture = _getTimeline().onError(
ErrorReporter(context, 'Unable to load timeline after scroll down') ErrorReporter(
.onErrorCallback, context,
'Unable to load timeline after scroll down',
).onErrorCallback,
); );
}); });
await loadTimelineFuture; await loadTimelineFuture;
@ -1093,9 +1079,9 @@ class ChatController extends State<ChatPageWithRoom>
} }
void clearSelectedEvents() => setState(() { void clearSelectedEvents() => setState(() {
selectedEvents.clear(); selectedEvents.clear();
showEmojiPicker = false; showEmojiPicker = false;
}); });
void clearSingleSelectedEvent() { void clearSingleSelectedEvent() {
if (selectedEvents.length <= 1) { if (selectedEvents.length <= 1) {
@ -1115,12 +1101,13 @@ class ChatController extends State<ChatPageWithRoom>
setState(() { setState(() {
pendingText = sendController.text; pendingText = sendController.text;
editEvent = selectedEvents.first; editEvent = selectedEvents.first;
sendController.text = sendController.text = editEvent!
editEvent!.getDisplayEvent(timeline!).calcLocalizedBodyFallback( .getDisplayEvent(timeline!)
MatrixLocals(L10n.of(context)), .calcLocalizedBodyFallback(
withSenderNamePrefix: false, MatrixLocals(L10n.of(context)),
hideReply: true, withSenderNamePrefix: false,
); hideReply: true,
);
selectedEvents.clear(); selectedEvents.clear();
}); });
inputFocus.requestFocus(); inputFocus.requestFocus();
@ -1145,22 +1132,15 @@ class ChatController extends State<ChatPageWithRoom>
if (!mounted) return; if (!mounted) return;
context.go('/rooms/${result.result!}'); context.go('/rooms/${result.result!}');
await showFutureLoadingDialog( await showFutureLoadingDialog(context: context, future: room.leave);
context: context,
future: room.leave,
);
} }
void onSelectMessage(Event event) { void onSelectMessage(Event event) {
if (!event.redacted) { if (!event.redacted) {
if (selectedEvents.contains(event)) { if (selectedEvents.contains(event)) {
setState( setState(() => selectedEvents.remove(event));
() => selectedEvents.remove(event),
);
} else { } else {
setState( setState(() => selectedEvents.add(event));
() => selectedEvents.add(event),
);
} }
selectedEvents.sort( selectedEvents.sort(
(a, b) => a.originServerTs.compareTo(b.originServerTs), (a, b) => a.originServerTs.compareTo(b.originServerTs),
@ -1243,7 +1223,8 @@ class ChatController extends State<ChatPageWithRoom>
void pinEvent() { void pinEvent() {
final pinnedEventIds = room.pinnedEventIds; final pinnedEventIds = room.pinnedEventIds;
final selectedEventIds = selectedEvents.map((e) => e.eventId).toSet(); final selectedEventIds = selectedEvents.map((e) => e.eventId).toSet();
final unpin = selectedEventIds.length == 1 && final unpin =
selectedEventIds.length == 1 &&
pinnedEventIds.contains(selectedEventIds.single); pinnedEventIds.contains(selectedEventIds.single);
if (unpin) { if (unpin) {
pinnedEventIds.removeWhere(selectedEventIds.contains); pinnedEventIds.removeWhere(selectedEventIds.contains);
@ -1353,20 +1334,20 @@ class ChatController extends State<ChatPageWithRoom>
try { try {
await voipPlugin!.voip.inviteToCall(room, callType); await voipPlugin!.voip.inviteToCall(room, callType);
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(e.toLocalizedString(context))), context,
); ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
} }
} }
void cancelReplyEventAction() => setState(() { void cancelReplyEventAction() => setState(() {
if (editEvent != null) { if (editEvent != null) {
sendController.text = pendingText; sendController.text = pendingText;
pendingText = ''; pendingText = '';
} }
replyEvent = null; replyEvent = null;
editEvent = null; editEvent = null;
}); });
late final ValueNotifier<bool> _displayChatDetailsColumn; late final ValueNotifier<bool> _displayChatDetailsColumn;
@ -1382,38 +1363,30 @@ class ChatController extends State<ChatPageWithRoom>
final theme = Theme.of(context); final theme = Theme.of(context);
return Row( return Row(
children: [ children: [
Expanded( Expanded(child: ChatView(this)),
child: ChatView(this),
),
ValueListenableBuilder( ValueListenableBuilder(
valueListenable: _displayChatDetailsColumn, valueListenable: _displayChatDetailsColumn,
builder: (context, displayChatDetailsColumn, _) => builder: (context, displayChatDetailsColumn, _) =>
!FluffyThemes.isThreeColumnMode(context) || !FluffyThemes.isThreeColumnMode(context) ||
room.membership != Membership.join || room.membership != Membership.join ||
!displayChatDetailsColumn !displayChatDetailsColumn
? const SizedBox( ? const SizedBox(height: double.infinity, width: 0)
height: double.infinity, : Container(
width: 0, width: FluffyThemes.columnWidth,
) clipBehavior: Clip.hardEdge,
: Container( decoration: BoxDecoration(
width: FluffyThemes.columnWidth, border: Border(
clipBehavior: Clip.hardEdge, left: BorderSide(width: 1, color: theme.dividerColor),
decoration: BoxDecoration(
border: Border(
left: BorderSide(
width: 1,
color: theme.dividerColor,
),
),
),
child: ChatDetails(
roomId: roomId,
embeddedCloseButton: IconButton(
icon: const Icon(Icons.close),
onPressed: toggleDisplayChatDetailsColumn,
),
),
), ),
),
child: ChatDetails(
roomId: roomId,
embeddedCloseButton: IconButton(
icon: const Icon(Icons.close),
onPressed: toggleDisplayChatDetailsColumn,
),
),
),
), ),
], ],
); );

View file

@ -34,8 +34,8 @@ class ChatAppBarTitle extends StatelessWidget {
onTap: controller.isArchived onTap: controller.isArchived
? null ? null
: () => FluffyThemes.isThreeColumnMode(context) : () => FluffyThemes.isThreeColumnMode(context)
? controller.toggleDisplayChatDetailsColumn() ? controller.toggleDisplayChatDetailsColumn()
: context.go('/rooms/${room.id}/details'), : context.go('/rooms/${room.id}/details'),
child: Row( child: Row(
children: [ children: [
Hero( Hero(
@ -51,22 +51,22 @@ class ChatAppBarTitle extends StatelessWidget {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
Text( Text(
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(fontSize: 16),
fontSize: 16,
),
), ),
StreamBuilder( StreamBuilder(
stream: room.client.onSyncStatus.stream, stream: room.client.onSyncStatus.stream,
builder: (context, snapshot) { builder: (context, snapshot) {
final status = room.client.onSyncStatus.value ?? final status =
room.client.onSyncStatus.value ??
const SyncStatusUpdate(SyncStatus.waitingForResponse); const SyncStatusUpdate(SyncStatus.waitingForResponse);
final hide = FluffyThemes.isColumnMode(context) || final hide =
FluffyThemes.isColumnMode(context) ||
(room.client.onSync.value != null && (room.client.onSync.value != null &&
status.status != SyncStatus.error && status.status != SyncStatus.error &&
room.client.prevBatch != null); room.client.prevBatch != null);
@ -91,8 +91,9 @@ class ChatAppBarTitle extends StatelessWidget {
if (lastActiveTimestamp != null) { if (lastActiveTimestamp != null) {
return Text( return Text(
L10n.of(context).lastActiveAgo( L10n.of(context).lastActiveAgo(
lastActiveTimestamp lastActiveTimestamp.localizedTimeShort(
.localizedTimeShort(context), context,
),
), ),
style: style, style: style,
); );

View file

@ -52,8 +52,9 @@ class ChatEmojiPicker extends StatelessWidget {
), ),
categoryViewConfig: CategoryViewConfig( categoryViewConfig: CategoryViewConfig(
backspaceColor: theme.colorScheme.primary, backspaceColor: theme.colorScheme.primary,
iconColor: iconColor: theme.colorScheme.primary.withAlpha(
theme.colorScheme.primary.withAlpha(128), 128,
),
iconColorSelected: theme.colorScheme.primary, iconColorSelected: theme.colorScheme.primary,
indicatorColor: theme.colorScheme.primary, indicatorColor: theme.colorScheme.primary,
backgroundColor: theme.colorScheme.surface, backgroundColor: theme.colorScheme.surface,

View file

@ -16,10 +16,7 @@ import 'package:fluffychat/utils/platform_infos.dart';
class ChatEventList extends StatelessWidget { class ChatEventList extends StatelessWidget {
final ChatController controller; final ChatController controller;
const ChatEventList({ const ChatEventList({super.key, required this.controller});
super.key,
required this.controller,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -30,10 +27,7 @@ class ChatEventList extends StatelessWidget {
} }
final theme = Theme.of(context); final theme = Theme.of(context);
final colors = [ final colors = [theme.secondaryBubbleColor, theme.bubbleColor];
theme.secondaryBubbleColor,
theme.bubbleColor,
];
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0; final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
@ -83,11 +77,8 @@ class ChatEventList extends StatelessWidget {
); );
} }
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [SeenByRow(controller), TypingIndicators(controller)],
SeenByRow(controller),
TypingIndicators(controller),
],
); );
} }
@ -101,8 +92,9 @@ class ChatEventList extends StatelessWidget {
!event.isCollapsedState && event.isVisibleInGui, !event.isCollapsedState && event.isVisibleInGui,
); );
if (visibleIndex > timeline.events.length - 50) { if (visibleIndex > timeline.events.length - 50) {
WidgetsBinding.instance WidgetsBinding.instance.addPostFrameCallback(
.addPostFrameCallback(controller.requestHistory); controller.requestHistory,
);
} }
return Center( return Center(
child: AnimatedSwitcher( child: AnimatedSwitcher(
@ -126,7 +118,8 @@ class ChatEventList extends StatelessWidget {
// The message at this index: // The message at this index:
final event = events[i]; final event = events[i];
final animateIn = animateInEventIndex != null && final animateIn =
animateInEventIndex != null &&
timeline.events.length > animateInEventIndex && timeline.events.length > animateInEventIndex &&
event == timeline.events[animateInEventIndex]; event == timeline.events[animateInEventIndex];
@ -134,10 +127,12 @@ class ChatEventList extends StatelessWidget {
final previousEvent = i > 0 ? events[i - 1] : null; final previousEvent = i > 0 ? events[i - 1] : null;
// Collapsed state event // Collapsed state event
final canExpand = event.isCollapsedState && final canExpand =
event.isCollapsedState &&
nextEvent?.isCollapsedState == true && nextEvent?.isCollapsedState == true &&
previousEvent?.isCollapsedState != true; previousEvent?.isCollapsedState != true;
final isCollapsed = event.isCollapsedState && final isCollapsed =
event.isCollapsedState &&
previousEvent?.isCollapsedState == true && previousEvent?.isCollapsedState == true &&
!controller.expandedEventIds.contains(event.eventId); !controller.expandedEventIds.contains(event.eventId);
@ -161,11 +156,12 @@ class ChatEventList extends StatelessWidget {
scrollToEventId: (String eventId) => scrollToEventId: (String eventId) =>
controller.scrollToEventId(eventId), controller.scrollToEventId(eventId),
longPressSelect: controller.selectedEvents.isNotEmpty, longPressSelect: controller.selectedEvents.isNotEmpty,
selected: controller.selectedEvents selected: controller.selectedEvents.any(
.any((e) => e.eventId == event.eventId), (e) => e.eventId == event.eventId,
),
singleSelected: singleSelected:
controller.selectedEvents.singleOrNull?.eventId == controller.selectedEvents.singleOrNull?.eventId ==
event.eventId, event.eventId,
onEdit: () => controller.editSelectedEventAction(), onEdit: () => controller.editSelectedEventAction(),
timeline: timeline, timeline: timeline,
displayReadMarker: displayReadMarker:
@ -181,9 +177,9 @@ class ChatEventList extends StatelessWidget {
: null, : null,
onExpand: canExpand onExpand: canExpand
? () => controller.expandEventsFrom( ? () => controller.expandEventsFrom(
event, event,
!controller.expandedEventIds.contains(event.eventId), !controller.expandedEventIds.contains(event.eventId),
) )
: null, : null,
), ),
); );

View file

@ -53,12 +53,13 @@ class ChatInputRow extends StatelessWidget {
); );
} }
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: .end,
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: .spaceBetween,
children: controller.selectMode children: controller.selectMode
? <Widget>[ ? <Widget>[
if (controller.selectedEvents if (controller.selectedEvents.every(
.every((event) => event.status == EventStatus.error)) (event) => event.status == EventStatus.error,
))
SizedBox( SizedBox(
height: height, height: height,
child: TextButton( child: TextButton(
@ -90,36 +91,36 @@ class ChatInputRow extends StatelessWidget {
), ),
controller.selectedEvents.length == 1 controller.selectedEvents.length == 1
? controller.selectedEvents.first ? controller.selectedEvents.first
.getDisplayEvent(controller.timeline!) .getDisplayEvent(controller.timeline!)
.status .status
.isSent .isSent
? SizedBox( ? SizedBox(
height: height, height: height,
child: TextButton( child: TextButton(
style: selectedTextButtonStyle, style: selectedTextButtonStyle,
onPressed: controller.replyAction, onPressed: controller.replyAction,
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Text(L10n.of(context).reply), Text(L10n.of(context).reply),
const Icon(Icons.keyboard_arrow_right), const Icon(Icons.keyboard_arrow_right),
], ],
),
), ),
), )
) : SizedBox(
: SizedBox( height: height,
height: height, child: TextButton(
child: TextButton( style: selectedTextButtonStyle,
style: selectedTextButtonStyle, onPressed: controller.sendAgainAction,
onPressed: controller.sendAgainAction, child: Row(
child: Row( children: <Widget>[
children: <Widget>[ Text(L10n.of(context).tryToSendAgain),
Text(L10n.of(context).tryToSendAgain), const SizedBox(width: 4),
const SizedBox(width: 4), const Icon(Icons.send_outlined, size: 16),
const Icon(Icons.send_outlined, size: 16), ],
], ),
), ),
), )
)
: const SizedBox.shrink(), : const SizedBox.shrink(),
] ]
: <Widget>[ : <Widget>[
@ -127,8 +128,9 @@ class ChatInputRow extends StatelessWidget {
AnimatedContainer( AnimatedContainer(
duration: FluffyThemes.animationDuration, duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,
width: width: controller.sendController.text.isNotEmpty
controller.sendController.text.isNotEmpty ? 0 : height, ? 0
: height,
height: height, height: height,
alignment: Alignment.center, alignment: Alignment.center,
decoration: const BoxDecoration(), decoration: const BoxDecoration(),
@ -190,8 +192,9 @@ class ChatInputRow extends StatelessWidget {
theme.colorScheme.onPrimaryContainer, theme.colorScheme.onPrimaryContainer,
foregroundColor: foregroundColor:
theme.colorScheme.primaryContainer, theme.colorScheme.primaryContainer,
child: child: const Icon(
const Icon(Icons.video_camera_back_outlined), Icons.video_camera_back_outlined,
),
), ),
title: Text(L10n.of(context).sendVideo), title: Text(L10n.of(context).sendVideo),
contentPadding: const EdgeInsets.all(0), contentPadding: const EdgeInsets.all(0),
@ -270,19 +273,20 @@ class ChatInputRow extends StatelessWidget {
tooltip: L10n.of(context).emojis, tooltip: L10n.of(context).emojis,
color: theme.colorScheme.onPrimaryContainer, color: theme.colorScheme.onPrimaryContainer,
icon: PageTransitionSwitcher( icon: PageTransitionSwitcher(
transitionBuilder: ( transitionBuilder:
Widget child, (
Animation<double> primaryAnimation, Widget child,
Animation<double> secondaryAnimation, Animation<double> primaryAnimation,
) { Animation<double> secondaryAnimation,
return SharedAxisTransition( ) {
animation: primaryAnimation, return SharedAxisTransition(
secondaryAnimation: secondaryAnimation, animation: primaryAnimation,
transitionType: SharedAxisTransitionType.scaled, secondaryAnimation: secondaryAnimation,
fillColor: Colors.transparent, transitionType: SharedAxisTransitionType.scaled,
child: child, fillColor: Colors.transparent,
); child: child,
}, );
},
child: Icon( child: Icon(
controller.showEmojiPicker controller.showEmojiPicker
? Icons.keyboard ? Icons.keyboard
@ -313,9 +317,9 @@ class ChatInputRow extends StatelessWidget {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
textInputAction: textInputAction:
AppSettings.sendOnEnter.value == true && AppSettings.sendOnEnter.value == true &&
PlatformInfos.isMobile PlatformInfos.isMobile
? TextInputAction.send ? TextInputAction.send
: null, : null,
onSubmitted: controller.onInputBarSubmitted, onSubmitted: controller.onInputBarSubmitted,
onSubmitImage: controller.sendImageFromClipBoard, onSubmitImage: controller.sendImageFromClipBoard,
focusNode: controller.inputFocus, focusNode: controller.inputFocus,
@ -335,14 +339,18 @@ class ChatInputRow extends StatelessWidget {
filled: false, filled: false,
), ),
onChanged: controller.onInputBarChanged, onChanged: controller.onInputBarChanged,
suggestionEmojis: getDefaultEmojiLocale( suggestionEmojis:
AppSettings.emojiSuggestionLocale.value.isNotEmpty getDefaultEmojiLocale(
? Locale(AppSettings.emojiSuggestionLocale.value) AppSettings.emojiSuggestionLocale.value.isNotEmpty
: Localizations.localeOf(context), ? Locale(
).fold( AppSettings.emojiSuggestionLocale.value,
[], )
(emojis, category) => emojis..addAll(category.emoji), : Localizations.localeOf(context),
), ).fold(
[],
(emojis, category) =>
emojis..addAll(category.emoji),
),
), ),
), ),
), ),
@ -350,19 +358,21 @@ class ChatInputRow extends StatelessWidget {
height: height, height: height,
width: height, width: height,
alignment: Alignment.center, alignment: Alignment.center,
child: PlatformInfos.platformCanRecord && child:
PlatformInfos.platformCanRecord &&
controller.sendController.text.isEmpty controller.sendController.text.isEmpty
? IconButton( ? IconButton(
tooltip: L10n.of(context).voiceMessage, tooltip: L10n.of(context).voiceMessage,
onPressed: () => onPressed: () =>
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
L10n.of(context) L10n.of(
.longPressToRecordVoiceMessage, context,
).longPressToRecordVoiceMessage,
),
),
), ),
),
),
onLongPress: () => recordingViewModel onLongPress: () => recordingViewModel
.startRecording(controller.room), .startRecording(controller.room),
style: IconButton.styleFrom( style: IconButton.styleFrom(
@ -394,9 +404,9 @@ class _ChatAccountPicker extends StatelessWidget {
const _ChatAccountPicker(this.controller); const _ChatAccountPicker(this.controller);
void _popupMenuButtonSelected(String mxid, BuildContext context) { void _popupMenuButtonSelected(String mxid, BuildContext context) {
final client = Matrix.of(context) final client = Matrix.of(
.currentBundle! context,
.firstWhere((cl) => cl!.userID == mxid, orElse: () => null); ).currentBundle!.firstWhere((cl) => cl!.userID == mxid, orElse: () => null);
if (client == null) { if (client == null) {
Logs().w('Attempted to switch to a non-existing client $mxid'); Logs().w('Attempted to switch to a non-existing client $mxid');
return; return;
@ -423,7 +433,8 @@ class _ChatAccountPicker extends StatelessWidget {
builder: (context, snapshot) => ListTile( builder: (context, snapshot) => ListTile(
leading: Avatar( leading: Avatar(
mxContent: snapshot.data?.avatarUrl, mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ?? name:
snapshot.data?.displayName ??
client.userID!.localpart, client.userID!.localpart,
size: 20, size: 20,
), ),
@ -436,7 +447,8 @@ class _ChatAccountPicker extends StatelessWidget {
.toList(), .toList(),
child: Avatar( child: Avatar(
mxContent: snapshot.data?.avatarUrl, mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ?? name:
snapshot.data?.displayName ??
Matrix.of(context).client.userID!.localpart, Matrix.of(context).client.userID!.localpart,
size: 20, size: 20,
), ),

View file

@ -49,8 +49,9 @@ class ChatView extends StatelessWidget {
IconButton( IconButton(
icon: const Icon(Icons.message_outlined), icon: const Icon(Icons.message_outlined),
tooltip: L10n.of(context).replyInThread, tooltip: L10n.of(context).replyInThread,
onPressed: () => controller onPressed: () => controller.enterThread(
.enterThread(controller.selectedEvents.single.eventId), controller.selectedEvents.single.eventId,
),
), ),
IconButton( IconButton(
icon: const Icon(Icons.copy_outlined), icon: const Icon(Icons.copy_outlined),
@ -83,7 +84,7 @@ class ChatView extends StatelessWidget {
onTap: controller.pinEvent, onTap: controller.pinEvent,
value: null, value: null,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.push_pin_outlined), const Icon(Icons.push_pin_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -96,7 +97,7 @@ class ChatView extends StatelessWidget {
onTap: () => controller.saveSelectedEvent(context), onTap: () => controller.saveSelectedEvent(context),
value: null, value: null,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.download_outlined), const Icon(Icons.download_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -107,7 +108,7 @@ class ChatView extends StatelessWidget {
PopupMenuItem( PopupMenuItem(
value: _EventContextAction.info, value: _EventContextAction.info,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.info_outlined), const Icon(Icons.info_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -119,12 +120,9 @@ class ChatView extends StatelessWidget {
PopupMenuItem( PopupMenuItem(
value: _EventContextAction.report, value: _EventContextAction.report,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon( const Icon(Icons.shield_outlined, color: Colors.red),
Icons.shield_outlined,
color: Colors.red,
),
const SizedBox(width: 12), const SizedBox(width: 12),
Text(L10n.of(context).reportMessage), Text(L10n.of(context).reportMessage),
], ],
@ -166,7 +164,8 @@ class ChatView extends StatelessWidget {
final accountConfig = Matrix.of(context).client.applicationAccountConfig; final accountConfig = Matrix.of(context).client.applicationAccountConfig;
return PopScope( return PopScope(
canPop: controller.selectedEvents.isEmpty && canPop:
controller.selectedEvents.isEmpty &&
!controller.showEmojiPicker && !controller.showEmojiPicker &&
controller.activeThreadId == null, controller.activeThreadId == null,
onPopInvokedWithResult: (pop, _) async { onPopInvokedWithResult: (pop, _) async {
@ -207,8 +206,8 @@ class ChatView extends StatelessWidget {
), ),
backgroundColor: controller.selectedEvents.isEmpty backgroundColor: controller.selectedEvents.isEmpty
? controller.activeThreadId != null ? controller.activeThreadId != null
? theme.colorScheme.secondaryContainer ? theme.colorScheme.secondaryContainer
: null : null
: theme.colorScheme.tertiaryContainer, : theme.colorScheme.tertiaryContainer,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
leading: controller.selectMode leading: controller.selectMode
@ -219,36 +218,31 @@ class ChatView extends StatelessWidget {
color: theme.colorScheme.onTertiaryContainer, color: theme.colorScheme.onTertiaryContainer,
) )
: activeThreadId != null : activeThreadId != null
? IconButton( ? IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: controller.closeThread, onPressed: controller.closeThread,
tooltip: L10n.of(context).backToMainChat, tooltip: L10n.of(context).backToMainChat,
color: theme.colorScheme.onSecondaryContainer, color: theme.colorScheme.onSecondaryContainer,
) )
: FluffyThemes.isColumnMode(context) : FluffyThemes.isColumnMode(context)
? null ? null
: StreamBuilder<Object>( : StreamBuilder<Object>(
stream: Matrix.of(context) stream: Matrix.of(context).client.onSync.stream.where(
.client (syncUpdate) => syncUpdate.hasRoomUpdate,
.onSync ),
.stream builder: (context, _) => UnreadRoomsBadge(
.where( filter: (r) => r.id != controller.roomId,
(syncUpdate) => syncUpdate.hasRoomUpdate, badgePosition: BadgePosition.topEnd(end: 8, top: 4),
), child: const Center(child: BackButton()),
builder: (context, _) => UnreadRoomsBadge( ),
filter: (r) => r.id != controller.roomId, ),
badgePosition:
BadgePosition.topEnd(end: 8, top: 4),
child: const Center(child: BackButton()),
),
),
titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0, titleSpacing: FluffyThemes.isColumnMode(context) ? 24 : 0,
title: ChatAppBarTitle(controller), title: ChatAppBarTitle(controller),
actions: _appBarActions(context), actions: _appBarActions(context),
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: Size.fromHeight(appbarBottomHeight), preferredSize: Size.fromHeight(appbarBottomHeight),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
PinnedEvents(controller), PinnedEvents(controller),
if (activeThreadId != null) if (activeThreadId != null)
@ -285,9 +279,7 @@ class ChatView extends StatelessWidget {
title: L10n.of(context).jumpToLastReadMessage, title: L10n.of(context).jumpToLastReadMessage,
trailing: TextButton( trailing: TextButton(
onPressed: () { onPressed: () {
controller.scrollToEventId( controller.scrollToEventId(scrollUpBannerEventId);
scrollUpBannerEventId,
);
controller.discardScrollUpBannerEventId(); controller.discardScrollUpBannerEventId();
}, },
child: Text(L10n.of(context).jump), child: Text(L10n.of(context).jump),
@ -299,7 +291,8 @@ class ChatView extends StatelessWidget {
), ),
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.miniCenterFloat, FloatingActionButtonLocation.miniCenterFloat,
floatingActionButton: controller.showScrollDownButton && floatingActionButton:
controller.showScrollDownButton &&
controller.selectedEvents.isEmpty controller.selectedEvents.isEmpty
? Padding( ? Padding(
padding: const EdgeInsets.only(bottom: 56.0), padding: const EdgeInsets.only(bottom: 56.0),
@ -348,10 +341,7 @@ class ChatView extends StatelessWidget {
), ),
), ),
if (controller.showScrollDownButton) if (controller.showScrollDownButton)
Divider( Divider(height: 1, color: theme.dividerColor),
height: 1,
color: theme.dividerColor,
),
if (controller.room.isExtinct) if (controller.room.isExtinct)
Container( Container(
margin: EdgeInsets.all(bottomSheetPadding), margin: EdgeInsets.all(bottomSheetPadding),
@ -380,14 +370,11 @@ class ChatView extends StatelessWidget {
), ),
child: controller.room.isAbandonedDMRoom == true child: controller.room.isAbandonedDMRoom == true
? Row( ? Row(
mainAxisAlignment: mainAxisAlignment: .spaceEvenly,
MainAxisAlignment.spaceEvenly,
children: [ children: [
TextButton.icon( TextButton.icon(
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.all( padding: const EdgeInsets.all(16),
16,
),
foregroundColor: foregroundColor:
theme.colorScheme.error, theme.colorScheme.error,
), ),
@ -395,15 +382,11 @@ class ChatView extends StatelessWidget {
Icons.archive_outlined, Icons.archive_outlined,
), ),
onPressed: controller.leaveChat, onPressed: controller.leaveChat,
label: Text( label: Text(L10n.of(context).leave),
L10n.of(context).leave,
),
), ),
TextButton.icon( TextButton.icon(
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.all( padding: const EdgeInsets.all(16),
16,
),
), ),
icon: const Icon( icon: const Icon(
Icons.forum_outlined, Icons.forum_outlined,
@ -416,7 +399,7 @@ class ChatView extends StatelessWidget {
], ],
) )
: Column( : Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
ReplyDisplay(controller), ReplyDisplay(controller),
ChatInputRow(controller), ChatInputRow(controller),
@ -432,10 +415,7 @@ class ChatView extends StatelessWidget {
Container( Container(
color: theme.scaffoldBackgroundColor.withAlpha(230), color: theme.scaffoldBackgroundColor.withAlpha(230),
alignment: Alignment.center, alignment: Alignment.center,
child: const Icon( child: const Icon(Icons.upload_outlined, size: 100),
Icons.upload_outlined,
size: 100,
),
), ),
], ],
), ),

View file

@ -15,11 +15,9 @@ class EncryptionButton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return StreamBuilder<SyncUpdate>( return StreamBuilder<SyncUpdate>(
stream: Matrix.of(context) stream: Matrix.of(
.client context,
.onSync ).client.onSync.stream.where((s) => s.deviceLists != null),
.stream
.where((s) => s.deviceLists != null),
builder: (context, snapshot) { builder: (context, snapshot) {
final shouldBeEncrypted = room.joinRules != JoinRules.public; final shouldBeEncrypted = room.joinRules != JoinRules.public;
return FutureBuilder<EncryptionHealthState>( return FutureBuilder<EncryptionHealthState>(

View file

@ -12,21 +12,16 @@ import 'package:fluffychat/widgets/avatar.dart';
extension EventInfoDialogExtension on Event { extension EventInfoDialogExtension on Event {
void showInfoDialog(BuildContext context) => showAdaptiveBottomSheet( void showInfoDialog(BuildContext context) => showAdaptiveBottomSheet(
context: context, context: context,
builder: (context) => builder: (context) => EventInfoDialog(l10n: L10n.of(context), event: this),
EventInfoDialog(l10n: L10n.of(context), event: this), );
);
} }
class EventInfoDialog extends StatelessWidget { class EventInfoDialog extends StatelessWidget {
final Event event; final Event event;
final L10n l10n; final L10n l10n;
const EventInfoDialog({ const EventInfoDialog({required this.event, required this.l10n, super.key});
required this.event,
required this.l10n,
super.key,
});
String prettyJson(MatrixEvent event) { String prettyJson(MatrixEvent event) {
const decoder = JsonDecoder(); const decoder = JsonDecoder();
@ -79,9 +74,7 @@ class EventInfoDialog extends StatelessWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: SelectableText( child: SelectableText(
prettyJson(MatrixEvent.fromJson(event.toJson())), prettyJson(MatrixEvent.fromJson(event.toJson())),
style: TextStyle( style: TextStyle(color: theme.colorScheme.onSurface),
color: theme.colorScheme.onSurface,
),
), ),
), ),
), ),
@ -98,9 +91,7 @@ class EventInfoDialog extends StatelessWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: SelectableText( child: SelectableText(
prettyJson(originalSource), prettyJson(originalSource),
style: TextStyle( style: TextStyle(color: theme.colorScheme.onSurface),
color: theme.colorScheme.onSurface,
),
), ),
), ),
), ),

View file

@ -100,12 +100,13 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
onPressed: () { onPressed: () {
audioPlayer.pause(); audioPlayer.pause();
audioPlayer.dispose(); audioPlayer.dispose();
matrix.voiceMessageEventId.value = matrix.voiceMessageEventId.value = matrix.audioPlayer =
matrix.audioPlayer = null; null;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(matrix.context) ScaffoldMessenger.of(
.clearMaterialBanners(); matrix.context,
).clearMaterialBanners();
}); });
}, },
icon: const Icon(Icons.close_outlined), icon: const Icon(Icons.close_outlined),
@ -128,8 +129,8 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
}); });
final currentPlayer = final currentPlayer =
matrix.voiceMessageEventId.value != widget.event.eventId matrix.voiceMessageEventId.value != widget.event.eventId
? null ? null
: matrix.audioPlayer; : matrix.audioPlayer;
if (currentPlayer != null) { if (currentPlayer != null) {
if (currentPlayer.isAtEndPosition) { if (currentPlayer.isAtEndPosition) {
currentPlayer.seek(Duration.zero); currentPlayer.seek(Duration.zero);
@ -158,8 +159,9 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
? (progress) { ? (progress) {
final progressPercentage = progress / fileSize; final progressPercentage = progress / fileSize;
setState(() { setState(() {
_downloadProgress = _downloadProgress = progressPercentage < 1
progressPercentage < 1 ? progressPercentage : null; ? progressPercentage
: null;
}); });
} }
: null, : null,
@ -190,11 +192,9 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
}); });
} catch (e, s) { } catch (e, s) {
Logs().v('Could not download audio file', e, s); Logs().v('Could not download audio file', e, s);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar( context,
content: Text(e.toLocalizedString(context)), ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
),
);
rethrow; rethrow;
} }
if (!context.mounted) return; if (!context.mounted) return;
@ -209,9 +209,8 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
} }
audioPlayer.play().onError( audioPlayer.play().onError(
ErrorReporter(context, 'Unable to play audio message') ErrorReporter(context, 'Unable to play audio message').onErrorCallback,
.onErrorCallback, );
);
} }
void _toggleSpeed() async { void _toggleSpeed() async {
@ -289,8 +288,9 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
return ValueListenableBuilder( return ValueListenableBuilder(
valueListenable: matrix.voiceMessageEventId, valueListenable: matrix.voiceMessageEventId,
builder: (context, eventId, _) { builder: (context, eventId, _) {
final audioPlayer = final audioPlayer = eventId != widget.event.eventId
eventId != widget.event.eventId ? null : matrix.audioPlayer; ? null
: matrix.audioPlayer;
final fileDescription = widget.event.fileDescription; final fileDescription = widget.event.fileDescription;
@ -317,15 +317,15 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
return Padding( return Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth, maxWidth: FluffyThemes.columnWidth,
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: <Widget>[ children: <Widget>[
SizedBox( SizedBox(
width: buttonSize, width: buttonSize,
@ -366,9 +366,11 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
), ),
child: Row( child: Row(
children: [ children: [
for (var i = 0; for (
i < AudioPlayerWidget.wavesCount; var i = 0;
i++) i < AudioPlayerWidget.wavesCount;
i++
)
Expanded( Expanded(
child: Container( child: Container(
height: 32, height: 32,
@ -376,13 +378,14 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
child: Container( child: Container(
margin: margin:
const EdgeInsets.symmetric( const EdgeInsets.symmetric(
horizontal: 1, horizontal: 1,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: i < wavePosition color: i < wavePosition
? widget.color ? widget.color
: widget.color : widget.color.withAlpha(
.withAlpha(128), 128,
),
borderRadius: borderRadius:
BorderRadius.circular(64), BorderRadius.circular(64),
), ),
@ -396,7 +399,8 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
SizedBox( SizedBox(
height: 32, height: 32,
child: Slider( child: Slider(
thumbColor: widget.event.senderId == thumbColor:
widget.event.senderId ==
widget.event.room.client.userID widget.event.room.client.userID
? theme.colorScheme.onPrimary ? theme.colorScheme.onPrimary
: theme.colorScheme.primary, : theme.colorScheme.primary,
@ -425,10 +429,7 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
width: 36, width: 36,
child: Text( child: Text(
statusText, statusText,
style: TextStyle( style: TextStyle(color: widget.color, fontSize: 12),
color: widget.color,
fontSize: 12,
),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@ -442,11 +443,13 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
), ),
secondChild: Material( secondChild: Material(
color: widget.color.withAlpha(64), color: widget.color.withAlpha(64),
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(AppConfig.borderRadius), AppConfig.borderRadius,
),
child: InkWell( child: InkWell(
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(AppConfig.borderRadius), AppConfig.borderRadius,
),
onTap: _toggleSpeed, onTap: _toggleSpeed,
child: SizedBox( child: SizedBox(
width: 32, width: 32,
@ -481,8 +484,9 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
), ),
child: Linkify( child: Linkify(
text: fileDescription, text: fileDescription,
textScaleFactor: textScaleFactor: MediaQuery.textScalerOf(
MediaQuery.textScalerOf(context).scale(1), context,
).scale(1),
style: TextStyle( style: TextStyle(
color: widget.color, color: widget.color,
fontSize: widget.fontSize, fontSize: widget.fontSize,

View file

@ -37,13 +37,10 @@ class _CuteContentState extends State<CuteContent> {
return GestureDetector( return GestureDetector(
onTap: addOverlay, onTap: addOverlay,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
Text( Text(widget.event.text, style: const TextStyle(fontSize: 150)),
widget.event.text,
style: const TextStyle(fontSize: 150),
),
if (label != null) Text(label), if (label != null) Text(label),
], ],
), ),
@ -112,10 +109,7 @@ class _CuteEventOverlayState extends State<CuteEventOverlay>
with TickerProviderStateMixin { with TickerProviderStateMixin {
final List<Size> items = List.generate( final List<Size> items = List.generate(
50, 50,
(index) => Size( (index) => Size(Random().nextDouble(), 4 + (Random().nextDouble() * 4)),
Random().nextDouble(),
4 + (Random().nextDouble() * 4),
),
); );
AnimationController? controller; AnimationController? controller;
@ -150,14 +144,13 @@ class _CuteEventOverlayState extends State<CuteEventOverlay>
.map( .map(
(position) => Positioned( (position) => Positioned(
left: position.width * width, left: position.width * width,
bottom: (height * bottom:
(height *
.25 * .25 *
position.height * position.height *
(controller?.value ?? 0)) - (controller?.value ?? 0)) -
_CuteOverlayContent.size, _CuteOverlayContent.size,
child: _CuteOverlayContent( child: _CuteOverlayContent(emoji: widget.emoji),
emoji: widget.emoji,
),
), ),
) )
.toList(), .toList(),
@ -186,10 +179,7 @@ class _CuteOverlayContent extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox.square( return SizedBox.square(
dimension: size, dimension: size,
child: Text( child: Text(emoji, style: const TextStyle(fontSize: 48)),
emoji,
style: const TextStyle(fontSize: 48),
),
); );
} }
} }

View file

@ -141,7 +141,8 @@ class HtmlMessage extends StatelessWidget {
if (node is! dom.Element) { if (node is! dom.Element) {
return TextSpan(text: node.text); return TextSpan(text: node.text);
} }
final style = atomOneDarkTheme[node.className.split('-').last] ?? final style =
atomOneDarkTheme[node.className.split('-').last] ??
atomOneDarkTheme['root']; atomOneDarkTheme['root'];
return TextSpan( return TextSpan(
@ -151,11 +152,7 @@ class HtmlMessage extends StatelessWidget {
} }
/// Transforms a Node to an InlineSpan. /// Transforms a Node to an InlineSpan.
InlineSpan _renderHtml( InlineSpan _renderHtml(dom.Node node, BuildContext context, {int depth = 1}) {
dom.Node node,
BuildContext context, {
int depth = 1,
}) {
// We must not render elements nested more than 100 elements deep: // We must not render elements nested more than 100 elements deep:
if (depth >= 100) return const TextSpan(); if (depth >= 100) return const TextSpan();
@ -245,9 +242,9 @@ class HtmlMessage extends StatelessWidget {
final isCheckbox = node.className == 'task-list-item'; final isCheckbox = node.className == 'task-list-item';
final checkboxIndex = isCheckbox final checkboxIndex = isCheckbox
? node.rootElement ? node.rootElement
.getElementsByClassName('task-list-item') .getElementsByClassName('task-list-item')
.indexOf(node) + .indexOf(node) +
1 1
: null; : null;
final checkedByReaction = !isCheckbox final checkedByReaction = !isCheckbox
? null ? null
@ -283,7 +280,8 @@ class HtmlMessage extends StatelessWidget {
activeColor: textColor.withAlpha(64), activeColor: textColor.withAlpha(64),
value: value:
staticallyChecked || checkedByReaction != null, staticallyChecked || checkedByReaction != null,
onChanged: eventId == null || onChanged:
eventId == null ||
checkboxIndex == null || checkboxIndex == null ||
staticallyChecked || staticallyChecked ||
!room.canSendDefaultMessages || !room.canSendDefaultMessages ||
@ -292,25 +290,21 @@ class HtmlMessage extends StatelessWidget {
room.client.userID) room.client.userID)
? null ? null
: (_) => showFutureLoadingDialog( : (_) => showFutureLoadingDialog(
context: context, context: context,
future: () => checkedByReaction != null future: () => checkedByReaction != null
? room.redactEvent( ? room.redactEvent(
checkedByReaction.eventId, checkedByReaction.eventId,
) )
: room.checkCheckbox( : room.checkCheckbox(
eventId, eventId,
checkboxIndex, checkboxIndex,
), ),
), ),
), ),
), ),
), ),
), ),
..._renderWithLineBreaks( ..._renderWithLineBreaks(node.nodes, context, depth: depth),
node.nodes,
context,
depth: depth,
),
], ],
style: TextStyle(fontSize: fontSize, color: textColor), style: TextStyle(fontSize: fontSize, color: textColor),
), ),
@ -322,12 +316,7 @@ class HtmlMessage extends StatelessWidget {
child: Container( child: Container(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border( border: Border(left: BorderSide(color: textColor, width: 5)),
left: BorderSide(
color: textColor,
width: 5,
),
),
), ),
child: Text.rich( child: Text.rich(
TextSpan( TextSpan(
@ -347,7 +336,8 @@ class HtmlMessage extends StatelessWidget {
); );
case 'code': case 'code':
final isInline = node.parent?.localName != 'pre'; final isInline = node.parent?.localName != 'pre';
final lang = node.className final lang =
node.className
.split(' ') .split(' ')
.singleWhereOrNull( .singleWhereOrNull(
(className) => className.startsWith('language-'), (className) => className.startsWith('language-'),
@ -355,8 +345,9 @@ class HtmlMessage extends StatelessWidget {
?.split('language-') ?.split('language-')
.last ?? .last ??
'md'; 'md';
final highlightedHtml = final highlightedHtml = highlight
highlight.parse(node.text, language: lang).toHtml(); .parse(node.text, language: lang)
.toHtml();
final element = parser.parse(highlightedHtml).body; final element = parser.parse(highlightedHtml).body;
if (element == null) { if (element == null) {
return const TextSpan(text: 'Unable to render code block!'); return const TextSpan(text: 'Unable to render code block!');
@ -372,14 +363,9 @@ class HtmlMessage extends StatelessWidget {
child: Padding( child: Padding(
padding: isInline padding: isInline
? const EdgeInsets.symmetric(horizontal: 4.0) ? const EdgeInsets.symmetric(horizontal: 4.0)
: const EdgeInsets.symmetric( : const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
horizontal: 8.0,
vertical: 4.0,
),
child: Text.rich( child: Text.rich(
TextSpan( TextSpan(children: [_renderCodeBlockNode(element)]),
children: [_renderCodeBlockNode(element)],
),
selectionColor: hightlightTextColor.withAlpha(128), selectionColor: hightlightTextColor.withAlpha(128),
), ),
), ),
@ -448,10 +434,7 @@ class HtmlMessage extends StatelessWidget {
), ),
], ],
), ),
style: TextStyle( style: TextStyle(fontSize: fontSize, color: textColor),
fontSize: fontSize,
color: textColor,
),
), ),
), ),
), ),
@ -489,17 +472,13 @@ class HtmlMessage extends StatelessWidget {
default: default:
return TextSpan( return TextSpan(
style: switch (node.localName) { style: switch (node.localName) {
'body' => TextStyle( 'body' => TextStyle(fontSize: fontSize, color: textColor),
fontSize: fontSize,
color: textColor,
),
'a' => linkStyle, 'a' => linkStyle,
'strong' => const TextStyle(fontWeight: FontWeight.bold), 'strong' => const TextStyle(fontWeight: FontWeight.bold),
'em' || 'i' => const TextStyle(fontStyle: FontStyle.italic), 'em' || 'i' => const TextStyle(fontStyle: FontStyle.italic),
'del' || 'del' || 's' || 'strikethrough' => const TextStyle(
's' || decoration: TextDecoration.lineThrough,
'strikethrough' => ),
const TextStyle(decoration: TextDecoration.lineThrough),
'u' => const TextStyle(decoration: TextDecoration.underline), 'u' => const TextStyle(decoration: TextDecoration.underline),
'h1' => TextStyle(fontSize: fontSize * 1.6, height: 2), 'h1' => TextStyle(fontSize: fontSize * 1.6, height: 2),
'h2' => TextStyle(fontSize: fontSize * 1.5, height: 2), 'h2' => TextStyle(fontSize: fontSize * 1.5, height: 2),
@ -508,22 +487,19 @@ class HtmlMessage extends StatelessWidget {
'h5' => TextStyle(fontSize: fontSize * 1.2, height: 1.75), 'h5' => TextStyle(fontSize: fontSize * 1.2, height: 1.75),
'h6' => TextStyle(fontSize: fontSize * 1.1, height: 1.5), 'h6' => TextStyle(fontSize: fontSize * 1.1, height: 1.5),
'span' => TextStyle( 'span' => TextStyle(
color: node.attributes['color']?.hexToColor ?? color:
node.attributes['data-mx-color']?.hexToColor ?? node.attributes['color']?.hexToColor ??
textColor, node.attributes['data-mx-color']?.hexToColor ??
backgroundColor: textColor,
node.attributes['data-mx-bg-color']?.hexToColor, backgroundColor: node.attributes['data-mx-bg-color']?.hexToColor,
), ),
'sup' => 'sup' => const TextStyle(
const TextStyle(fontFeatures: [FontFeature.superscripts()]), fontFeatures: [FontFeature.superscripts()],
),
'sub' => const TextStyle(fontFeatures: [FontFeature.subscripts()]), 'sub' => const TextStyle(fontFeatures: [FontFeature.subscripts()]),
_ => null, _ => null,
}, },
children: _renderWithLineBreaks( children: _renderWithLineBreaks(node.nodes, context, depth: depth),
node.nodes,
context,
depth: depth,
),
); );
} }
} }
@ -533,10 +509,7 @@ class HtmlMessage extends StatelessWidget {
final element = parser.parse(html).body ?? dom.Element.html(''); final element = parser.parse(html).body ?? dom.Element.html('');
return Text.rich( return Text.rich(
_renderHtml(element, context), _renderHtml(element, context),
style: TextStyle( style: TextStyle(fontSize: fontSize, color: textColor),
fontSize: fontSize,
color: textColor,
),
maxLines: limitHeight ? 64 : null, maxLines: limitHeight ? 64 : null,
overflow: TextOverflow.fade, overflow: TextOverflow.fade,
selectionColor: textColor.withAlpha(128), selectionColor: textColor.withAlpha(128),
@ -568,13 +541,9 @@ class MatrixPill extends StatelessWidget {
splashColor: Colors.transparent, splashColor: Colors.transparent,
onTap: UrlLauncher(outerContext, uri).launchUrl, onTap: UrlLauncher(outerContext, uri).launchUrl,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Avatar( Avatar(mxContent: avatar, name: name, size: 16),
mxContent: avatar,
name: name,
size: 16,
),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
name, name,

View file

@ -46,8 +46,8 @@ class ImageBubble extends StatelessWidget {
Widget _buildPlaceholder(BuildContext context) { Widget _buildPlaceholder(BuildContext context) {
final String blurHashString = final String blurHashString =
event.infoMap['xyz.amorgan.blurhash'] is String event.infoMap['xyz.amorgan.blurhash'] is String
? event.infoMap['xyz.amorgan.blurhash'] ? event.infoMap['xyz.amorgan.blurhash']
: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj'; : 'LEHV6nWB2yk8pyo0adR*.7kCMdnj';
return SizedBox( return SizedBox(
width: width, width: width,
height: height, height: height,
@ -68,11 +68,8 @@ class ImageBubble extends StatelessWidget {
if (!tapToView) return; if (!tapToView) return;
showDialog( showDialog(
context: context, context: context,
builder: (_) => ImageViewer( builder: (_) =>
event, ImageViewer(event, timeline: timeline, outerContext: context),
timeline: timeline,
outerContext: context,
),
); );
} }
@ -94,7 +91,7 @@ class ImageBubble extends StatelessWidget {
} }
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
spacing: 8, spacing: 8,
children: [ children: [
Material( Material(
@ -131,22 +128,21 @@ class ImageBubble extends StatelessWidget {
SizedBox( SizedBox(
width: width, width: width,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
horizontal: 16,
vertical: 8,
),
child: Linkify( child: Linkify(
text: fileDescription, text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1), textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle( style: TextStyle(
color: textColor, color: textColor,
fontSize: AppSettings.fontSizeFactor.value * fontSize:
AppSettings.fontSizeFactor.value *
AppConfig.messageFontSize, AppConfig.messageFontSize,
), ),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: linkColor, color: linkColor,
fontSize: AppSettings.fontSizeFactor.value * fontSize:
AppSettings.fontSizeFactor.value *
AppConfig.messageFontSize, AppConfig.messageFontSize,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
decorationColor: linkColor, decorationColor: linkColor,

View file

@ -107,10 +107,12 @@ class Message extends StatelessWidget {
final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; final alignment = ownMessage ? Alignment.topRight : Alignment.topLeft;
var color = theme.colorScheme.surfaceContainerHigh; var color = theme.colorScheme.surfaceContainerHigh;
final displayTime = event.type == EventTypes.RoomCreate || final displayTime =
event.type == EventTypes.RoomCreate ||
nextEvent == null || nextEvent == null ||
!event.originServerTs.sameEnvironment(nextEvent!.originServerTs); !event.originServerTs.sameEnvironment(nextEvent!.originServerTs);
final nextEventSameSender = nextEvent != null && final nextEventSameSender =
nextEvent != null &&
{ {
EventTypes.Message, EventTypes.Message,
EventTypes.Sticker, EventTypes.Sticker,
@ -119,7 +121,8 @@ class Message extends StatelessWidget {
nextEvent!.senderId == event.senderId && nextEvent!.senderId == event.senderId &&
!displayTime; !displayTime;
final previousEventSameSender = previousEvent != null && final previousEventSameSender =
previousEvent != null &&
{ {
EventTypes.Message, EventTypes.Message,
EventTypes.Sticker, EventTypes.Sticker,
@ -128,17 +131,19 @@ class Message extends StatelessWidget {
previousEvent!.senderId == event.senderId && previousEvent!.senderId == event.senderId &&
previousEvent!.originServerTs.sameEnvironment(event.originServerTs); previousEvent!.originServerTs.sameEnvironment(event.originServerTs);
final textColor = final textColor = ownMessage
ownMessage ? theme.onBubbleColor : theme.colorScheme.onSurface; ? theme.onBubbleColor
: theme.colorScheme.onSurface;
final linkColor = ownMessage final linkColor = ownMessage
? theme.brightness == Brightness.light ? theme.brightness == Brightness.light
? theme.colorScheme.primaryFixed ? theme.colorScheme.primaryFixed
: theme.colorScheme.onTertiaryContainer : theme.colorScheme.onTertiaryContainer
: theme.colorScheme.primary; : theme.colorScheme.primary;
final rowMainAxisAlignment = final rowMainAxisAlignment = ownMessage
ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start; ? MainAxisAlignment.end
: MainAxisAlignment.start;
final displayEvent = event.getDisplayEvent(timeline); final displayEvent = event.getDisplayEvent(timeline);
const hardCorner = Radius.circular(4); const hardCorner = Radius.circular(4);
@ -146,12 +151,15 @@ class Message extends StatelessWidget {
final borderRadius = BorderRadius.only( final borderRadius = BorderRadius.only(
topLeft: !ownMessage && nextEventSameSender ? hardCorner : roundedCorner, topLeft: !ownMessage && nextEventSameSender ? hardCorner : roundedCorner,
topRight: ownMessage && nextEventSameSender ? hardCorner : roundedCorner, topRight: ownMessage && nextEventSameSender ? hardCorner : roundedCorner,
bottomLeft: bottomLeft: !ownMessage && previousEventSameSender
!ownMessage && previousEventSameSender ? hardCorner : roundedCorner, ? hardCorner
bottomRight: : roundedCorner,
ownMessage && previousEventSameSender ? hardCorner : roundedCorner, bottomRight: ownMessage && previousEventSameSender
? hardCorner
: roundedCorner,
); );
final noBubble = ({ final noBubble =
({
MessageTypes.Video, MessageTypes.Video,
MessageTypes.Image, MessageTypes.Image,
MessageTypes.Sticker, MessageTypes.Sticker,
@ -165,8 +173,9 @@ class Message extends StatelessWidget {
event.numberEmotes <= 3); event.numberEmotes <= 3);
if (ownMessage) { if (ownMessage) {
color = color = displayEvent.status.isError
displayEvent.status.isError ? Colors.redAccent : theme.bubbleColor; ? Colors.redAccent
: theme.bubbleColor;
} }
final resetAnimateIn = this.resetAnimateIn; final resetAnimateIn = this.resetAnimateIn;
@ -176,10 +185,7 @@ class Message extends StatelessWidget {
if (singleSelected) { if (singleSelected) {
sentReactions.addAll( sentReactions.addAll(
event event
.aggregatedEvents( .aggregatedEvents(timeline, RelationshipTypes.reaction)
timeline,
RelationshipTypes.reaction,
)
.where( .where(
(event) => (event) =>
event.senderId == event.room.client.userID && event.senderId == event.room.client.userID &&
@ -194,11 +200,15 @@ class Message extends StatelessWidget {
); );
} }
final showReceiptsRow = final showReceiptsRow = event.hasAggregatedEvents(
event.hasAggregatedEvents(timeline, RelationshipTypes.reaction); timeline,
RelationshipTypes.reaction,
);
final threadChildren = final threadChildren = event.aggregatedEvents(
event.aggregatedEvents(timeline, RelationshipTypes.thread); timeline,
RelationshipTypes.thread,
);
final showReactionPicker = final showReactionPicker =
singleSelected && event.room.canSendDefaultMessages; singleSelected && event.room.canSendDefaultMessages;
@ -210,9 +220,7 @@ class Message extends StatelessWidget {
key: ValueKey(event.eventId), key: ValueKey(event.eventId),
background: const Padding( background: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0), padding: EdgeInsets.symmetric(horizontal: 12.0),
child: Center( child: Center(child: Icon(Icons.check_outlined)),
child: Icon(Icons.check_outlined),
),
), ),
direction: AppSettings.swipeRightToLeftToReply.value direction: AppSettings.swipeRightToLeftToReply.value
? SwipeDirection.endToStart ? SwipeDirection.endToStart
@ -229,9 +237,8 @@ class Message extends StatelessWidget {
bottom: previousEventSameSender ? 1.0 : 4.0, bottom: previousEventSameSender ? 1.0 : 4.0,
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: crossAxisAlignment: ownMessage ? .end : .start,
ownMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
if (displayTime || selected) if (displayTime || selected)
Padding( Padding(
@ -242,8 +249,9 @@ class Message extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.only(top: 4.0), padding: const EdgeInsets.only(top: 4.0),
child: Material( child: Material(
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(AppConfig.borderRadius * 2), AppConfig.borderRadius * 2,
),
color: theme.colorScheme.surface.withAlpha(128), color: theme.colorScheme.surface.withAlpha(128),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -305,13 +313,13 @@ class Message extends StatelessWidget {
), ),
color: selected || highlightMarker color: selected || highlightMarker
? theme.colorScheme.secondaryContainer ? theme.colorScheme.secondaryContainer
.withAlpha(128) .withAlpha(128)
: Colors.transparent, : Colors.transparent,
), ),
), ),
), ),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
mainAxisAlignment: rowMainAxisAlignment, mainAxisAlignment: rowMainAxisAlignment,
children: [ children: [
if (longPressSelect && !event.redacted) if (longPressSelect && !event.redacted)
@ -336,18 +344,17 @@ class Message extends StatelessWidget {
child: SizedBox( child: SizedBox(
width: 16, width: 16,
height: 16, height: 16,
child: event.status == child:
EventStatus.error event.status == EventStatus.error
? const Icon( ? const Icon(
Icons.error, Icons.error,
color: Colors.red, color: Colors.red,
) )
: event.fileSendingStatus != null : event.fileSendingStatus != null
? const CircularProgressIndicator ? const CircularProgressIndicator.adaptive(
.adaptive( strokeWidth: 1,
strokeWidth: 1, )
) : null,
: null,
), ),
), ),
) )
@ -355,17 +362,18 @@ class Message extends StatelessWidget {
FutureBuilder<User?>( FutureBuilder<User?>(
future: event.fetchSenderUser(), future: event.fetchSenderUser(),
builder: (context, snapshot) { builder: (context, snapshot) {
final user = snapshot.data ?? final user =
snapshot.data ??
event.senderFromMemoryOrFallback; event.senderFromMemoryOrFallback;
return Avatar( return Avatar(
mxContent: user.avatarUrl, mxContent: user.avatarUrl,
name: user.calcDisplayname(), name: user.calcDisplayname(),
onTap: () => onTap: () =>
showMemberActionsPopupMenu( showMemberActionsPopupMenu(
context: context, context: context,
user: user, user: user,
onMention: onMention, onMention: onMention,
), ),
presenceUserId: user.stateKey, presenceUserId: user.stateKey,
presenceBackgroundColor: wallpaperMode presenceBackgroundColor: wallpaperMode
? Colors.transparent ? Colors.transparent
@ -375,9 +383,8 @@ class Message extends StatelessWidget {
), ),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment: .start,
CrossAxisAlignment.start, mainAxisSize: .min,
mainAxisSize: MainAxisSize.min,
children: [ children: [
if (!nextEventSameSender) if (!nextEventSameSender)
Padding( Padding(
@ -385,16 +392,16 @@ class Message extends StatelessWidget {
left: 8.0, left: 8.0,
bottom: 4, bottom: 4,
), ),
child: ownMessage || child:
ownMessage ||
event.room.isDirectChat event.room.isDirectChat
? const SizedBox(height: 12) ? const SizedBox(height: 12)
: FutureBuilder<User?>( : FutureBuilder<User?>(
future: future: event
event.fetchSenderUser(), .fetchSenderUser(),
builder: builder: (context, snapshot) {
(context, snapshot) { final displayname =
final displayname = snapshot snapshot.data
.data
?.calcDisplayname() ?? ?.calcDisplayname() ??
event event
.senderFromMemoryOrFallback .senderFromMemoryOrFallback
@ -405,29 +412,30 @@ class Message extends StatelessWidget {
fontSize: 11, fontSize: 11,
fontWeight: fontWeight:
FontWeight.bold, FontWeight.bold,
color: (theme.brightness == color:
(theme.brightness ==
Brightness Brightness
.light .light
? displayname ? displayname
.color .color
: displayname : displayname
.lightColorText), .lightColorText),
shadows: shadows:
!wallpaperMode !wallpaperMode
? null ? null
: [ : [
const Shadow( const Shadow(
offset: offset:
Offset( Offset(
0.0, 0.0,
0.0, 0.0,
), ),
blurRadius: blurRadius:
3, 3,
color: Colors color: Colors
.black, .black,
), ),
], ],
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow overflow: TextOverflow
@ -438,25 +446,25 @@ class Message extends StatelessWidget {
), ),
Container( Container(
alignment: alignment, alignment: alignment,
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(left: 8), left: 8,
),
child: GestureDetector( child: GestureDetector(
onLongPress: longPressSelect onLongPress: longPressSelect
? null ? null
: () { : () {
HapticFeedback HapticFeedback.heavyImpact();
.heavyImpact();
onSelect(event); onSelect(event);
}, },
child: AnimatedOpacity( child: AnimatedOpacity(
opacity: animateIn opacity: animateIn
? 0 ? 0
: event.messageType == : event.messageType ==
MessageTypes MessageTypes
.BadEncrypted || .BadEncrypted ||
event.status.isSending event.status.isSending
? 0.5 ? 0.5
: 1, : 1,
duration: FluffyThemes duration: FluffyThemes
.animationDuration, .animationDuration,
curve: curve:
@ -471,7 +479,8 @@ class Message extends StatelessWidget {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: BubbleBackground( child: BubbleBackground(
colors: colors, colors: colors,
ignore: noBubble || ignore:
noBubble ||
!ownMessage || !ownMessage ||
MediaQuery.highContrastOf( MediaQuery.highContrastOf(
context, context,
@ -482,24 +491,24 @@ class Message extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: borderRadius:
BorderRadius.circular( BorderRadius.circular(
AppConfig.borderRadius, AppConfig
), .borderRadius,
),
), ),
constraints: constraints:
const BoxConstraints( const BoxConstraints(
maxWidth: FluffyThemes maxWidth:
.columnWidth * FluffyThemes
1.5, .columnWidth *
), 1.5,
),
child: Column( child: Column(
mainAxisSize: mainAxisSize: .min,
MainAxisSize.min,
crossAxisAlignment: crossAxisAlignment:
CrossAxisAlignment CrossAxisAlignment
.start, .start,
children: <Widget>[ children: <Widget>[
if (event if (event.inReplyToEventId(
.inReplyToEventId(
includingFallback: includingFallback:
false, false,
) != ) !=
@ -507,21 +516,23 @@ class Message extends StatelessWidget {
FutureBuilder<Event?>( FutureBuilder<Event?>(
future: event future: event
.getReplyEvent( .getReplyEvent(
timeline, timeline,
), ),
builder: ( builder:
BuildContext (
BuildContext
context, context,
snapshot, snapshot,
) { ) {
final replyEvent = final replyEvent =
snapshot snapshot
.hasData .hasData
? snapshot ? snapshot
.data! .data!
: Event( : Event(
eventId: eventId:
event.inReplyToEventId() ?? '\$fake_event_id', event.inReplyToEventId() ??
'\$fake_event_id',
content: { content: {
'msgtype': 'msgtype':
'm.text', 'm.text',
@ -539,45 +550,42 @@ class Message extends StatelessWidget {
originServerTs: originServerTs:
DateTime.now(), DateTime.now(),
); );
return Padding( return Padding(
padding: padding:
const EdgeInsets const EdgeInsets.only(
.only( left:
left: 16, 16,
right: 16, right:
top: 8, 16,
), top:
child: Material( 8,
color: Colors ),
.transparent, child: Material(
borderRadius: color: Colors
ReplyContent .transparent,
.borderRadius, borderRadius:
child: ReplyContent
InkWell( .borderRadius,
borderRadius: child: InkWell(
ReplyContent borderRadius:
.borderRadius, ReplyContent.borderRadius,
onTap: () => onTap: () => scrollToEventId(
scrollToEventId( replyEvent
replyEvent .eventId,
.eventId, ),
), child: AbsorbPointer(
child: child: ReplyContent(
AbsorbPointer( replyEvent,
child: ownMessage:
ReplyContent( ownMessage,
replyEvent, timeline:
ownMessage: timeline,
ownMessage, ),
timeline: ),
timeline,
), ),
), ),
), );
), },
);
},
), ),
MessageContent( MessageContent(
displayEvent, displayEvent,
@ -591,18 +599,17 @@ class Message extends StatelessWidget {
), ),
if (event if (event
.hasAggregatedEvents( .hasAggregatedEvents(
timeline, timeline,
RelationshipTypes RelationshipTypes
.edit, .edit,
)) ))
Padding( Padding(
padding: padding:
const EdgeInsets const EdgeInsets.only(
.only( bottom: 8.0,
bottom: 8.0, left: 16.0,
left: 16.0, right: 16.0,
right: 16.0, ),
),
child: Row( child: Row(
mainAxisSize: mainAxisSize:
MainAxisSize MainAxisSize
@ -614,22 +621,21 @@ class Message extends StatelessWidget {
.edit_outlined, .edit_outlined,
color: textColor color: textColor
.withAlpha( .withAlpha(
164, 164,
), ),
size: 14, size: 14,
), ),
Text( Text(
displayEvent displayEvent
.originServerTs .originServerTs
.localizedTimeShort( .localizedTimeShort(
context, context,
), ),
style: style: TextStyle(
TextStyle(
color: textColor color: textColor
.withAlpha( .withAlpha(
164, 164,
), ),
fontSize: fontSize:
11, 11,
), ),
@ -657,46 +663,43 @@ class Message extends StatelessWidget {
? Padding( ? Padding(
padding: padding:
const EdgeInsets.all( const EdgeInsets.all(
4.0, 4.0,
), ),
child: Material( child: Material(
elevation: 4, elevation: 4,
borderRadius: borderRadius:
BorderRadius.circular( BorderRadius.circular(
AppConfig.borderRadius, AppConfig
), .borderRadius,
),
shadowColor: theme shadowColor: theme
.colorScheme.surface .colorScheme
.surface
.withAlpha(128), .withAlpha(128),
child: child: SingleChildScrollView(
SingleChildScrollView(
scrollDirection: scrollDirection:
Axis.horizontal, Axis.horizontal,
child: Row( child: Row(
mainAxisSize: mainAxisSize: .min,
MainAxisSize.min,
children: [ children: [
...AppConfig ...AppConfig.defaultReactions.map(
.defaultReactions (
.map( emoji,
(emoji) => ) => IconButton(
IconButton(
padding: padding:
EdgeInsets EdgeInsets
.zero, .zero,
icon: Center( icon: Center(
child: child: Opacity(
Opacity( opacity:
opacity: sentReactions sentReactions.contains(
.contains( emoji,
emoji, )
)
? 0.33 ? 0.33
: 1, : 1,
child: Text( child: Text(
emoji, emoji,
style: style: const TextStyle(
const TextStyle(
fontSize: fontSize:
20, 20,
), ),
@ -708,19 +711,20 @@ class Message extends StatelessWidget {
), ),
onPressed: onPressed:
sentReactions sentReactions
.contains( .contains(
emoji, emoji,
) )
? null ? null
: () { : () {
onSelect( onSelect(
event, event,
); );
event.room.sendReaction( event.room.sendReaction(
event.eventId, event
emoji, .eventId,
); emoji,
}, );
},
), ),
), ),
IconButton( IconButton(
@ -731,72 +735,55 @@ class Message extends StatelessWidget {
tooltip: L10n.of( tooltip: L10n.of(
context, context,
).customReaction, ).customReaction,
onPressed: onPressed: () async {
() async { final emoji = await showAdaptiveBottomSheet<String>(
final emoji =
await showAdaptiveBottomSheet<
String>(
context: context:
context, context,
builder: builder: (context) => Scaffold(
(context) => appBar: AppBar(
Scaffold( title: Text(
appBar: L10n.of(
AppBar(
title:
Text(
L10n.of(context)
.customReaction,
),
leading:
CloseButton(
onPressed:
() =>
Navigator.of(
context, context,
).pop( ).customReaction,
null, ),
), leading: CloseButton(
onPressed: () => Navigator.of(
context,
).pop(null),
), ),
), ),
body: body: SizedBox(
SizedBox(
height: double height: double
.infinity, .infinity,
child: child: EmojiPicker(
EmojiPicker( onEmojiSelected:
onEmojiSelected: ( (
_, _,
emoji, emoji,
) => ) =>
Navigator.of( Navigator.of(
context, context,
).pop( ).pop(
emoji emoji.emoji,
.emoji, ),
), config: Config(
config: locale: Localizations.localeOf(
Config( context,
locale: ),
Localizations.localeOf(context), emojiViewConfig: const EmojiViewConfig(
emojiViewConfig:
const EmojiViewConfig(
backgroundColor: backgroundColor:
Colors.transparent, Colors.transparent,
), ),
bottomActionBarConfig: bottomActionBarConfig: const BottomActionBarConfig(
const BottomActionBarConfig(
enabled: enabled:
false, false,
), ),
categoryViewConfig: categoryViewConfig: CategoryViewConfig(
CategoryViewConfig(
initCategory: initCategory:
Category.SMILEYS, Category.SMILEYS,
backspaceColor: backspaceColor:
theme.colorScheme.primary, theme.colorScheme.primary,
iconColor: iconColor: theme.colorScheme.primary.withAlpha(
theme.colorScheme.primary.withAlpha(
128, 128,
), ),
iconColorSelected: iconColorSelected:
@ -806,10 +793,8 @@ class Message extends StatelessWidget {
backgroundColor: backgroundColor:
theme.colorScheme.surface, theme.colorScheme.surface,
), ),
skinToneConfig: skinToneConfig: SkinToneConfig(
SkinToneConfig( dialogBackgroundColor: Color.lerp(
dialogBackgroundColor:
Color.lerp(
theme.colorScheme.surface, theme.colorScheme.surface,
theme.colorScheme.primaryContainer, theme.colorScheme.primaryContainer,
0.75, 0.75,
@ -828,17 +813,18 @@ class Message extends StatelessWidget {
} }
if (sentReactions if (sentReactions
.contains( .contains(
emoji, emoji,
)) { )) {
return; return;
} }
onSelect(event); onSelect(event);
await event.room await event.room
.sendReaction( .sendReaction(
event.eventId, event
emoji, .eventId,
); emoji,
);
}, },
), ),
], ],
@ -904,10 +890,7 @@ class Message extends StatelessWidget {
onPressed: () => enterThread(event.eventId), onPressed: () => enterThread(event.eventId),
icon: const Icon(Icons.message), icon: const Icon(Icons.message),
label: Text( label: Text(
'${L10n.of(context).countReplies(threadChildren.length)} | ${threadChildren.first.calcLocalizedBodyFallback( '${L10n.of(context).countReplies(threadChildren.length)} | ${threadChildren.first.calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)), withSenderNamePrefix: true)}',
MatrixLocals(L10n.of(context)),
withSenderNamePrefix: true,
)}',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -933,8 +916,9 @@ class Message extends StatelessWidget {
vertical: 2, vertical: 2,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(AppConfig.borderRadius / 3), AppConfig.borderRadius / 3,
),
color: theme.colorScheme.surface.withAlpha(128), color: theme.colorScheme.surface.withAlpha(128),
), ),
child: Text( child: Text(
@ -1005,8 +989,10 @@ class BubblePainter extends CustomPainter {
final scrollableRect = Offset.zero & scrollableBox.size; final scrollableRect = Offset.zero & scrollableBox.size;
final bubbleBox = context.findRenderObject() as RenderBox; final bubbleBox = context.findRenderObject() as RenderBox;
final origin = final origin = bubbleBox.localToGlobal(
bubbleBox.localToGlobal(Offset.zero, ancestor: scrollableBox); Offset.zero,
ancestor: scrollableBox,
);
final paint = Paint() final paint = Paint()
..shader = ui.Gradient.linear( ..shader = ui.Gradient.linear(
scrollableRect.topCenter, scrollableRect.topCenter,

View file

@ -50,9 +50,7 @@ class MessageContent extends StatelessWidget {
if (event.content['can_request_session'] != true) { if (event.content['can_request_session'] != true) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(event.calcLocalizedBodyFallback(MatrixLocals(l10n))),
event.calcLocalizedBodyFallback(MatrixLocals(l10n)),
),
), ),
); );
return; return;
@ -91,11 +89,7 @@ class MessageContent extends StatelessWidget {
trailing: const Icon(Icons.lock_outlined), trailing: const Icon(Icons.lock_outlined),
), ),
const Divider(), const Divider(),
Text( Text(event.calcLocalizedBodyFallback(MatrixLocals(l10n))),
event.calcLocalizedBodyFallback(
MatrixLocals(l10n),
),
),
], ],
), ),
), ),
@ -116,8 +110,9 @@ class MessageContent extends StatelessWidget {
case MessageTypes.Image: case MessageTypes.Image:
case MessageTypes.Sticker: case MessageTypes.Sticker:
if (event.redacted) continue textmessage; if (event.redacted) continue textmessage;
final maxSize = final maxSize = event.messageType == MessageTypes.Sticker
event.messageType == MessageTypes.Sticker ? 128.0 : 256.0; ? 128.0
: 256.0;
final w = event.content final w = event.content
.tryGetMap<String, Object?>('info') .tryGetMap<String, Object?>('info')
?.tryGet<int>('w'); ?.tryGet<int>('w');
@ -152,12 +147,12 @@ class MessageContent extends StatelessWidget {
return CuteContent(event); return CuteContent(event);
case MessageTypes.Audio: case MessageTypes.Audio:
if (PlatformInfos.isMobile || if (PlatformInfos.isMobile ||
PlatformInfos.isMacOS || PlatformInfos.isMacOS ||
PlatformInfos.isWeb PlatformInfos.isWeb
// Disabled until https://github.com/bleonard252/just_audio_mpv/issues/3 // Disabled until https://github.com/bleonard252/just_audio_mpv/issues/3
// is fixed // is fixed
// || PlatformInfos.isLinux // || PlatformInfos.isLinux
) { ) {
return AudioPlayerWidget( return AudioPlayerWidget(
event, event,
color: textColor, color: textColor,
@ -193,8 +188,9 @@ class MessageContent extends StatelessWidget {
fontSize: fontSize, fontSize: fontSize,
); );
case MessageTypes.Location: case MessageTypes.Location:
final geoUri = final geoUri = Uri.tryParse(
Uri.tryParse(event.content.tryGet<String>('geo_uri')!); event.content.tryGet<String>('geo_uri')!,
);
if (geoUri != null && geoUri.scheme == 'geo') { if (geoUri != null && geoUri.scheme == 'geo') {
final latlong = geoUri.path final latlong = geoUri.path
.split(';') .split(';')
@ -206,7 +202,7 @@ class MessageContent extends StatelessWidget {
latlong.first != null && latlong.first != null &&
latlong.last != null) { latlong.last != null) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
MapBubble( MapBubble(
latitude: latlong.first!, latitude: latlong.first!,
@ -215,8 +211,10 @@ class MessageContent extends StatelessWidget {
const SizedBox(height: 6), const SizedBox(height: 6),
OutlinedButton.icon( OutlinedButton.icon(
icon: Icon(Icons.location_on_outlined, color: textColor), icon: Icon(Icons.location_on_outlined, color: textColor),
onPressed: onPressed: UrlLauncher(
UrlLauncher(context, geoUri.toString()).launchUrl, context,
geoUri.toString(),
).launchUrl,
label: Text( label: Text(
L10n.of(context).openInMaps, L10n.of(context).openInMaps,
style: TextStyle(color: textColor), style: TextStyle(color: textColor),
@ -248,25 +246,25 @@ class MessageContent extends StatelessWidget {
html = '* $html'; html = '* $html';
} }
final bigEmotes = event.onlyEmotes && final bigEmotes =
event.onlyEmotes &&
event.numberEmotes > 0 && event.numberEmotes > 0 &&
event.numberEmotes <= 3; event.numberEmotes <= 3;
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
horizontal: 16,
vertical: 8,
),
child: HtmlMessage( child: HtmlMessage(
html: html, html: html,
textColor: textColor, textColor: textColor,
room: event.room, room: event.room,
fontSize: AppSettings.fontSizeFactor.value * fontSize:
AppSettings.fontSizeFactor.value *
AppConfig.messageFontSize * AppConfig.messageFontSize *
(bigEmotes ? 5 : 1), (bigEmotes ? 5 : 1),
limitHeight: !selected, limitHeight: !selected,
linkStyle: TextStyle( linkStyle: TextStyle(
color: linkColor, color: linkColor,
fontSize: AppSettings.fontSizeFactor.value * fontSize:
AppSettings.fontSizeFactor.value *
AppConfig.messageFontSize, AppConfig.messageFontSize,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
decorationColor: linkColor, decorationColor: linkColor,
@ -352,16 +350,14 @@ class RedactionWidget extends StatelessWidget {
future: event.redactedBecause?.fetchSenderUser(), future: event.redactedBecause?.fetchSenderUser(),
builder: (context, snapshot) { builder: (context, snapshot) {
final reason = event.redactedBecause?.content.tryGet<String>('reason'); final reason = event.redactedBecause?.content.tryGet<String>('reason');
final redactedBy = snapshot.data?.calcDisplayname() ?? final redactedBy =
snapshot.data?.calcDisplayname() ??
event.redactedBecause?.senderId.localpart ?? event.redactedBecause?.senderId.localpart ??
L10n.of(context).user; L10n.of(context).user;
return _ButtonContent( return _ButtonContent(
label: reason == null label: reason == null
? L10n.of(context).redactedBy(redactedBy) ? L10n.of(context).redactedBy(redactedBy)
: L10n.of(context).redactedByBecause( : L10n.of(context).redactedByBecause(redactedBy, reason),
redactedBy,
reason,
),
icon: '🗑️', icon: '🗑️',
textColor: buttonTextColor.withAlpha(128), textColor: buttonTextColor.withAlpha(128),
onPressed: () => onInfoTab!(event), onPressed: () => onInfoTab!(event),
@ -390,18 +386,12 @@ class _ButtonContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
horizontal: 16,
vertical: 8,
),
child: InkWell( child: InkWell(
onTap: onPressed, onTap: onPressed,
child: Text( child: Text(
'$icon $label', '$icon $label',
style: TextStyle( style: TextStyle(color: textColor, fontSize: fontSize),
color: textColor,
fontSize: fontSize,
),
), ),
), ),
); );

View file

@ -27,15 +27,15 @@ class MessageDownloadContent extends StatelessWidget {
final filetype = (filename.contains('.') final filetype = (filename.contains('.')
? filename.split('.').last.toUpperCase() ? filename.split('.').last.toUpperCase()
: event.content : event.content
.tryGetMap<String, dynamic>('info') .tryGetMap<String, dynamic>('info')
?.tryGet<String>('mimetype') ?.tryGet<String>('mimetype')
?.toUpperCase() ?? ?.toUpperCase() ??
'UNKNOWN'); 'UNKNOWN');
final sizeString = event.sizeString ?? '?MB'; final sizeString = event.sizeString ?? '?MB';
final fileDescription = event.fileDescription; final fileDescription = event.fileDescription;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
spacing: 8, spacing: 8,
children: [ children: [
Material( Material(
@ -47,7 +47,7 @@ class MessageDownloadContent extends StatelessWidget {
width: 400, width: 400,
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
spacing: 16, spacing: 16,
children: [ children: [
CircleAvatar( CircleAvatar(
@ -56,8 +56,8 @@ class MessageDownloadContent extends StatelessWidget {
), ),
Flexible( Flexible(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Text( Text(
filename, filename,
@ -93,13 +93,15 @@ class MessageDownloadContent extends StatelessWidget {
textScaleFactor: MediaQuery.textScalerOf(context).scale(1), textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle( style: TextStyle(
color: textColor, color: textColor,
fontSize: AppSettings.fontSizeFactor.value * fontSize:
AppSettings.fontSizeFactor.value *
AppConfig.messageFontSize, AppConfig.messageFontSize,
), ),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: linkColor, color: linkColor,
fontSize: AppSettings.fontSizeFactor.value * fontSize:
AppSettings.fontSizeFactor.value *
AppConfig.messageFontSize, AppConfig.messageFontSize,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
decorationColor: linkColor, decorationColor: linkColor,

View file

@ -17,8 +17,10 @@ class MessageReactions extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final allReactionEvents = final allReactionEvents = event.aggregatedEvents(
event.aggregatedEvents(timeline, RelationshipTypes.reaction); timeline,
RelationshipTypes.reaction,
);
final reactionMap = <String, _ReactionEntry>{}; final reactionMap = <String, _ReactionEntry>{};
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
@ -113,7 +115,7 @@ class _Reaction extends StatelessWidget {
Widget content; Widget content;
if (reactionKey.startsWith('mxc://')) { if (reactionKey.startsWith('mxc://')) {
content = Row( content = Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: <Widget>[ children: <Widget>[
MxcImage( MxcImage(
uri: Uri.parse(reactionKey), uri: Uri.parse(reactionKey),
@ -190,17 +192,14 @@ class _AdaptableReactorsDialog extends StatelessWidget {
final Client? client; final Client? client;
final _ReactionEntry? reactionEntry; final _ReactionEntry? reactionEntry;
const _AdaptableReactorsDialog({ const _AdaptableReactorsDialog({this.client, this.reactionEntry});
this.client,
this.reactionEntry,
});
Future<bool?> show(BuildContext context) => showAdaptiveDialog( Future<bool?> show(BuildContext context) => showAdaptiveDialog(
context: context, context: context,
builder: (context) => this, builder: (context) => this,
barrierDismissible: true, barrierDismissible: true,
useRootNavigator: false, useRootNavigator: false,
); );
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -226,9 +225,6 @@ class _AdaptableReactorsDialog extends StatelessWidget {
final title = Center(child: Text(reactionEntry!.key)); final title = Center(child: Text(reactionEntry!.key));
return AlertDialog.adaptive( return AlertDialog.adaptive(title: title, content: body);
title: title,
content: body,
);
} }
} }

View file

@ -24,10 +24,8 @@ class PollWidget extends StatelessWidget {
super.key, super.key,
}); });
void _endPoll(BuildContext context) => showFutureLoadingDialog( void _endPoll(BuildContext context) =>
context: context, showFutureLoadingDialog(context: context, future: () => event.endPoll());
future: () => event.endPoll(),
);
void _toggleVote( void _toggleVote(
BuildContext context, BuildContext context,
@ -60,20 +58,19 @@ class PollWidget extends StatelessWidget {
} }
final responses = event.getPollResponses(timeline); final responses = event.getPollResponses(timeline);
final pollHasBeenEnded = event.getPollHasBeenEnded(timeline); final pollHasBeenEnded = event.getPollHasBeenEnded(timeline);
final canVote = event.room.canSendEvent(PollEventContent.responseType) && final canVote =
event.room.canSendEvent(PollEventContent.responseType) &&
!pollHasBeenEnded; !pollHasBeenEnded;
final maxPolls = responses.length; final maxPolls = responses.length;
final answersVisible = final answersVisible =
eventContent.pollStartContent.kind == PollKind.disclosed || eventContent.pollStartContent.kind == PollKind.disclosed ||
pollHasBeenEnded; pollHasBeenEnded;
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(vertical: 8),
vertical: 8,
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
@ -82,13 +79,15 @@ class PollWidget extends StatelessWidget {
textScaleFactor: MediaQuery.textScalerOf(context).scale(1), textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle( style: TextStyle(
color: textColor, color: textColor,
fontSize: AppSettings.fontSizeFactor.value * fontSize:
AppSettings.fontSizeFactor.value *
AppConfig.messageFontSize, AppConfig.messageFontSize,
), ),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: linkColor, color: linkColor,
fontSize: AppSettings.fontSizeFactor.value * fontSize:
AppSettings.fontSizeFactor.value *
AppConfig.messageFontSize, AppConfig.messageFontSize,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
decorationColor: linkColor, decorationColor: linkColor,
@ -97,99 +96,101 @@ class PollWidget extends StatelessWidget {
), ),
), ),
Divider(color: linkColor.withAlpha(64)), Divider(color: linkColor.withAlpha(64)),
...eventContent.pollStartContent.answers.map( ...eventContent.pollStartContent.answers.map((answer) {
(answer) { final votedUserIds = responses.entries
final votedUserIds = responses.entries .where((entry) => entry.value.contains(answer.id))
.where((entry) => entry.value.contains(answer.id)) .map((entry) => entry.key)
.map((entry) => entry.key) .toSet();
.toSet(); return Material(
return Material( color: Colors.transparent,
color: Colors.transparent, clipBehavior: Clip.hardEdge,
clipBehavior: Clip.hardEdge, child: CheckboxListTile.adaptive(
child: CheckboxListTile.adaptive( value:
value: responses[event.room.client.userID!] responses[event.room.client.userID!]?.contains(answer.id) ??
?.contains(answer.id) ?? false,
false, contentPadding: const EdgeInsets.symmetric(horizontal: 16),
contentPadding: const EdgeInsets.symmetric(horizontal: 16), checkboxScaleFactor: 1.5,
checkboxScaleFactor: 1.5, checkboxShape: RoundedRectangleBorder(
checkboxShape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(32),
borderRadius: BorderRadius.circular(32), ),
onChanged: !canVote
? null
: (_) => _toggleVote(
context,
answer.id,
eventContent.pollStartContent.maxSelections,
),
title: Text(
answer.mText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: textColor,
fontSize:
AppConfig.messageFontSize *
AppSettings.fontSizeFactor.value,
), ),
onChanged: !canVote ),
? null subtitle: answersVisible
: (_) => _toggleVote( ? Column(
context, crossAxisAlignment: .start,
answer.id, mainAxisSize: .min,
eventContent.pollStartContent.maxSelections, children: [
), SingleChildScrollView(
title: Text( scrollDirection: Axis.horizontal,
answer.mText, child: Row(
maxLines: 1, children: [
overflow: TextOverflow.ellipsis, Text(
style: TextStyle( L10n.of(
color: textColor, context,
fontSize: AppConfig.messageFontSize * ).countVotes(votedUserIds.length),
AppSettings.fontSizeFactor.value, maxLines: 1,
), overflow: TextOverflow.ellipsis,
), style: TextStyle(
subtitle: answersVisible color: linkColor,
? Column( fontSize:
crossAxisAlignment: CrossAxisAlignment.start, 12 * AppSettings.fontSizeFactor.value,
mainAxisSize: MainAxisSize.min, ),
children: [ ),
SingleChildScrollView( const SizedBox(width: 2),
scrollDirection: Axis.horizontal, ...votedUserIds.map((userId) {
child: Row( final user = event.room
children: [ .getState(EventTypes.RoomMember, userId)
Text( ?.asUser(event.room);
L10n.of(context) return Padding(
.countVotes(votedUserIds.length), padding: const EdgeInsets.symmetric(
maxLines: 1, horizontal: 2.0,
overflow: TextOverflow.ellipsis, ),
style: TextStyle( child: Avatar(
color: linkColor, mxContent: user?.avatarUrl,
fontSize: name:
user?.calcDisplayname() ??
userId.localpart,
size:
12 * AppSettings.fontSizeFactor.value, 12 * AppSettings.fontSizeFactor.value,
), ),
), );
const SizedBox(width: 2), }),
...votedUserIds.map((userId) { const SizedBox(width: 2),
final user = event.room ],
.getState(EventTypes.RoomMember, userId)
?.asUser(event.room);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 2.0,
),
child: Avatar(
mxContent: user?.avatarUrl,
name: user?.calcDisplayname() ??
userId.localpart,
size: 12 *
AppSettings.fontSizeFactor.value,
),
);
}),
const SizedBox(width: 2),
],
),
), ),
LinearProgressIndicator( ),
color: linkColor, LinearProgressIndicator(
backgroundColor: linkColor.withAlpha(128), color: linkColor,
borderRadius: backgroundColor: linkColor.withAlpha(128),
BorderRadius.circular(AppConfig.borderRadius), borderRadius: BorderRadius.circular(
value: maxPolls == 0 AppConfig.borderRadius,
? 0
: votedUserIds.length / maxPolls,
), ),
], value: maxPolls == 0
) ? 0
: null, : votedUserIds.length / maxPolls,
), ),
); ],
}, )
), : null,
),
);
}),
if (!pollHasBeenEnded && event.senderId == event.room.client.userID) if (!pollHasBeenEnded && event.senderId == event.room.client.userID)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),

View file

@ -29,21 +29,22 @@ class ReplyContent extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final timeline = this.timeline; final timeline = this.timeline;
final displayEvent = final displayEvent = timeline != null
timeline != null ? replyEvent.getDisplayEvent(timeline) : replyEvent; ? replyEvent.getDisplayEvent(timeline)
: replyEvent;
final fontSize = final fontSize =
AppConfig.messageFontSize * AppSettings.fontSizeFactor.value; AppConfig.messageFontSize * AppSettings.fontSizeFactor.value;
final color = theme.brightness == Brightness.dark final color = theme.brightness == Brightness.dark
? theme.colorScheme.onTertiaryContainer ? theme.colorScheme.onTertiaryContainer
: ownMessage : ownMessage
? theme.colorScheme.tertiaryContainer ? theme.colorScheme.tertiaryContainer
: theme.colorScheme.tertiary; : theme.colorScheme.tertiary;
return Material( return Material(
color: Colors.transparent, color: Colors.transparent,
borderRadius: borderRadius, borderRadius: borderRadius,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: <Widget>[ children: <Widget>[
Container( Container(
width: 5, width: 5,
@ -56,8 +57,8 @@ class ReplyContent extends StatelessWidget {
const SizedBox(width: 6), const SizedBox(width: 6),
Flexible( Flexible(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: <Widget>[ children: <Widget>[
FutureBuilder<User?>( FutureBuilder<User?>(
initialData: displayEvent.senderFromMemoryOrFallback, initialData: displayEvent.senderFromMemoryOrFallback,
@ -87,8 +88,8 @@ class ReplyContent extends StatelessWidget {
color: theme.brightness == Brightness.dark color: theme.brightness == Brightness.dark
? theme.colorScheme.onSurface ? theme.colorScheme.onSurface
: ownMessage : ownMessage
? theme.colorScheme.onTertiary ? theme.colorScheme.onTertiary
: theme.colorScheme.onSurface, : theme.colorScheme.onSurface,
fontSize: fontSize, fontSize: fontSize,
), ),
), ),

View file

@ -30,7 +30,7 @@ class RoomCreationStateEvent extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Avatar( Avatar(
mxContent: event.room.avatar, mxContent: event.room.avatar,

View file

@ -35,8 +35,9 @@ class StateMessage extends StatelessWidget {
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
child: Material( child: Material(
color: theme.colorScheme.surface.withAlpha(128), color: theme.colorScheme.surface.withAlpha(128),
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(AppConfig.borderRadius / 3), AppConfig.borderRadius / 3,
),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8.0, horizontal: 8.0,

View file

@ -35,8 +35,10 @@ class EventVideoPlayer extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final supportsVideoPlayer = PlatformInfos.supportsVideoPlayer; final supportsVideoPlayer = PlatformInfos.supportsVideoPlayer;
final blurHash = (event.infoMap as Map<String, dynamic>) final blurHash =
.tryGet<String>('xyz.amorgan.blurhash') ?? (event.infoMap as Map<String, dynamic>).tryGet<String>(
'xyz.amorgan.blurhash',
) ??
fallbackBlurHash; fallbackBlurHash;
final fileDescription = event.fileDescription; final fileDescription = event.fileDescription;
const maxDimension = 300.0; const maxDimension = 300.0;
@ -49,11 +51,12 @@ class EventVideoPlayer extends StatelessWidget {
final height = videoHeight / modifier; final height = videoHeight / modifier;
final durationInt = infoMap?.tryGet<int>('duration'); final durationInt = infoMap?.tryGet<int>('duration');
final duration = final duration = durationInt == null
durationInt == null ? null : Duration(milliseconds: durationInt); ? null
: Duration(milliseconds: durationInt);
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
spacing: 8, spacing: 8,
children: [ children: [
Material( Material(
@ -128,22 +131,21 @@ class EventVideoPlayer extends StatelessWidget {
SizedBox( SizedBox(
width: width, width: width,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
horizontal: 16,
vertical: 8,
),
child: Linkify( child: Linkify(
text: fileDescription, text: fileDescription,
textScaleFactor: MediaQuery.textScalerOf(context).scale(1), textScaleFactor: MediaQuery.textScalerOf(context).scale(1),
style: TextStyle( style: TextStyle(
color: textColor, color: textColor,
fontSize: AppSettings.fontSizeFactor.value * fontSize:
AppSettings.fontSizeFactor.value *
AppConfig.messageFontSize, AppConfig.messageFontSize,
), ),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: linkColor, color: linkColor,
fontSize: AppSettings.fontSizeFactor.value * fontSize:
AppSettings.fontSizeFactor.value *
AppConfig.messageFontSize, AppConfig.messageFontSize,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
decorationColor: linkColor, decorationColor: linkColor,

View file

@ -62,10 +62,7 @@ class InputBar extends StatelessWidget {
final commandSearch = commandMatch[1]!.toLowerCase(); final commandSearch = commandMatch[1]!.toLowerCase();
for (final command in room.client.commands.keys) { for (final command in room.client.commands.keys) {
if (command.contains(commandSearch)) { if (command.contains(commandSearch)) {
ret.add({ ret.add({'type': 'command', 'name': command});
'type': 'command',
'name': command,
});
} }
if (ret.length > maxResults) return ret; if (ret.length > maxResults) return ret;
@ -107,8 +104,8 @@ class InputBar extends StatelessWidget {
'type': 'emote', 'type': 'emote',
'name': emote.key, 'name': emote.key,
'pack': packSearch, 'pack': packSearch,
'pack_avatar_url': 'pack_avatar_url': emotePacks[packSearch]!.pack.avatarUrl
emotePacks[packSearch]!.pack.avatarUrl?.toString(), ?.toString(),
'pack_display_name': 'pack_display_name':
emotePacks[packSearch]!.pack.displayName ?? packSearch, emotePacks[packSearch]!.pack.displayName ?? packSearch,
'mxc': emote.value.url.toString(), 'mxc': emote.value.url.toString(),
@ -159,8 +156,9 @@ class InputBar extends StatelessWidget {
for (final user in room.getParticipants()) { for (final user in room.getParticipants()) {
if ((user.displayName != null && if ((user.displayName != null &&
(user.displayName!.toLowerCase().contains(userSearch) || (user.displayName!.toLowerCase().contains(userSearch) ||
slugify(user.displayName!.toLowerCase()) slugify(
.contains(userSearch))) || user.displayName!.toLowerCase(),
).contains(userSearch))) ||
user.id.split(':')[0].toLowerCase().contains(userSearch)) { user.id.split(':')[0].toLowerCase().contains(userSearch)) {
ret.add({ ret.add({
'type': 'user', 'type': 'user',
@ -258,11 +256,7 @@ class InputBar extends StatelessWidget {
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
), ),
), ),
title: Text( title: Text(label, maxLines: 1, overflow: TextOverflow.ellipsis),
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
), ),
); );
} }
@ -280,7 +274,7 @@ class InputBar extends StatelessWidget {
isThumbnail: false, isThumbnail: false,
), ),
title: Row( title: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: .center,
children: <Widget>[ children: <Widget>[
Text(suggestion['name']!), Text(suggestion['name']!),
Expanded( Expanded(
@ -311,7 +305,8 @@ class InputBar extends StatelessWidget {
onTap: () => onSelected(suggestion), onTap: () => onSelected(suggestion),
leading: Avatar( leading: Avatar(
mxContent: url, mxContent: url,
name: suggestion.tryGet<String>('displayname') ?? name:
suggestion.tryGet<String>('displayname') ??
suggestion.tryGet<String>('mxid'), suggestion.tryGet<String>('mxid'),
size: size, size: size,
client: client, client: client,
@ -323,8 +318,10 @@ class InputBar extends StatelessWidget {
} }
String insertSuggestion(Map<String, String?> suggestion) { String insertSuggestion(Map<String, String?> suggestion) {
final replaceText = final replaceText = controller!.text.substring(
controller!.text.substring(0, controller!.selection.baseOffset); 0,
controller!.selection.baseOffset,
);
var startText = ''; var startText = '';
final afterText = replaceText == controller!.text final afterText = replaceText == controller!.text
? '' ? ''
@ -409,10 +406,7 @@ class InputBar extends StatelessWidget {
bytes: data, bytes: data,
name: content.uri.split('/').last, name: content.uri.split('/').last,
); );
room.sendFileEvent( room.sendFileEvent(file, shrinkImageMaxDimension: 1600);
file,
shrinkImageMaxDimension: 1600,
);
}, },
), ),
minLines: minLines, minLines: minLines,

View file

@ -39,7 +39,8 @@ class PinnedEvents extends StatelessWidget {
(event) => AdaptiveModalAction( (event) => AdaptiveModalAction(
value: event?.eventId ?? '', value: event?.eventId ?? '',
icon: const Icon(Icons.push_pin_outlined), icon: const Icon(Icons.push_pin_outlined),
label: event?.calcLocalizedBodyFallback( label:
event?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)), MatrixLocals(L10n.of(context)),
withSenderNamePrefix: true, withSenderNamePrefix: true,
hideReply: true, hideReply: true,
@ -68,7 +69,8 @@ class PinnedEvents extends StatelessWidget {
builder: (context, snapshot) { builder: (context, snapshot) {
final event = snapshot.data; final event = snapshot.data;
return ChatAppBarListTile( return ChatAppBarListTile(
title: event?.calcLocalizedBodyFallback( title:
event?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)), MatrixLocals(L10n.of(context)),
withSenderNamePrefix: true, withSenderNamePrefix: true,
hideReply: true, hideReply: true,

View file

@ -46,8 +46,8 @@ class RecordingInputRow extends StatelessWidget {
builder: (context, constraints) { builder: (context, constraints) {
const width = 4; const width = 4;
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: .end,
children: state.amplitudeTimeline.reversed children: state.amplitudeTimeline.reversed
.take((constraints.maxWidth / (width + 2)).floor()) .take((constraints.maxWidth / (width + 2)).floor())
.toList() .toList()

View file

@ -20,10 +20,7 @@ import 'events/audio_player.dart';
class RecordingViewModel extends StatefulWidget { class RecordingViewModel extends StatefulWidget {
final Widget Function(BuildContext, RecordingViewModelState) builder; final Widget Function(BuildContext, RecordingViewModelState) builder;
const RecordingViewModel({ const RecordingViewModel({required this.builder, super.key});
required this.builder,
super.key,
});
@override @override
RecordingViewModelState createState() => RecordingViewModelState(); RecordingViewModelState createState() => RecordingViewModelState();
@ -70,10 +67,10 @@ class RecordingViewModelState extends State<RecordingViewModel> {
? AudioEncoder.wav ? AudioEncoder.wav
// Everywhere else we use opus if supported by the platform: // Everywhere else we use opus if supported by the platform:
: !PlatformInfos : !PlatformInfos
.isIOS && // Blocked by https://github.com/llfbandit/record/issues/560 .isIOS && // Blocked by https://github.com/llfbandit/record/issues/560
await audioRecorder.isEncoderSupported(AudioEncoder.opus) await audioRecorder.isEncoderSupported(AudioEncoder.opus)
? AudioEncoder.opus ? AudioEncoder.opus
: AudioEncoder.aacLc; : AudioEncoder.aacLc;
fileName = fileName =
'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}'; 'recording${DateTime.now().microsecondsSinceEpoch}.${codec.fileExtension}';
String? path; String? path;
@ -126,8 +123,9 @@ class RecordingViewModelState extends State<RecordingViewModel> {
void _subscribe() { void _subscribe() {
_recorderSubscription?.cancel(); _recorderSubscription?.cancel();
_recorderSubscription = _recorderSubscription = Timer.periodic(const Duration(milliseconds: 100), (
Timer.periodic(const Duration(milliseconds: 100), (_) async { _,
) async {
final amplitude = await _audioRecorder!.getAmplitude(); final amplitude = await _audioRecorder!.getAmplitude();
var value = 100 + amplitude.current * 2; var value = 100 + amplitude.current * 2;
value = value < 1 ? 1 : value; value = value < 1 ? 1 : value;
@ -178,7 +176,8 @@ class RecordingViewModelState extends State<RecordingViewModel> {
int duration, int duration,
List<int> waveform, List<int> waveform,
String? fileName, String? fileName,
) onSend, )
onSend,
) async { ) async {
_recorderSubscription?.cancel(); _recorderSubscription?.cancel();
final path = await _audioRecorder?.stop(); final path = await _audioRecorder?.stop();

View file

@ -23,9 +23,7 @@ class ReplyDisplay extends StatelessWidget {
? 56 ? 56
: 0, : 0,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(color: theme.colorScheme.onInverseSurface),
color: theme.colorScheme.onInverseSurface,
),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
IconButton( IconButton(
@ -63,10 +61,7 @@ class _EditContent extends StatelessWidget {
} }
return Row( return Row(
children: <Widget>[ children: <Widget>[
Icon( Icon(Icons.edit, color: theme.colorScheme.primary),
Icons.edit,
color: theme.colorScheme.primary,
),
Container(width: 15.0), Container(width: 15.0),
Text( Text(
event.calcLocalizedBodyFallback( event.calcLocalizedBodyFallback(
@ -76,9 +71,7 @@ class _EditContent extends StatelessWidget {
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(color: theme.textTheme.bodyMedium!.color),
color: theme.textTheme.bodyMedium!.color,
),
), ),
], ],
); );

View file

@ -20,14 +20,16 @@ class SeenByRow extends StatelessWidget {
width: double.infinity, width: double.infinity,
alignment: Alignment.center, alignment: Alignment.center,
child: AnimatedContainer( child: AnimatedContainer(
constraints: constraints: const BoxConstraints(
const BoxConstraints(maxWidth: FluffyThemes.maxTimelineWidth), maxWidth: FluffyThemes.maxTimelineWidth,
),
height: seenByUsers.isEmpty ? 0 : 24, height: seenByUsers.isEmpty ? 0 : 24,
duration: seenByUsers.isEmpty duration: seenByUsers.isEmpty
? Duration.zero ? Duration.zero
: FluffyThemes.animationDuration, : FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,
alignment: controller.timeline!.events.isNotEmpty && alignment:
controller.timeline!.events.isNotEmpty &&
controller.timeline!.events.first.senderId == controller.timeline!.events.first.senderId ==
Matrix.of(context).client.userID Matrix.of(context).client.userID
? Alignment.topRight ? Alignment.topRight
@ -40,12 +42,12 @@ class SeenByRow extends StatelessWidget {
? seenByUsers.sublist(0, maxAvatars) ? seenByUsers.sublist(0, maxAvatars)
: seenByUsers) : seenByUsers)
.map( .map(
(user) => Avatar( (user) => Avatar(
mxContent: user.avatarUrl, mxContent: user.avatarUrl,
name: user.calcDisplayname(), name: user.calcDisplayname(),
size: 16, size: 16,
), ),
), ),
if (seenByUsers.length > maxAvatars) if (seenByUsers.length > maxAvatars)
SizedBox( SizedBox(
width: 16, width: 16,

View file

@ -119,8 +119,9 @@ class SendFileDialogState extends State<SendFileDialog> {
if (e.error != MatrixError.M_LIMIT_EXCEEDED || retryAfterMs == null) { if (e.error != MatrixError.M_LIMIT_EXCEEDED || retryAfterMs == null) {
rethrow; rethrow;
} }
final retryAfterDuration = final retryAfterDuration = Duration(
Duration(milliseconds: retryAfterMs + 1000); milliseconds: retryAfterMs + 1000,
);
scaffoldMessenger.showSnackBar( scaffoldMessenger.showSnackBar(
SnackBar( SnackBar(
@ -164,8 +165,9 @@ class SendFileDialogState extends State<SendFileDialog> {
} }
Future<String> _calcCombinedFileSize() async { Future<String> _calcCombinedFileSize() async {
final lengths = final lengths = await Future.wait(
await Future.wait(widget.files.map((file) => file.length())); widget.files.map((file) => file.length()),
);
return lengths.fold<double>(0, (p, length) => p + length).sizeString; return lengths.fold<double>(0, (p, length) => p + length).sizeString;
} }
@ -216,7 +218,7 @@ class SendFileDialogState extends State<SendFileDialog> {
width: 256, width: 256,
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const SizedBox(height: 12), const SizedBox(height: 12),
if (uniqueFileType == 'image') if (uniqueFileType == 'image')
@ -243,8 +245,8 @@ class SendFileDialogState extends State<SendFileDialog> {
final bytes = snapshot.data; final bytes = snapshot.data;
if (bytes == null) { if (bytes == null) {
return const Center( return const Center(
child: CircularProgressIndicator child:
.adaptive(), CircularProgressIndicator.adaptive(),
); );
} }
if (snapshot.error != null) { if (snapshot.error != null) {
@ -272,8 +274,11 @@ class SendFileDialogState extends State<SendFileDialog> {
: null, : null,
fit: BoxFit.contain, fit: BoxFit.contain,
errorBuilder: (context, e, s) { errorBuilder: (context, e, s) {
Logs() Logs().w(
.w('Unable to preview image', e, s); 'Unable to preview image',
e,
s,
);
return const Center( return const Center(
child: SizedBox( child: SizedBox(
width: 256, width: 256,
@ -303,17 +308,17 @@ class SendFileDialogState extends State<SendFileDialog> {
uniqueFileType == null uniqueFileType == null
? Icons.description_outlined ? Icons.description_outlined
: uniqueFileType == 'video' : uniqueFileType == 'video'
? Icons.video_file_outlined ? Icons.video_file_outlined
: uniqueFileType == 'audio' : uniqueFileType == 'audio'
? Icons.audio_file_outlined ? Icons.audio_file_outlined
: Icons.description_outlined, : Icons.description_outlined,
size: 32, size: 32,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
Text( Text(
fileName, fileName,
@ -347,10 +352,12 @@ class SendFileDialogState extends State<SendFileDialog> {
// Workaround for SwitchListTile.adaptive crashes in CupertinoDialog // Workaround for SwitchListTile.adaptive crashes in CupertinoDialog
if ({'image', 'video'}.contains(uniqueFileType)) if ({'image', 'video'}.contains(uniqueFileType))
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: .center,
children: [ children: [
if ({TargetPlatform.iOS, TargetPlatform.macOS} if ({
.contains(theme.platform)) TargetPlatform.iOS,
TargetPlatform.macOS,
}.contains(theme.platform))
CupertinoSwitch( CupertinoSwitch(
value: compressionSupported && compress, value: compressionSupported && compress,
onChanged: compressionSupported onChanged: compressionSupported
@ -367,11 +374,11 @@ class SendFileDialogState extends State<SendFileDialog> {
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Text( Text(
L10n.of(context).compress, L10n.of(context).compress,
@ -430,9 +437,7 @@ extension on ScaffoldMessengerState {
const SizedBox( const SizedBox(
width: 16, width: 16,
height: 16, height: 16,
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(strokeWidth: 2),
strokeWidth: 2,
),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text(title), Text(title),

View file

@ -14,10 +14,7 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart';
class SendLocationDialog extends StatefulWidget { class SendLocationDialog extends StatefulWidget {
final Room room; final Room room;
const SendLocationDialog({ const SendLocationDialog({required this.room, super.key});
required this.room,
super.key,
});
@override @override
SendLocationDialogState createState() => SendLocationDialogState(); SendLocationDialogState createState() => SendLocationDialogState();
@ -102,12 +99,13 @@ class SendLocationDialogState extends State<SendLocationDialog> {
} else if (denied) { } else if (denied) {
contentWidget = Text(L10n.of(context).locationPermissionDeniedNotice); contentWidget = Text(L10n.of(context).locationPermissionDeniedNotice);
} else if (error != null) { } else if (error != null) {
contentWidget = contentWidget = Text(
Text(L10n.of(context).errorObtainingLocation(error.toString())); L10n.of(context).errorObtainingLocation(error.toString()),
);
} else { } else {
contentWidget = Row( contentWidget = Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
const CupertinoActivityIndicator(), const CupertinoActivityIndicator(),
const SizedBox(width: 12), const SizedBox(width: 12),

View file

@ -50,14 +50,15 @@ class _StartPollBottomSheetState extends State<StartPollBottomSheet> {
} catch (e, s) { } catch (e, s) {
Logs().w('Unable to create poll', e, s); Logs().w('Unable to create poll', e, s);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(e.toLocalizedString(context))), context,
); ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
} }
} }
void _updateCanCreate([dynamic _]) { void _updateCanCreate([dynamic _]) {
final newCanCreate = _bodyController.text.trim().isNotEmpty && final newCanCreate =
_bodyController.text.trim().isNotEmpty &&
!_answers.any((controller) => controller.text.trim().isEmpty); !_answers.any((controller) => controller.text.trim().isEmpty);
if (_canCreate != newCanCreate) { if (_canCreate != newCanCreate) {
setState(() { setState(() {
@ -71,9 +72,7 @@ class _StartPollBottomSheetState extends State<StartPollBottomSheet> {
const maxAnswers = 10; const maxAnswers = 10;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: CloseButton( leading: CloseButton(onPressed: Navigator.of(context).pop),
onPressed: Navigator.of(context).pop,
),
title: Text(L10n.of(context).startPoll), title: Text(L10n.of(context).startPoll),
), ),
body: ListView( body: ListView(
@ -119,8 +118,8 @@ class _StartPollBottomSheetState extends State<StartPollBottomSheet> {
icon: const Icon(Icons.add_outlined), icon: const Icon(Icons.add_outlined),
onPressed: _answers.length < maxAnswers onPressed: _answers.length < maxAnswers
? () => setState(() { ? () => setState(() {
_answers.add(TextEditingController()); _answers.add(TextEditingController());
}) })
: null, : null,
label: Text(L10n.of(context).addAnswerOption), label: Text(L10n.of(context).addAnswerOption),
), ),

View file

@ -38,15 +38,17 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
final filteredImagePackImageEntried = pack.images.entries.toList(); final filteredImagePackImageEntried = pack.images.entries.toList();
if (searchFilter?.isNotEmpty ?? false) { if (searchFilter?.isNotEmpty ?? false) {
filteredImagePackImageEntried.removeWhere( filteredImagePackImageEntried.removeWhere(
(e) => !(e.key.toLowerCase().contains(searchFilter!.toLowerCase()) || (e) =>
(e.value.body !(e.key.toLowerCase().contains(searchFilter!.toLowerCase()) ||
?.toLowerCase() (e.value.body?.toLowerCase().contains(
.contains(searchFilter!.toLowerCase()) ?? searchFilter!.toLowerCase(),
false)), ) ??
false)),
); );
} }
final imageKeys = final imageKeys = filteredImagePackImageEntried
filteredImagePackImageEntried.map((e) => e.key).toList(); .map((e) => e.key)
.toList();
if (imageKeys.isEmpty) { if (imageKeys.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -82,8 +84,9 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
key: ValueKey(image.url.toString()), key: ValueKey(image.url.toString()),
onTap: () { onTap: () {
// copy the image // copy the image
final imageCopy = final imageCopy = ImagePackImageContent.fromJson(
ImagePackImageContent.fromJson(image.toJson().copy()); image.toJson().copy(),
);
// set the body, if it doesn't exist, to the key // set the body, if it doesn't exist, to the key
imageCopy.body ??= imageKeys[imageIndex]; imageCopy.body ??= imageKeys[imageIndex];
widget.onSelected(imageCopy); widget.onSelected(imageCopy);
@ -137,7 +140,7 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
SliverFillRemaining( SliverFillRemaining(
child: Center( child: Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Text(L10n.of(context).noEmotesFound), Text(L10n.of(context).noEmotesFound),
const SizedBox(height: 12), const SizedBox(height: 12),

View file

@ -21,8 +21,9 @@ class TypingIndicators extends StatelessWidget {
return StreamBuilder<Object>( return StreamBuilder<Object>(
stream: controller.room.client.onSync.stream.where( stream: controller.room.client.onSync.stream.where(
(syncUpdate) => (syncUpdate) =>
syncUpdate.rooms?.join?[controller.room.id]?.ephemeral syncUpdate.rooms?.join?[controller.room.id]?.ephemeral?.any(
?.any((ephemeral) => ephemeral.type == 'm.typing') ?? (ephemeral) => ephemeral.type == 'm.typing',
) ??
false, false,
), ),
builder: (context, _) { builder: (context, _) {
@ -33,22 +34,21 @@ class TypingIndicators extends StatelessWidget {
width: double.infinity, width: double.infinity,
alignment: Alignment.center, alignment: Alignment.center,
child: AnimatedContainer( child: AnimatedContainer(
constraints: constraints: const BoxConstraints(
const BoxConstraints(maxWidth: FluffyThemes.maxTimelineWidth), maxWidth: FluffyThemes.maxTimelineWidth,
),
height: typingUsers.isEmpty ? 0 : avatarSize + 8, height: typingUsers.isEmpty ? 0 : avatarSize + 8,
duration: FluffyThemes.animationDuration, duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,
alignment: controller.timeline!.events.isNotEmpty && alignment:
controller.timeline!.events.isNotEmpty &&
controller.timeline!.events.first.senderId == controller.timeline!.events.first.senderId ==
Matrix.of(context).client.userID Matrix.of(context).client.userID
? Alignment.topRight ? Alignment.topRight
: Alignment.topLeft, : Alignment.topLeft,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(), decoration: const BoxDecoration(),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
horizontal: 8.0,
vertical: 4.0,
),
child: Row( child: Row(
children: [ children: [
Container( Container(
@ -115,17 +115,14 @@ class __TypingDotsState extends State<_TypingDots> {
@override @override
void initState() { void initState() {
_timer = Timer.periodic( _timer = Timer.periodic(animationDuration, (_) {
animationDuration, if (!mounted) {
(_) { return;
if (!mounted) { }
return; setState(() {
} _tick = (_tick + 1) % 4;
setState(() { });
_tick = (_tick + 1) % 4; });
});
},
);
super.initState(); super.initState();
} }
@ -141,7 +138,7 @@ class __TypingDotsState extends State<_TypingDots> {
const size = 8.0; const size = 8.0;
return Row( return Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
for (var i = 1; i <= 3; i++) for (var i = 1; i <= 3; i++)
AnimatedContainer( AnimatedContainer(

View file

@ -27,15 +27,15 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
bool guestAccessLoading = false; bool guestAccessLoading = false;
Room get room => Matrix.of(context).client.getRoomById(widget.roomId)!; Room get room => Matrix.of(context).client.getRoomById(widget.roomId)!;
Set<Room> get knownSpaceParents => { Set<Room> get knownSpaceParents => {
...room.client.rooms.where( ...room.client.rooms.where(
(space) => (space) =>
space.isSpace && space.isSpace &&
space.spaceChildren.any((child) => child.roomId == room.id), space.spaceChildren.any((child) => child.roomId == room.id),
), ),
...room.spaceParents ...room.spaceParents
.map((parent) => room.client.getRoomById(parent.roomId ?? '')) .map((parent) => room.client.getRoomById(parent.roomId ?? ''))
.whereType<Room>(), .whereType<Room>(),
}; };
String get roomVersion => String get roomVersion =>
room room
@ -87,21 +87,20 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
try { try {
await room.setJoinRules( await room.setJoinRules(
newJoinRules, newJoinRules,
allowConditionRoomIds: {JoinRules.restricted, JoinRules.knockRestricted} allowConditionRoomIds:
.contains(newJoinRules) {
JoinRules.restricted,
JoinRules.knockRestricted,
}.contains(newJoinRules)
? knownSpaceParents.map((parent) => parent.id).toList() ? knownSpaceParents.map((parent) => parent.id).toList()
: null, : null,
); );
} catch (e, s) { } catch (e, s) {
Logs().w('Unable to change join rules', e, s); Logs().w('Unable to change join rules', e, s);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar( context,
content: Text( ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
e.toLocalizedString(context),
),
),
);
} }
} finally { } finally {
if (mounted) { if (mounted) {
@ -123,13 +122,9 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
} catch (e, s) { } catch (e, s) {
Logs().w('Unable to change history visibility', e, s); Logs().w('Unable to change history visibility', e, s);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar( context,
content: Text( ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
e.toLocalizedString(context),
),
),
);
} }
} finally { } finally {
if (mounted) { if (mounted) {
@ -151,13 +146,9 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
} catch (e, s) { } catch (e, s) {
Logs().w('Unable to change guest access', e, s); Logs().w('Unable to change guest access', e, s);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar( context,
content: Text( ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
e.toLocalizedString(context),
),
),
);
} }
} finally { } finally {
if (mounted) { if (mounted) {
@ -216,8 +207,11 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
newRoom = room.client.getRoomById(newRoomId); newRoom = room.client.getRoomById(newRoomId);
} }
if ({JoinRules.invite, JoinRules.knock, JoinRules.knockRestricted} if ({
.contains(room.joinRules)) { JoinRules.invite,
JoinRules.knock,
JoinRules.knockRestricted,
}.contains(room.joinRules)) {
final users = await room.requestParticipants([ final users = await room.requestParticipants([
Membership.join, Membership.join,
Membership.invite, Membership.invite,
@ -282,7 +276,8 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
cancelLabel: L10n.of(context).no, cancelLabel: L10n.of(context).no,
); );
final altAliases = room final altAliases =
room
.getState(EventTypes.RoomCanonicalAlias) .getState(EventTypes.RoomCanonicalAlias)
?.content ?.content
.tryGetList<String>('alt_aliases') .tryGetList<String>('alt_aliases')
@ -298,17 +293,13 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () => room.client.setRoomStateWithKey( future: () => room.client
room.id, .setRoomStateWithKey(room.id, EventTypes.RoomCanonicalAlias, '', {
EventTypes.RoomCanonicalAlias, 'alias': canonicalAliasConsent == OkCancelResult.ok
'', ? alias
{ : room.canonicalAlias,
'alias': canonicalAliasConsent == OkCancelResult.ok if (altAliases.isNotEmpty) 'alt_aliases': altAliases.toList(),
? alias }),
: room.canonicalAlias,
if (altAliases.isNotEmpty) 'alt_aliases': altAliases.toList(),
},
),
); );
} }
@ -335,13 +326,9 @@ class ChatAccessSettingsController extends State<ChatAccessSettings> {
} catch (e, s) { } catch (e, s) {
Logs().w('Unable to change visibility', e, s); Logs().w('Unable to change visibility', e, s);
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar( context,
content: Text( ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
e.toLocalizedString(context),
),
),
);
} }
} finally { } finally {
if (mounted) { if (mounted) {

View file

@ -24,17 +24,19 @@ class ChatAccessSettingsPageView extends StatelessWidget {
), ),
body: MaxWidthBody( body: MaxWidthBody(
child: StreamBuilder<Object>( child: StreamBuilder<Object>(
stream: room.client.onRoomState.stream stream: room.client.onRoomState.stream.where(
.where((update) => update.roomId == controller.room.id), (update) => update.roomId == controller.room.id,
),
builder: (context, snapshot) { builder: (context, snapshot) {
final canonicalAlias = room.canonicalAlias; final canonicalAlias = room.canonicalAlias;
final altAliases = room final altAliases =
room
.getState(EventTypes.RoomCanonicalAlias) .getState(EventTypes.RoomCanonicalAlias)
?.content ?.content
.tryGetList<String>('alt_aliases') ?? .tryGetList<String>('alt_aliases') ??
[]; [];
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
ListTile( ListTile(
title: Text( title: Text(
@ -47,12 +49,13 @@ class ChatAccessSettingsPageView extends StatelessWidget {
), ),
RadioGroup<HistoryVisibility>( RadioGroup<HistoryVisibility>(
groupValue: room.historyVisibility, groupValue: room.historyVisibility,
onChanged: controller.historyVisibilityLoading || onChanged:
controller.historyVisibilityLoading ||
!room.canChangeHistoryVisibility !room.canChangeHistoryVisibility
? (_) {} ? (_) {}
: controller.setHistoryVisibility, : controller.setHistoryVisibility,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
for (final historyVisibility in HistoryVisibility.values) for (final historyVisibility in HistoryVisibility.values)
RadioListTile<HistoryVisibility>.adaptive( RadioListTile<HistoryVisibility>.adaptive(
@ -80,12 +83,13 @@ class ChatAccessSettingsPageView extends StatelessWidget {
groupValue: room.joinRules, groupValue: room.joinRules,
onChanged: controller.setJoinRule, onChanged: controller.setJoinRule,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
for (final joinRule in controller.availableJoinRules) for (final joinRule in controller.availableJoinRules)
if (joinRule != JoinRules.private) if (joinRule != JoinRules.private)
RadioListTile<JoinRules>.adaptive( RadioListTile<JoinRules>.adaptive(
enabled: !controller.joinRulesLoading && enabled:
!controller.joinRulesLoading &&
room.canChangeJoinRules, room.canChangeJoinRules,
title: Text( title: Text(
joinRule.localizedString( joinRule.localizedString(
@ -99,8 +103,10 @@ class ChatAccessSettingsPageView extends StatelessWidget {
), ),
), ),
Divider(color: theme.dividerColor), Divider(color: theme.dividerColor),
if ({JoinRules.public, JoinRules.knock} if ({
.contains(room.joinRules)) ...[ JoinRules.public,
JoinRules.knock,
}.contains(room.joinRules)) ...[
ListTile( ListTile(
title: Text( title: Text(
L10n.of(context).areGuestsAllowedToJoin, L10n.of(context).areGuestsAllowedToJoin,
@ -114,11 +120,12 @@ class ChatAccessSettingsPageView extends StatelessWidget {
groupValue: room.guestAccess, groupValue: room.guestAccess,
onChanged: controller.setGuestAccess, onChanged: controller.setGuestAccess,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
for (final guestAccess in GuestAccess.values) for (final guestAccess in GuestAccess.values)
RadioListTile<GuestAccess>.adaptive( RadioListTile<GuestAccess>.adaptive(
enabled: !controller.guestAccessLoading && enabled:
!controller.guestAccessLoading &&
room.canChangeGuestAccess, room.canChangeGuestAccess,
title: Text( title: Text(
guestAccess.getLocalizedString( guestAccess.getLocalizedString(
@ -148,9 +155,10 @@ class ChatAccessSettingsPageView extends StatelessWidget {
if (canonicalAlias.isNotEmpty) if (canonicalAlias.isNotEmpty)
_AliasListTile( _AliasListTile(
alias: canonicalAlias, alias: canonicalAlias,
onDelete: room.canChangeStateEvent( onDelete:
EventTypes.RoomCanonicalAlias, room.canChangeStateEvent(
) EventTypes.RoomCanonicalAlias,
)
? () => controller.deleteAlias(canonicalAlias) ? () => controller.deleteAlias(canonicalAlias)
: null, : null,
isCanonicalAlias: true, isCanonicalAlias: true,
@ -158,9 +166,10 @@ class ChatAccessSettingsPageView extends StatelessWidget {
for (final alias in altAliases) for (final alias in altAliases)
_AliasListTile( _AliasListTile(
alias: alias, alias: alias,
onDelete: room.canChangeStateEvent( onDelete:
EventTypes.RoomCanonicalAlias, room.canChangeStateEvent(
) EventTypes.RoomCanonicalAlias,
)
? () => controller.deleteAlias(alias) ? () => controller.deleteAlias(alias)
: null, : null,
), ),
@ -172,10 +181,11 @@ class ChatAccessSettingsPageView extends StatelessWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
localAddresses.remove(room.canonicalAlias); localAddresses.remove(room.canonicalAlias);
localAddresses localAddresses.removeWhere(
.removeWhere((alias) => altAliases.contains(alias)); (alias) => altAliases.contains(alias),
);
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: localAddresses children: localAddresses
.map( .map(
(alias) => _AliasListTile( (alias) => _AliasListTile(
@ -257,10 +267,7 @@ class _AliasListTile extends StatelessWidget {
? const Icon(Icons.star) ? const Icon(Icons.star)
: const Icon(Icons.link_outlined), : const Icon(Icons.link_outlined),
title: InkWell( title: InkWell(
onTap: () => FluffyShare.share( onTap: () => FluffyShare.share('https://matrix.to/#/$alias', context),
'https://matrix.to/#/$alias',
context,
),
child: SelectableText( child: SelectableText(
alias, alias,
style: TextStyle( style: TextStyle(

View file

@ -46,11 +46,7 @@ class ChatDetailsController extends State<ChatDetails> {
title: L10n.of(context).changeTheNameOfTheGroup, title: L10n.of(context).changeTheNameOfTheGroup,
okLabel: L10n.of(context).ok, okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel, cancelLabel: L10n.of(context).cancel,
initialText: room.getLocalizedDisplayname( initialText: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
MatrixLocals(
L10n.of(context),
),
),
); );
if (input == null) return; if (input == null) return;
final success = await showFutureLoadingDialog( final success = await showFutureLoadingDialog(
@ -83,9 +79,7 @@ class ChatDetailsController extends State<ChatDetails> {
); );
if (success.error == null) { if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(content: Text(L10n.of(context).chatDescriptionHasBeenChanged)),
content: Text(L10n.of(context).chatDescriptionHasBeenChanged),
),
); );
} }
} }
@ -138,10 +132,7 @@ class ChatDetailsController extends State<ChatDetails> {
imageQuality: 50, imageQuality: 50,
); );
if (result == null) return; if (result == null) return;
file = MatrixFile( file = MatrixFile(bytes: await result.readAsBytes(), name: result.path);
bytes: await result.readAsBytes(),
name: result.path,
);
} else { } else {
final picked = await selectFiles( final picked = await selectFiles(
context, context,

View file

@ -29,9 +29,7 @@ class ChatDetailsView extends StatelessWidget {
final room = Matrix.of(context).client.getRoomById(controller.roomId!); final room = Matrix.of(context).client.getRoomById(controller.roomId!);
if (room == null) { if (room == null) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: Text(L10n.of(context).oopsSomethingWentWrong)),
title: Text(L10n.of(context).oopsSomethingWentWrong),
),
body: Center( body: Center(
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
), ),
@ -42,13 +40,15 @@ class ChatDetailsView extends StatelessWidget {
final roomAvatar = room.avatar; final roomAvatar = room.avatar;
return StreamBuilder( return StreamBuilder(
stream: room.client.onRoomState.stream stream: room.client.onRoomState.stream.where(
.where((update) => update.roomId == room.id), (update) => update.roomId == room.id,
),
builder: (context, snapshot) { builder: (context, snapshot) {
var members = room.getParticipants().toList() var members = room.getParticipants().toList()
..sort((b, a) => a.powerLevel.compareTo(b.powerLevel)); ..sort((b, a) => a.powerLevel.compareTo(b.powerLevel));
members = members.take(10).toList(); members = members.take(10).toList();
final actualMembersCount = (room.summary.mInvitedMemberCount ?? 0) + final actualMembersCount =
(room.summary.mInvitedMemberCount ?? 0) +
(room.summary.mJoinedMemberCount ?? 0); (room.summary.mJoinedMemberCount ?? 0);
final canRequestMoreMembers = members.length < actualMembersCount; final canRequestMoreMembers = members.length < actualMembersCount;
final iconColor = theme.textTheme.bodyLarge!.color; final iconColor = theme.textTheme.bodyLarge!.color;
@ -57,7 +57,8 @@ class ChatDetailsView extends StatelessWidget {
); );
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: controller.widget.embeddedCloseButton ?? leading:
controller.widget.embeddedCloseButton ??
const Center(child: BackButton()), const Center(child: BackButton()),
elevation: theme.appBarTheme.elevation, elevation: theme.appBarTheme.elevation,
actions: <Widget>[ actions: <Widget>[
@ -65,19 +66,15 @@ class ChatDetailsView extends StatelessWidget {
IconButton( IconButton(
tooltip: L10n.of(context).share, tooltip: L10n.of(context).share,
icon: const Icon(Icons.qr_code_rounded), icon: const Icon(Icons.qr_code_rounded),
onPressed: () => showQrCodeViewer( onPressed: () =>
context, showQrCodeViewer(context, room.canonicalAlias),
room.canonicalAlias,
),
) )
else if (directChatMatrixID != null) else if (directChatMatrixID != null)
IconButton( IconButton(
tooltip: L10n.of(context).share, tooltip: L10n.of(context).share,
icon: const Icon(Icons.qr_code_rounded), icon: const Icon(Icons.qr_code_rounded),
onPressed: () => showQrCodeViewer( onPressed: () =>
context, showQrCodeViewer(context, directChatMatrixID),
directChatMatrixID,
),
), ),
if (controller.widget.embeddedCloseButton == null) if (controller.widget.embeddedCloseButton == null)
ChatSettingsPopupMenu(room, false), ChatSettingsPopupMenu(room, false),
@ -92,7 +89,7 @@ class ChatDetailsView extends StatelessWidget {
itemCount: members.length + 1 + (canRequestMoreMembers ? 1 : 0), itemCount: members.length + 1 + (canRequestMoreMembers ? 1 : 0),
itemBuilder: (BuildContext context, int i) => i == 0 itemBuilder: (BuildContext context, int i) => i == 0
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: .stretch,
children: <Widget>[ children: <Widget>[
Row( Row(
children: [ children: [
@ -103,19 +100,19 @@ class ChatDetailsView extends StatelessWidget {
Hero( Hero(
tag: tag:
controller.widget.embeddedCloseButton != controller.widget.embeddedCloseButton !=
null null
? 'embedded_content_banner' ? 'embedded_content_banner'
: 'content_banner', : 'content_banner',
child: Avatar( child: Avatar(
mxContent: room.avatar, mxContent: room.avatar,
name: displayname, name: displayname,
size: Avatar.defaultSize * 2.5, size: Avatar.defaultSize * 2.5,
onTap: roomAvatar != null onTap: roomAvatar != null
? () => showDialog( ? () => showDialog(
context: context, context: context,
builder: (_) => builder: (_) =>
MxcImageViewer(roomAvatar), MxcImageViewer(roomAvatar),
) )
: null, : null,
), ),
), ),
@ -139,8 +136,8 @@ class ChatDetailsView extends StatelessWidget {
), ),
Expanded( Expanded(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
TextButton.icon( TextButton.icon(
onPressed: () => room.isDirectChat onPressed: () => room.isDirectChat
@ -148,20 +145,20 @@ class ChatDetailsView extends StatelessWidget {
: room.canChangeStateEvent( : room.canChangeStateEvent(
EventTypes.RoomName, EventTypes.RoomName,
) )
? controller.setDisplaynameAction() ? controller.setDisplaynameAction()
: FluffyShare.share( : FluffyShare.share(
displayname, displayname,
context, context,
copyOnly: true, copyOnly: true,
), ),
icon: Icon( icon: Icon(
room.isDirectChat room.isDirectChat
? Icons.chat_bubble_outline ? Icons.chat_bubble_outline
: room.canChangeStateEvent( : room.canChangeStateEvent(
EventTypes.RoomName, EventTypes.RoomName,
) )
? Icons.edit_outlined ? Icons.edit_outlined
: Icons.copy_outlined, : Icons.copy_outlined,
size: 16, size: 16,
), ),
style: TextButton.styleFrom( style: TextButton.styleFrom(
@ -194,9 +191,9 @@ class ChatDetailsView extends StatelessWidget {
iconColor: theme.colorScheme.secondary, iconColor: theme.colorScheme.secondary,
), ),
label: Text( label: Text(
L10n.of(context).countParticipants( L10n.of(
actualMembersCount, context,
), ).countParticipants(actualMembersCount),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 12), // style: const TextStyle(fontSize: 12),
@ -220,13 +217,14 @@ class ChatDetailsView extends StatelessWidget {
), ),
trailing: trailing:
room.canChangeStateEvent(EventTypes.RoomTopic) room.canChangeStateEvent(EventTypes.RoomTopic)
? IconButton( ? IconButton(
onPressed: controller.setTopicAction, onPressed: controller.setTopicAction,
tooltip: tooltip: L10n.of(
L10n.of(context).setChatDescription, context,
icon: const Icon(Icons.edit_outlined), ).setChatDescription,
) icon: const Icon(Icons.edit_outlined),
: null, )
: null,
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -236,8 +234,9 @@ class ChatDetailsView extends StatelessWidget {
text: room.topic.isEmpty text: room.topic.isEmpty
? L10n.of(context).noChatDescriptionYet ? L10n.of(context).noChatDescriptionYet
: room.topic, : room.topic,
textScaleFactor: textScaleFactor: MediaQuery.textScalerOf(
MediaQuery.textScalerOf(context).scale(1), context,
).scale(1),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: const TextStyle( linkStyle: const TextStyle(
color: Colors.blueAccent, color: Colors.blueAccent,
@ -269,14 +268,13 @@ class ChatDetailsView extends StatelessWidget {
Icons.admin_panel_settings_outlined, Icons.admin_panel_settings_outlined,
), ),
), ),
title: Text( title: Text(L10n.of(context).accessAndVisibility),
L10n.of(context).accessAndVisibility,
),
subtitle: Text( subtitle: Text(
L10n.of(context).accessAndVisibilityDescription, L10n.of(context).accessAndVisibilityDescription,
), ),
onTap: () => context onTap: () => context.push(
.push('/rooms/${room.id}/details/access'), '/rooms/${room.id}/details/access',
),
trailing: const Icon(Icons.chevron_right_outlined), trailing: const Icon(Icons.chevron_right_outlined),
), ),
ListTile( ListTile(
@ -288,21 +286,20 @@ class ChatDetailsView extends StatelessWidget {
backgroundColor: backgroundColor:
theme.colorScheme.surfaceContainer, theme.colorScheme.surfaceContainer,
foregroundColor: iconColor, foregroundColor: iconColor,
child: const Icon( child: const Icon(Icons.tune_outlined),
Icons.tune_outlined,
),
), ),
trailing: const Icon(Icons.chevron_right_outlined), trailing: const Icon(Icons.chevron_right_outlined),
onTap: () => context onTap: () => context.push(
.push('/rooms/${room.id}/details/permissions'), '/rooms/${room.id}/details/permissions',
),
), ),
], ],
Divider(color: theme.dividerColor), Divider(color: theme.dividerColor),
ListTile( ListTile(
title: Text( title: Text(
L10n.of(context).countParticipants( L10n.of(
actualMembersCount, context,
), ).countParticipants(actualMembersCount),
style: TextStyle( style: TextStyle(
color: theme.colorScheme.secondary, color: theme.colorScheme.secondary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -326,25 +323,25 @@ class ChatDetailsView extends StatelessWidget {
], ],
) )
: i < members.length + 1 : i < members.length + 1
? ParticipantListItem(members[i - 1]) ? ParticipantListItem(members[i - 1])
: ListTile( : ListTile(
title: Text( title: Text(
L10n.of(context).loadCountMoreParticipants( L10n.of(context).loadCountMoreParticipants(
(actualMembersCount - members.length), (actualMembersCount - members.length),
),
),
leading: CircleAvatar(
backgroundColor: theme.scaffoldBackgroundColor,
child: const Icon(
Icons.group_outlined,
color: Colors.grey,
),
),
onTap: () => context.push(
'/rooms/${controller.roomId!}/details/members',
),
trailing: const Icon(Icons.chevron_right_outlined),
), ),
),
leading: CircleAvatar(
backgroundColor: theme.scaffoldBackgroundColor,
child: const Icon(
Icons.group_outlined,
color: Colors.grey,
),
),
onTap: () => context.push(
'/rooms/${controller.roomId!}/details/members',
),
trailing: const Icon(Icons.chevron_right_outlined),
),
), ),
), ),
); );

View file

@ -27,8 +27,8 @@ class ParticipantListItem extends StatelessWidget {
final permissionBatch = user.powerLevel >= 100 final permissionBatch = user.powerLevel >= 100
? L10n.of(context).admin ? L10n.of(context).admin
: user.powerLevel >= 50 : user.powerLevel >= 50
? L10n.of(context).moderator ? L10n.of(context).moderator
: ''; : '';
return ListTile( return ListTile(
onTap: () => showMemberActionsPopupMenu(context: context, user: user), onTap: () => showMemberActionsPopupMenu(context: context, user: user),
@ -42,17 +42,12 @@ class ParticipantListItem extends StatelessWidget {
), ),
if (permissionBatch.isNotEmpty) if (permissionBatch.isNotEmpty)
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: user.powerLevel >= 100 color: user.powerLevel >= 100
? theme.colorScheme.tertiary ? theme.colorScheme.tertiary
: theme.colorScheme.tertiaryContainer, : theme.colorScheme.tertiaryContainer,
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(AppConfig.borderRadius),
AppConfig.borderRadius,
),
), ),
child: Text( child: Text(
permissionBatch, permissionBatch,
@ -66,8 +61,10 @@ class ParticipantListItem extends StatelessWidget {
membershipBatch == null membershipBatch == null
? const SizedBox.shrink() ? const SizedBox.shrink()
: Container( : Container(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(vertical: 4, horizontal: 8), vertical: 4,
horizontal: 8,
),
margin: const EdgeInsets.symmetric(horizontal: 8), margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.secondaryContainer, color: theme.colorScheme.secondaryContainer,
@ -84,11 +81,7 @@ class ParticipantListItem extends StatelessWidget {
), ),
], ],
), ),
subtitle: Text( subtitle: Text(user.id, maxLines: 1, overflow: TextOverflow.ellipsis),
user.id,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
leading: Opacity( leading: Opacity(
opacity: user.membership == Membership.join ? 1 : 0.5, opacity: user.membership == Membership.join ? 1 : 0.5,
child: Avatar( child: Avatar(

View file

@ -42,7 +42,7 @@ class ChatEncryptionSettingsView extends StatelessWidget {
), ),
body: MaxWidthBody( body: MaxWidthBody(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
SwitchListTile( SwitchListTile(
secondary: CircleAvatar( secondary: CircleAvatar(
@ -76,16 +76,14 @@ class ChatEncryptionSettingsView extends StatelessWidget {
ListTile( ListTile(
title: Text( title: Text(
L10n.of(context).deviceKeys, L10n.of(context).deviceKeys,
style: const TextStyle( style: const TextStyle(fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
),
), ),
), ),
StreamBuilder( StreamBuilder(
stream: room.client.onRoomState.stream stream: room.client.onRoomState.stream.where(
.where((update) => update.roomId == controller.room.id), (update) => update.roomId == controller.room.id,
builder: (context, snapshot) => ),
FutureBuilder<List<DeviceKeys>>( builder: (context, snapshot) => FutureBuilder<List<DeviceKeys>>(
future: room.getUserDeviceKeys(), future: room.getUserDeviceKeys(),
builder: (BuildContext context, snapshot) { builder: (BuildContext context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
@ -108,20 +106,21 @@ class ChatEncryptionSettingsView extends StatelessWidget {
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemCount: deviceKeys.length, itemCount: deviceKeys.length,
itemBuilder: (BuildContext context, int i) => Column( itemBuilder: (BuildContext context, int i) => Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
if (i == 0 || if (i == 0 ||
deviceKeys[i].userId != deviceKeys[i].userId !=
deviceKeys[i - 1].userId) ...[ deviceKeys[i - 1].userId) ...[
const Divider(), const Divider(),
FutureBuilder( FutureBuilder(
future: room.client future: room.client.getUserProfile(
.getUserProfile(deviceKeys[i].userId), deviceKeys[i].userId,
),
builder: (context, snapshot) { builder: (context, snapshot) {
final displayname = final displayname =
snapshot.data?.displayname ?? snapshot.data?.displayname ??
deviceKeys[i].userId.localpart ?? deviceKeys[i].userId.localpart ??
deviceKeys[i].userId; deviceKeys[i].userId;
return ListTile( return ListTile(
leading: Avatar( leading: Avatar(
name: displayname, name: displayname,
@ -146,14 +145,14 @@ class ChatEncryptionSettingsView extends StatelessWidget {
deviceKeys[i].verified deviceKeys[i].verified
? L10n.of(context).verified ? L10n.of(context).verified
: deviceKeys[i].blocked : deviceKeys[i].blocked
? L10n.of(context).blocked ? L10n.of(context).blocked
: L10n.of(context).unverified, : L10n.of(context).unverified,
style: TextStyle( style: TextStyle(
color: deviceKeys[i].verified color: deviceKeys[i].verified
? Colors.green ? Colors.green
: deviceKeys[i].blocked : deviceKeys[i].blocked
? Colors.red ? Colors.red
: Colors.orange, : Colors.orange,
), ),
), ),
const Text(' | ID: '), const Text(' | ID: '),
@ -185,9 +184,7 @@ class ChatEncryptionSettingsView extends StatelessWidget {
child: Center( child: Center(
child: Text( child: Text(
L10n.of(context).encryptionNotEnabled, L10n.of(context).encryptionNotEnabled,
style: const TextStyle( style: const TextStyle(fontStyle: FontStyle.italic),
fontStyle: FontStyle.italic,
),
), ),
), ),
), ),

View file

@ -38,13 +38,7 @@ enum PopupMenuAction {
archive, archive,
} }
enum ActiveFilter { enum ActiveFilter { allChats, messages, groups, unread, spaces }
allChats,
messages,
groups,
unread,
spaces,
}
extension LocalizedActiveFilter on ActiveFilter { extension LocalizedActiveFilter on ActiveFilter {
String toLocalizedString(BuildContext context) { String toLocalizedString(BuildContext context) {
@ -102,8 +96,8 @@ class ChatListController extends State<ChatList>
} }
void clearActiveSpace() => setState(() { void clearActiveSpace() => setState(() {
_activeSpaceId = null; _activeSpaceId = null;
}); });
void onChatTap(Room room) async { void onChatTap(Room room) async {
if (room.membership == Membership.invite) { if (room.membership == Membership.invite) {
@ -124,9 +118,7 @@ class ChatListController extends State<ChatList>
if (room.membership == Membership.ban) { if (room.membership == Membership.ban) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(content: Text(L10n.of(context).youHaveBeenBannedFromThisChat)),
content: Text(L10n.of(context).youHaveBeenBannedFromThisChat),
),
); );
return; return;
} }
@ -159,11 +151,9 @@ class ChatListController extends State<ChatList>
} }
} }
List<Room> get filteredRooms => Matrix.of(context) List<Room> get filteredRooms => Matrix.of(
.client context,
.rooms ).client.rooms.where(getRoomFilterByActiveFilter(activeFilter)).toList();
.where(getRoomFilterByActiveFilter(activeFilter))
.toList();
bool isSearchMode = false; bool isSearchMode = false;
Future<QueryPublicRoomsResponse>? publicRoomsResponse; Future<QueryPublicRoomsResponse>? publicRoomsResponse;
@ -222,8 +212,9 @@ class ChatListController extends State<ChatList>
if (searchQuery.isValidMatrixId && if (searchQuery.isValidMatrixId &&
searchQuery.sigil == '#' && searchQuery.sigil == '#' &&
roomSearchResult.chunk roomSearchResult.chunk.any(
.any((room) => room.canonicalAlias == searchQuery) == (room) => room.canonicalAlias == searchQuery,
) ==
false) { false) {
final response = await client.getRoomIdByAlias(searchQuery); final response = await client.getRoomIdByAlias(searchQuery);
final roomId = response.roomId; final roomId = response.roomId;
@ -246,13 +237,9 @@ class ChatListController extends State<ChatList>
); );
} catch (e, s) { } catch (e, s) {
Logs().w('Searching has crashed', e, s); Logs().w('Searching has crashed', e, s);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar( context,
content: Text( ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
e.toLocalizedString(context),
),
),
);
} }
if (!isSearchMode) return; if (!isSearchMode) return;
setState(() { setState(() {
@ -333,22 +320,17 @@ class ChatListController extends State<ChatList>
showScaffoldDialog( showScaffoldDialog(
context: context, context: context,
builder: (context) => ShareScaffoldDialog( builder: (context) => ShareScaffoldDialog(
items: files.map( items: files.map((file) {
(file) { if ({SharedMediaType.text, SharedMediaType.url}.contains(file.type)) {
if ({ return TextShareItem(file.path);
SharedMediaType.text, }
SharedMediaType.url, return FileShareItem(
}.contains(file.type)) { XFile(
return TextShareItem(file.path); file.path.replaceFirst('file://', ''),
} mimeType: file.mimeType,
return FileShareItem( ),
XFile( );
file.path.replaceFirst('file://', ''), }).toList(),
mimeType: file.mimeType,
),
);
},
).toList(),
), ),
); );
} }
@ -370,22 +352,23 @@ class ChatListController extends State<ChatList>
.listen(_processIncomingSharedMedia, onError: print); .listen(_processIncomingSharedMedia, onError: print);
// For sharing images coming from outside the app while the app is closed // For sharing images coming from outside the app while the app is closed
ReceiveSharingIntent.instance ReceiveSharingIntent.instance.getInitialMedia().then(
.getInitialMedia() _processIncomingSharedMedia,
.then(_processIncomingSharedMedia); );
// For receiving shared Uris // For receiving shared Uris
_intentUriStreamSubscription = _intentUriStreamSubscription = AppLinks().uriLinkStream.listen(
AppLinks().uriLinkStream.listen(_processIncomingUris); _processIncomingUris,
);
if (PlatformInfos.isAndroid) { if (PlatformInfos.isAndroid) {
final shortcuts = FlutterShortcuts(); final shortcuts = FlutterShortcuts();
shortcuts.initialize().then( shortcuts.initialize().then(
(_) => shortcuts.listenAction((action) { (_) => shortcuts.listenAction((action) {
if (!mounted) return; if (!mounted) return;
UrlLauncher(context, action).launchUrl(); UrlLauncher(context, action).launchUrl();
}), }),
); );
} }
} }
@ -402,8 +385,9 @@ class ChatListController extends State<ChatList>
_hackyWebRTCFixForWeb(); _hackyWebRTCFixForWeb();
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
if (mounted) { if (mounted) {
searchServer = searchServer = Matrix.of(
Matrix.of(context).store.getString(_serverStoreNamespace); context,
).store.getString(_serverStoreNamespace);
Matrix.of(context).backgroundPush?.setupPush(); Matrix.of(context).backgroundPush?.setupPush();
UpdateNotifier.showUpdateSnackBar(context); UpdateNotifier.showUpdateSnackBar(context);
} }
@ -447,8 +431,9 @@ class ChatListController extends State<ChatList>
Offset.zero & overlay.size, Offset.zero & overlay.size,
); );
final displayname = final displayname = room.getLocalizedDisplayname(
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))); MatrixLocals(L10n.of(context)),
);
final spacesWithPowerLevels = room.client.rooms final spacesWithPowerLevels = room.client.rooms
.where( .where(
@ -466,19 +451,17 @@ class ChatListController extends State<ChatList>
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.open, value: ChatContextAction.open,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
spacing: 12.0, spacing: 12.0,
children: [ children: [
Avatar( Avatar(mxContent: room.avatar, name: displayname),
mxContent: room.avatar,
name: displayname,
),
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 128), constraints: const BoxConstraints(maxWidth: 128),
child: Text( child: Text(
displayname, displayname,
style: style: TextStyle(
TextStyle(color: Theme.of(context).colorScheme.onSurface), color: Theme.of(context).colorScheme.onSurface,
),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -491,7 +474,7 @@ class ChatListController extends State<ChatList>
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.goToSpace, value: ChatContextAction.goToSpace,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Avatar( Avatar(
mxContent: space.avatar, mxContent: space.avatar,
@ -511,7 +494,7 @@ class ChatListController extends State<ChatList>
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.mute, value: ChatContextAction.mute,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Icon( Icon(
room.pushRuleState == PushRuleState.notify room.pushRuleState == PushRuleState.notify
@ -530,7 +513,7 @@ class ChatListController extends State<ChatList>
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.markUnread, value: ChatContextAction.markUnread,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Icon( Icon(
room.markedUnread room.markedUnread
@ -549,7 +532,7 @@ class ChatListController extends State<ChatList>
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.favorite, value: ChatContextAction.favorite,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Icon( Icon(
room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined, room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined,
@ -567,7 +550,7 @@ class ChatListController extends State<ChatList>
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.addToSpace, value: ChatContextAction.addToSpace,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.group_work_outlined), const Icon(Icons.group_work_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -579,7 +562,7 @@ class ChatListController extends State<ChatList>
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.leave, value: ChatContextAction.leave,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Icon( Icon(
Icons.delete_outlined, Icons.delete_outlined,
@ -601,7 +584,7 @@ class ChatListController extends State<ChatList>
PopupMenuItem( PopupMenuItem(
value: ChatContextAction.block, value: ChatContextAction.block,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Icon( Icon(
Icons.block_outlined, Icons.block_outlined,
@ -684,8 +667,9 @@ class ChatListController extends State<ChatList>
.map( .map(
(space) => AdaptiveModalAction( (space) => AdaptiveModalAction(
value: space, value: space,
label: space label: space.getLocalizedDisplayname(
.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), MatrixLocals(L10n.of(context)),
),
), ),
) )
.toList(), .toList(),
@ -746,8 +730,9 @@ class ChatListController extends State<ChatList>
await client.accountDataLoading; await client.accountDataLoading;
await client.userDeviceKeysLoading; await client.userDeviceKeysLoading;
if (client.prevBatch == null) { if (client.prevBatch == null) {
await client.onSyncStatus.stream await client.onSyncStatus.stream.firstWhere(
.firstWhere((status) => status.status == SyncStatus.finished); (status) => status.status == SyncStatus.finished,
);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
@ -759,8 +744,9 @@ class ChatListController extends State<ChatList>
waitForFirstSync = true; waitForFirstSync = true;
}); });
if (client.userDeviceKeys[client.userID!]?.deviceKeys.values if (client.userDeviceKeys[client.userID!]?.deviceKeys.values.any(
.any((device) => !device.verified && !device.blocked) ?? (device) => !device.verified && !device.blocked,
) ??
false) { false) {
late final ScaffoldFeatureController controller; late final ScaffoldFeatureController controller;
final theme = Theme.of(context); final theme = Theme.of(context);
@ -772,9 +758,7 @@ class ChatListController extends State<ChatList>
closeIconColor: theme.colorScheme.onErrorContainer, closeIconColor: theme.colorScheme.onErrorContainer,
content: Text( content: Text(
L10n.of(context).oneOfYourDevicesIsNotVerified, L10n.of(context).oneOfYourDevicesIsNotVerified,
style: TextStyle( style: TextStyle(color: theme.colorScheme.onErrorContainer),
color: theme.colorScheme.onErrorContainer,
),
), ),
action: SnackBarAction( action: SnackBarAction(
onPressed: () { onPressed: () {
@ -810,20 +794,21 @@ class ChatListController extends State<ChatList>
setState(() { setState(() {
_activeSpaceId = null; _activeSpaceId = null;
Matrix.of(context).activeBundle = bundle; Matrix.of(context).activeBundle = bundle;
if (!Matrix.of(context) if (!Matrix.of(
.currentBundle! context,
.any((client) => client == Matrix.of(context).client)) { ).currentBundle!.any((client) => client == Matrix.of(context).client)) {
Matrix.of(context) Matrix.of(
.setActiveClient(Matrix.of(context).currentBundle!.first); context,
).setActiveClient(Matrix.of(context).currentBundle!.first);
} }
}); });
} }
void editBundlesForAccount(String? userId, String? activeBundle) async { void editBundlesForAccount(String? userId, String? activeBundle) async {
final l10n = L10n.of(context); final l10n = L10n.of(context);
final client = Matrix.of(context) final client = Matrix.of(
.widget context,
.clients[Matrix.of(context).getClientIndexByMatrixId(userId!)]; ).widget.clients[Matrix.of(context).getClientIndexByMatrixId(userId!)];
final action = await showModalActionPopup<EditBundleAction>( final action = await showModalActionPopup<EditBundleAction>(
context: context, context: context,
title: L10n.of(context).editBundlesForAccount, title: L10n.of(context).editBundlesForAccount,
@ -868,10 +853,9 @@ class ChatListController extends State<ChatList>
String? get secureActiveBundle { String? get secureActiveBundle {
if (Matrix.of(context).activeBundle == null || if (Matrix.of(context).activeBundle == null ||
!Matrix.of(context) !Matrix.of(
.accountBundles context,
.keys ).accountBundles.keys.contains(Matrix.of(context).activeBundle)) {
.contains(Matrix.of(context).activeBundle)) {
return Matrix.of(context).accountBundles.keys.first; return Matrix.of(context).accountBundles.keys.first;
} }
return Matrix.of(context).activeBundle; return Matrix.of(context).activeBundle;
@ -897,11 +881,7 @@ class ChatListController extends State<ChatList>
enum EditBundleAction { addToBundle, removeFromBundle } enum EditBundleAction { addToBundle, removeFromBundle }
enum InviteActions { enum InviteActions { accept, decline, block }
accept,
decline,
block,
}
enum ChatContextAction { enum ChatContextAction {
open, open,

View file

@ -59,9 +59,7 @@ class ChatListViewBody extends StatelessWidget {
const dummyChatCount = 4; const dummyChatCount = 4;
final filter = controller.searchController.text.toLowerCase(); final filter = controller.searchController.text.toLowerCase();
return StreamBuilder( return StreamBuilder(
key: ValueKey( key: ValueKey(client.userID.toString()),
client.userID.toString(),
),
stream: client.onSync.stream stream: client.onSync.stream
.where((s) => s.hasRoomUpdate) .where((s) => s.hasRoomUpdate)
.rateLimit(const Duration(seconds: 1)), .rateLimit(const Duration(seconds: 1)),
@ -74,151 +72,151 @@ class ChatListViewBody extends StatelessWidget {
slivers: [ slivers: [
ChatListHeader(controller: controller), ChatListHeader(controller: controller),
SliverList( SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate([
[ if (controller.isSearchMode) ...[
if (controller.isSearchMode) ...[ SearchTitle(
SearchTitle( title: L10n.of(context).publicRooms,
title: L10n.of(context).publicRooms, icon: const Icon(Icons.explore_outlined),
icon: const Icon(Icons.explore_outlined), ),
), PublicRoomsHorizontalList(publicRooms: publicRooms),
PublicRoomsHorizontalList(publicRooms: publicRooms), SearchTitle(
SearchTitle( title: L10n.of(context).publicSpaces,
title: L10n.of(context).publicSpaces, icon: const Icon(Icons.workspaces_outlined),
icon: const Icon(Icons.workspaces_outlined), ),
), PublicRoomsHorizontalList(publicRooms: publicSpaces),
PublicRoomsHorizontalList(publicRooms: publicSpaces), SearchTitle(
SearchTitle( title: L10n.of(context).users,
title: L10n.of(context).users, icon: const Icon(Icons.group_outlined),
icon: const Icon(Icons.group_outlined), ),
), AnimatedContainer(
AnimatedContainer( clipBehavior: Clip.hardEdge,
clipBehavior: Clip.hardEdge, decoration: const BoxDecoration(),
decoration: const BoxDecoration(), height:
height: userSearchResult == null || userSearchResult == null ||
userSearchResult.results.isEmpty userSearchResult.results.isEmpty
? 0 ? 0
: 106, : 106,
duration: FluffyThemes.animationDuration, duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,
child: userSearchResult == null child: userSearchResult == null
? null ? null
: ListView.builder( : ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: userSearchResult.results.length, itemCount: userSearchResult.results.length,
itemBuilder: (context, i) => _SearchItem( itemBuilder: (context, i) => _SearchItem(
title: title:
userSearchResult.results[i].displayName ?? userSearchResult.results[i].displayName ??
userSearchResult userSearchResult
.results[i].userId.localpart ?? .results[i]
L10n.of(context).unknownDevice, .userId
avatar: userSearchResult.results[i].avatarUrl, .localpart ??
onPressed: () => UserDialog.show( L10n.of(context).unknownDevice,
context: context, avatar: userSearchResult.results[i].avatarUrl,
profile: userSearchResult.results[i], onPressed: () => UserDialog.show(
), context: context,
profile: userSearchResult.results[i],
), ),
), ),
),
],
if (!controller.isSearchMode &&
AppSettings.showPresences.value)
GestureDetector(
onLongPress: () => controller.dismissStatusList(),
child: StatusMessageList(
onStatusEdit: controller.setStatus,
),
),
if (client.rooms.isNotEmpty && !controller.isSearchMode)
SizedBox(
height: 64,
child: ListView(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 12.0,
),
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: [
if (AppSettings.separateChatTypes.value)
ActiveFilter.messages
else
ActiveFilter.allChats,
ActiveFilter.groups,
ActiveFilter.unread,
if (spaceDelegateCandidates.isNotEmpty &&
!AppSettings.displayNavigationRail.value &&
!FluffyThemes.isColumnMode(context))
ActiveFilter.spaces,
]
.map(
(filter) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: FilterChip(
selected: filter == controller.activeFilter,
onSelected: (_) =>
controller.setActiveFilter(filter),
label:
Text(filter.toLocalizedString(context)),
),
),
)
.toList(),
),
),
if (controller.isSearchMode)
SearchTitle(
title: L10n.of(context).chats,
icon: const Icon(Icons.forum_outlined),
),
if (client.prevBatch != null &&
rooms.isEmpty &&
!controller.isSearchMode) ...[
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Stack(
alignment: Alignment.center,
children: [
const Column(
mainAxisSize: MainAxisSize.min,
children: [
DummyChatListItem(
opacity: 0.5,
animate: false,
),
DummyChatListItem(
opacity: 0.3,
animate: false,
),
],
),
Icon(
CupertinoIcons.chat_bubble_text_fill,
size: 128,
color: theme.colorScheme.secondary,
),
],
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
client.rooms.isEmpty
? L10n.of(context).noChatsFoundHere
: L10n.of(context).noMoreChatsFound,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: theme.colorScheme.secondary,
),
), ),
), ),
],
),
],
], ],
), if (!controller.isSearchMode &&
AppSettings.showPresences.value)
GestureDetector(
onLongPress: () => controller.dismissStatusList(),
child: StatusMessageList(
onStatusEdit: controller.setStatus,
),
),
if (client.rooms.isNotEmpty && !controller.isSearchMode)
SizedBox(
height: 64,
child: ListView(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 12.0,
),
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children:
[
if (AppSettings.separateChatTypes.value)
ActiveFilter.messages
else
ActiveFilter.allChats,
ActiveFilter.groups,
ActiveFilter.unread,
if (spaceDelegateCandidates.isNotEmpty &&
!AppSettings
.displayNavigationRail
.value &&
!FluffyThemes.isColumnMode(context))
ActiveFilter.spaces,
]
.map(
(filter) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: FilterChip(
selected:
filter == controller.activeFilter,
onSelected: (_) =>
controller.setActiveFilter(filter),
label: Text(
filter.toLocalizedString(context),
),
),
),
)
.toList(),
),
),
if (controller.isSearchMode)
SearchTitle(
title: L10n.of(context).chats,
icon: const Icon(Icons.forum_outlined),
),
if (client.prevBatch != null &&
rooms.isEmpty &&
!controller.isSearchMode) ...[
Column(
mainAxisAlignment: .center,
children: [
Stack(
alignment: Alignment.center,
children: [
const Column(
mainAxisSize: .min,
children: [
DummyChatListItem(opacity: 0.5, animate: false),
DummyChatListItem(opacity: 0.3, animate: false),
],
),
Icon(
CupertinoIcons.chat_bubble_text_fill,
size: 128,
color: theme.colorScheme.secondary,
),
],
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
client.rooms.isEmpty
? L10n.of(context).noChatsFoundHere
: L10n.of(context).noMoreChatsFound,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: theme.colorScheme.secondary,
),
),
),
],
),
],
]),
), ),
if (client.prevBatch == null) if (client.prevBatch == null)
SliverList( SliverList(
@ -257,10 +255,7 @@ class ChatListViewBody extends StatelessWidget {
} }
class PublicRoomsHorizontalList extends StatelessWidget { class PublicRoomsHorizontalList extends StatelessWidget {
const PublicRoomsHorizontalList({ const PublicRoomsHorizontalList({super.key, required this.publicRooms});
super.key,
required this.publicRooms,
});
final List<PublishedRoomsChunk>? publicRooms; final List<PublishedRoomsChunk>? publicRooms;
@ -279,7 +274,8 @@ class PublicRoomsHorizontalList extends StatelessWidget {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: publicRooms.length, itemCount: publicRooms.length,
itemBuilder: (context, i) => _SearchItem( itemBuilder: (context, i) => _SearchItem(
title: publicRooms[i].name ?? title:
publicRooms[i].name ??
publicRooms[i].canonicalAlias?.localpart ?? publicRooms[i].canonicalAlias?.localpart ??
L10n.of(context).group, L10n.of(context).group,
avatar: publicRooms[i].avatarUrl, avatar: publicRooms[i].avatarUrl,
@ -310,31 +306,26 @@ class _SearchItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) => InkWell( Widget build(BuildContext context) => InkWell(
onTap: onPressed, onTap: onPressed,
child: SizedBox( child: SizedBox(
width: 84, width: 84,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const SizedBox(height: 8), const SizedBox(height: 8),
Avatar( Avatar(mxContent: avatar, name: title),
mxContent: avatar, Padding(
name: title, padding: const EdgeInsets.all(8.0),
), child: Text(
Padding( title,
padding: const EdgeInsets.all(8.0), maxLines: 2,
child: Text( textAlign: TextAlign.center,
title, overflow: TextOverflow.ellipsis,
maxLines: 2, style: const TextStyle(fontSize: 12),
textAlign: TextAlign.center, ),
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 12,
),
),
),
],
), ),
), ],
); ),
),
);
} }

View file

@ -34,19 +34,19 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
title: StreamBuilder( title: StreamBuilder(
stream: client.onSyncStatus.stream, stream: client.onSyncStatus.stream,
builder: (context, snapshot) { builder: (context, snapshot) {
final status = client.onSyncStatus.value ?? final status =
client.onSyncStatus.value ??
const SyncStatusUpdate(SyncStatus.waitingForResponse); const SyncStatusUpdate(SyncStatus.waitingForResponse);
final hide = client.onSync.value != null && final hide =
client.onSync.value != null &&
status.status != SyncStatus.error && status.status != SyncStatus.error &&
client.prevBatch != null; client.prevBatch != null;
return TextField( return TextField(
controller: controller.searchController, controller: controller.searchController,
focusNode: controller.searchFocusNode, focusNode: controller.searchFocusNode,
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
onChanged: (text) => controller.onSearchEnter( onChanged: (text) =>
text, controller.onSearchEnter(text, globalSearch: globalSearch),
globalSearch: globalSearch,
),
decoration: InputDecoration( decoration: InputDecoration(
filled: true, filled: true,
fillColor: theme.colorScheme.secondaryContainer, fillColor: theme.colorScheme.secondaryContainer,
@ -66,19 +66,19 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
prefixIcon: hide prefixIcon: hide
? controller.isSearchMode ? controller.isSearchMode
? IconButton( ? IconButton(
tooltip: L10n.of(context).cancel, tooltip: L10n.of(context).cancel,
icon: const Icon(Icons.close_outlined), icon: const Icon(Icons.close_outlined),
onPressed: controller.cancelSearch, onPressed: controller.cancelSearch,
color: theme.colorScheme.onPrimaryContainer,
)
: IconButton(
onPressed: controller.startSearch,
icon: Icon(
Icons.search_outlined,
color: theme.colorScheme.onPrimaryContainer, color: theme.colorScheme.onPrimaryContainer,
), )
) : IconButton(
onPressed: controller.startSearch,
icon: Icon(
Icons.search_outlined,
color: theme.colorScheme.onPrimaryContainer,
),
)
: Container( : Container(
margin: const EdgeInsets.all(12), margin: const EdgeInsets.all(12),
width: 8, width: 8,
@ -97,37 +97,34 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
suffixIcon: controller.isSearchMode && globalSearch suffixIcon: controller.isSearchMode && globalSearch
? controller.isSearching ? controller.isSearching
? const Padding( ? const Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: 10.0, vertical: 10.0,
horizontal: 12, horizontal: 12,
),
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
), ),
), child: SizedBox.square(
) dimension: 24,
: TextButton.icon( child: CircularProgressIndicator.adaptive(
onPressed: controller.setServer, strokeWidth: 2,
style: TextButton.styleFrom( ),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(99),
), ),
textStyle: const TextStyle(fontSize: 12), )
), : TextButton.icon(
icon: const Icon(Icons.edit_outlined, size: 16), onPressed: controller.setServer,
label: Text( style: TextButton.styleFrom(
controller.searchServer ?? shape: RoundedRectangleBorder(
Matrix.of(context).client.homeserver!.host, borderRadius: BorderRadius.circular(99),
maxLines: 2, ),
), textStyle: const TextStyle(fontSize: 12),
) ),
: SizedBox( icon: const Icon(Icons.edit_outlined, size: 16),
width: 0, label: Text(
child: ClientChooserButton(controller), controller.searchServer ??
), Matrix.of(context).client.homeserver!.host,
maxLines: 2,
),
)
: SizedBox(width: 0, child: ClientChooserButton(controller)),
), ),
); );
}, },

View file

@ -48,8 +48,9 @@ class ChatListItem extends StatelessWidget {
final directChatMatrixId = room.directChatMatrixID; final directChatMatrixId = room.directChatMatrixID;
final isDirectChat = directChatMatrixId != null; final isDirectChat = directChatMatrixId != null;
final hasNotifications = room.notificationCount > 0; final hasNotifications = room.notificationCount > 0;
final backgroundColor = final backgroundColor = activeChat
activeChat ? theme.colorScheme.secondaryContainer : null; ? theme.colorScheme.secondaryContainer
: null;
final displayname = room.getLocalizedDisplayname( final displayname = room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)), MatrixLocals(L10n.of(context)),
); );
@ -64,10 +65,7 @@ class ChatListItem extends StatelessWidget {
final space = this.space; final space = this.space;
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
horizontal: 8,
vertical: 1,
),
child: Material( child: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius), borderRadius: BorderRadius.circular(AppConfig.borderRadius),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
@ -96,7 +94,8 @@ class ChatListItem extends StatelessWidget {
child: Avatar( child: Avatar(
border: BorderSide( border: BorderSide(
width: 2, width: 2,
color: backgroundColor ?? color:
backgroundColor ??
theme.colorScheme.surface, theme.colorScheme.surface,
), ),
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
@ -114,14 +113,15 @@ class ChatListItem extends StatelessWidget {
child: Avatar( child: Avatar(
border: space == null border: space == null
? room.isSpace ? room.isSpace
? BorderSide( ? BorderSide(
width: 1, width: 1,
color: theme.dividerColor, color: theme.dividerColor,
) )
: null : null
: BorderSide( : BorderSide(
width: 2, width: 2,
color: backgroundColor ?? color:
backgroundColor ??
theme.colorScheme.surface, theme.colorScheme.surface,
), ),
borderRadius: room.isSpace borderRadius: room.isSpace
@ -182,10 +182,7 @@ class ChatListItem extends StatelessWidget {
if (isMuted) if (isMuted)
const Padding( const Padding(
padding: EdgeInsets.only(left: 4.0), padding: EdgeInsets.only(left: 4.0),
child: Icon( child: Icon(Icons.notifications_off_outlined, size: 16),
Icons.notifications_off_outlined,
size: 16,
),
), ),
if (room.isFavourite) if (room.isFavourite)
Padding( Padding(
@ -202,8 +199,9 @@ class ChatListItem extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.only(left: 4.0), padding: const EdgeInsets.only(left: 4.0),
child: Text( child: Text(
room.latestEventReceivedTime room.latestEventReceivedTime.localizedTimeShort(
.localizedTimeShort(context), context,
),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: theme.colorScheme.outline, color: theme.colorScheme.outline,
@ -213,8 +211,8 @@ class ChatListItem extends StatelessWidget {
], ],
), ),
subtitle: Row( subtitle: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: <Widget>[ children: <Widget>[
if (typingText.isEmpty && if (typingText.isEmpty &&
ownMessage && ownMessage &&
@ -240,111 +238,111 @@ class ChatListItem extends StatelessWidget {
), ),
) )
: room.lastEvent?.relationshipType == : room.lastEvent?.relationshipType ==
RelationshipTypes.thread RelationshipTypes.thread
? Container( ? Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: theme.colorScheme.outline,
),
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
margin: const EdgeInsets.only(right: 4.0),
child: Row(
mainAxisSize: .min,
children: [
Icon(
Icons.message_outlined,
size: 12,
color: theme.colorScheme.outline,
),
const SizedBox(width: 4),
Text(
L10n.of(context).thread,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.outline, color: theme.colorScheme.outline,
), ),
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
), ),
padding: ],
const EdgeInsets.symmetric(horizontal: 8.0), ),
margin: const EdgeInsets.only(right: 4.0), )
child: Row( : const SizedBox.shrink(),
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.message_outlined,
size: 12,
color: theme.colorScheme.outline,
),
const SizedBox(width: 4),
Text(
L10n.of(context).thread,
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.outline,
),
),
],
),
)
: const SizedBox.shrink(),
), ),
Expanded( Expanded(
child: room.isSpace && room.membership == Membership.join child: room.isSpace && room.membership == Membership.join
? Text( ? Text(
L10n.of(context) L10n.of(
.countChats(room.spaceChildren.length), context,
).countChats(room.spaceChildren.length),
style: TextStyle(color: theme.colorScheme.outline), style: TextStyle(color: theme.colorScheme.outline),
) )
: typingText.isNotEmpty : typingText.isNotEmpty
? Text( ? Text(
typingText, typingText,
style: TextStyle( style: TextStyle(color: theme.colorScheme.primary),
color: theme.colorScheme.primary, maxLines: 1,
), softWrap: false,
maxLines: 1, )
softWrap: false, : FutureBuilder(
) key: ValueKey(
: FutureBuilder( '${lastEvent?.eventId}_${lastEvent?.type}_${lastEvent?.redacted}',
key: ValueKey( ),
'${lastEvent?.eventId}_${lastEvent?.type}_${lastEvent?.redacted}', future: needLastEventSender
), ? lastEvent.calcLocalizedBody(
future: needLastEventSender MatrixLocals(L10n.of(context)),
? lastEvent.calcLocalizedBody( hideReply: true,
MatrixLocals(L10n.of(context)), hideEdit: true,
hideReply: true, plaintextBody: true,
hideEdit: true, removeMarkdown: true,
plaintextBody: true, withSenderNamePrefix:
removeMarkdown: true, (!isDirectChat ||
withSenderNamePrefix: (!isDirectChat || directChatMatrixId !=
directChatMatrixId != room.lastEvent?.senderId),
room.lastEvent?.senderId), )
) : null,
initialData: lastEvent?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix:
(!isDirectChat ||
directChatMatrixId !=
room.lastEvent?.senderId),
),
builder: (context, snapshot) => Text(
room.membership == Membership.invite
? room
.getState(
EventTypes.RoomMember,
room.client.userID!,
)
?.content
.tryGet<String>('reason') ??
(isDirectChat
? L10n.of(context).newChatRequest
: L10n.of(context).inviteGroupChat)
: snapshot.data ??
L10n.of(context).noMessagesYet,
softWrap: false,
maxLines: room.notificationCount >= 1 ? 2 : 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: unread || room.hasNewMessages
? theme.colorScheme.onSurface
: theme.colorScheme.outline,
decoration: room.lastEvent?.redacted == true
? TextDecoration.lineThrough
: null, : null,
initialData:
lastEvent?.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)),
hideReply: true,
hideEdit: true,
plaintextBody: true,
removeMarkdown: true,
withSenderNamePrefix: (!isDirectChat ||
directChatMatrixId !=
room.lastEvent?.senderId),
),
builder: (context, snapshot) => Text(
room.membership == Membership.invite
? room
.getState(
EventTypes.RoomMember,
room.client.userID!,
)
?.content
.tryGet<String>('reason') ??
(isDirectChat
? L10n.of(context).newChatRequest
: L10n.of(context)
.inviteGroupChat)
: snapshot.data ??
L10n.of(context).noMessagesYet,
softWrap: false,
maxLines: room.notificationCount >= 1 ? 2 : 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: unread || room.hasNewMessages
? theme.colorScheme.onSurface
: theme.colorScheme.outline,
decoration: room.lastEvent?.redacted == true
? TextDecoration.lineThrough
: null,
),
),
), ),
),
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
UnreadBubble(room: room), UnreadBubble(room: room),
@ -353,27 +351,27 @@ class ChatListItem extends StatelessWidget {
onTap: onTap, onTap: onTap,
trailing: onForget == null trailing: onForget == null
? room.membership == Membership.invite ? room.membership == Membership.invite
? IconButton( ? IconButton(
tooltip: L10n.of(context).declineInvitation, tooltip: L10n.of(context).declineInvitation,
icon: const Icon(Icons.delete_forever_outlined), icon: const Icon(Icons.delete_forever_outlined),
color: theme.colorScheme.error, color: theme.colorScheme.error,
onPressed: () async { onPressed: () async {
final consent = await showOkCancelAlertDialog( final consent = await showOkCancelAlertDialog(
context: context, context: context,
title: L10n.of(context).declineInvitation, title: L10n.of(context).declineInvitation,
message: L10n.of(context).areYouSure, message: L10n.of(context).areYouSure,
okLabel: L10n.of(context).yes, okLabel: L10n.of(context).yes,
isDestructive: true, isDestructive: true,
); );
if (consent != OkCancelResult.ok) return; if (consent != OkCancelResult.ok) return;
if (!context.mounted) return; if (!context.mounted) return;
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: room.leave, future: room.leave,
); );
}, },
) )
: null : null
: IconButton( : IconButton(
icon: const Icon(Icons.delete_outlined), icon: const Icon(Icons.delete_outlined),
onPressed: onForget, onPressed: onForget,

View file

@ -38,10 +38,7 @@ class ChatListView extends StatelessWidget {
onGoToChats: controller.clearActiveSpace, onGoToChats: controller.clearActiveSpace,
onGoToSpaceId: controller.setActiveSpace, onGoToSpaceId: controller.setActiveSpace,
), ),
Container( Container(color: Theme.of(context).dividerColor, width: 1),
color: Theme.of(context).dividerColor,
width: 1,
),
], ],
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
@ -50,8 +47,8 @@ class ChatListView extends StatelessWidget {
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: Scaffold( child: Scaffold(
body: ChatListViewBody(controller), body: ChatListViewBody(controller),
floatingActionButton: !controller.isSearchMode && floatingActionButton:
controller.activeSpaceId == null !controller.isSearchMode && controller.activeSpaceId == null
? FloatingActionButton.extended( ? FloatingActionButton.extended(
onPressed: () => context.go('/rooms/newprivatechat'), onPressed: () => context.go('/rooms/newprivatechat'),
icon: const Icon(Icons.add_outlined), icon: const Icon(Icons.add_outlined),

View file

@ -25,8 +25,8 @@ class ClientChooserButton extends StatelessWidget {
(a, b) => a!.isValidMatrixId == b!.isValidMatrixId (a, b) => a!.isValidMatrixId == b!.isValidMatrixId
? 0 ? 0
: a.isValidMatrixId && !b.isValidMatrixId : a.isValidMatrixId && !b.isValidMatrixId
? -1 ? -1
: 1, : 1,
); );
return <PopupMenuEntry<Object>>[ return <PopupMenuEntry<Object>>[
PopupMenuItem( PopupMenuItem(
@ -97,8 +97,8 @@ class ClientChooserButton extends StatelessWidget {
PopupMenuItem( PopupMenuItem(
value: null, value: null,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Text( Text(
bundle!, bundle!,
@ -123,7 +123,8 @@ class ClientChooserButton extends StatelessWidget {
children: [ children: [
Avatar( Avatar(
mxContent: snapshot.data?.avatarUrl, mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ?? name:
snapshot.data?.displayName ??
client.userID!.localpart, client.userID!.localpart,
size: 32, size: 32,
), ),
@ -193,10 +194,7 @@ class ClientChooserButton extends StatelessWidget {
); );
} }
void _clientSelected( void _clientSelected(Object object, BuildContext context) async {
Object object,
BuildContext context,
) async {
if (object is Client) { if (object is Client) {
controller.setActiveClient(object); controller.setActiveClient(object);
} else if (object is String) { } else if (object is String) {

View file

@ -46,8 +46,8 @@ class NaviRailItem extends StatelessWidget {
child: AnimatedContainer( child: AnimatedContainer(
width: isSelected width: isSelected
? FluffyThemes.isColumnMode(context) ? FluffyThemes.isColumnMode(context)
? 8 ? 8
: 4 : 4
: 0, : 0,
duration: FluffyThemes.animationDuration, duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,

View file

@ -22,14 +22,8 @@ class SearchTitle extends StatelessWidget {
return Material( return Material(
shape: Border( shape: Border(
top: BorderSide( top: BorderSide(color: theme.dividerColor, width: 1),
color: theme.dividerColor, bottom: BorderSide(color: theme.dividerColor, width: 1),
width: 1,
),
bottom: BorderSide(
color: theme.dividerColor,
width: 1,
),
), ),
color: color ?? theme.colorScheme.surface, color: color ?? theme.colorScheme.surface,
child: InkWell( child: InkWell(
@ -38,10 +32,7 @@ class SearchTitle extends StatelessWidget {
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
horizontal: 16,
vertical: 8,
),
child: IconTheme( child: IconTheme(
data: theme.iconTheme.copyWith(size: 16), data: theme.iconTheme.copyWith(size: 16),
child: Row( child: Row(

View file

@ -28,12 +28,7 @@ enum AddRoomType { chat, subspace }
enum SpaceChildAction { edit, moveToSpace, removeFromSpace } enum SpaceChildAction { edit, moveToSpace, removeFromSpace }
enum SpaceActions { enum SpaceActions { settings, invite, members, leave }
settings,
invite,
members,
leave,
}
class SpaceView extends StatefulWidget { class SpaceView extends StatefulWidget {
final String spaceId; final String spaceId;
@ -124,8 +119,9 @@ class _SpaceViewState extends State<SpaceView> {
} catch (e, s) { } catch (e, s) {
Logs().w('Unable to load hierarchy', e, s); Logs().w('Unable to load hierarchy', e, s);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context) ScaffoldMessenger.of(
.showSnackBar(SnackBar(content: Text(e.toLocalizedString(context)))); context,
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
@ -141,9 +137,7 @@ class _SpaceViewState extends State<SpaceView> {
builder: (_) => PublicRoomDialog( builder: (_) => PublicRoomDialog(
chunk: item, chunk: item,
via: space?.spaceChildren via: space?.spaceChildren
.firstWhereOrNull( .firstWhereOrNull((child) => child.roomId == item.roomId)
(child) => child.roomId == item.roomId,
)
?.via, ?.via,
), ),
); );
@ -224,8 +218,9 @@ class _SpaceViewState extends State<SpaceView> {
if (roomType == AddRoomType.subspace) { if (roomType == AddRoomType.subspace) {
roomId = await client.createSpace( roomId = await client.createSpace(
name: names, name: names,
visibility: visibility: isPublicSpace
isPublicSpace ? sdk.Visibility.public : sdk.Visibility.private, ? sdk.Visibility.public
: sdk.Visibility.private,
); );
} else { } else {
roomId = await client.createGroupChat( roomId = await client.createGroupChat(
@ -234,8 +229,9 @@ class _SpaceViewState extends State<SpaceView> {
preset: isPublicSpace preset: isPublicSpace
? CreateRoomPreset.publicChat ? CreateRoomPreset.publicChat
: CreateRoomPreset.privateChat, : CreateRoomPreset.privateChat,
visibility: visibility: isPublicSpace
isPublicSpace ? sdk.Visibility.public : sdk.Visibility.private, ? sdk.Visibility.public
: sdk.Visibility.private,
initialState: isPublicSpace initialState: isPublicSpace
? null ? null
: [ : [
@ -289,7 +285,7 @@ class _SpaceViewState extends State<SpaceView> {
PopupMenuItem( PopupMenuItem(
value: SpaceChildAction.moveToSpace, value: SpaceChildAction.moveToSpace,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.move_down_outlined), const Icon(Icons.move_down_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -300,7 +296,7 @@ class _SpaceViewState extends State<SpaceView> {
PopupMenuItem( PopupMenuItem(
value: SpaceChildAction.edit, value: SpaceChildAction.edit,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.edit_outlined), const Icon(Icons.edit_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -311,7 +307,7 @@ class _SpaceViewState extends State<SpaceView> {
PopupMenuItem( PopupMenuItem(
value: SpaceChildAction.removeFromSpace, value: SpaceChildAction.removeFromSpace,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.group_remove_outlined), const Icon(Icons.group_remove_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -344,8 +340,9 @@ class _SpaceViewState extends State<SpaceView> {
.map( .map(
(space) => AdaptiveModalAction( (space) => AdaptiveModalAction(
value: space, value: space,
label: space label: space.getLocalizedDisplayname(
.getLocalizedDisplayname(MatrixLocals(L10n.of(context))), MatrixLocals(L10n.of(context)),
),
), ),
) )
.toList(), .toList(),
@ -392,19 +389,12 @@ class _SpaceViewState extends State<SpaceView> {
final displayname = final displayname =
room?.getLocalizedDisplayname() ?? L10n.of(context).nothingFound; room?.getLocalizedDisplayname() ?? L10n.of(context).nothingFound;
const avatarSize = Avatar.defaultSize / 1.5; const avatarSize = Avatar.defaultSize / 1.5;
final isAdmin = room?.canChangeStateEvent( final isAdmin = room?.canChangeStateEvent(EventTypes.SpaceChild) == true;
EventTypes.SpaceChild,
) ==
true;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: FluffyThemes.isColumnMode(context) leading: FluffyThemes.isColumnMode(context)
? null ? null
: Center( : Center(child: CloseButton(onPressed: widget.onBack)),
child: CloseButton(
onPressed: widget.onBack,
),
),
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
titleSpacing: FluffyThemes.isColumnMode(context) ? null : 0, titleSpacing: FluffyThemes.isColumnMode(context) ? null : 0,
title: ListTile( title: ListTile(
@ -432,7 +422,7 @@ class _SpaceViewState extends State<SpaceView> {
PopupMenuItem( PopupMenuItem(
value: AddRoomType.chat, value: AddRoomType.chat,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.group_add_outlined), const Icon(Icons.group_add_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -443,7 +433,7 @@ class _SpaceViewState extends State<SpaceView> {
PopupMenuItem( PopupMenuItem(
value: AddRoomType.subspace, value: AddRoomType.subspace,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.workspaces_outlined), const Icon(Icons.workspaces_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -460,7 +450,7 @@ class _SpaceViewState extends State<SpaceView> {
PopupMenuItem( PopupMenuItem(
value: SpaceActions.settings, value: SpaceActions.settings,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.settings_outlined), const Icon(Icons.settings_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -471,7 +461,7 @@ class _SpaceViewState extends State<SpaceView> {
PopupMenuItem( PopupMenuItem(
value: SpaceActions.invite, value: SpaceActions.invite,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.person_add_outlined), const Icon(Icons.person_add_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -482,7 +472,7 @@ class _SpaceViewState extends State<SpaceView> {
PopupMenuItem( PopupMenuItem(
value: SpaceActions.members, value: SpaceActions.members,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.group_outlined), const Icon(Icons.group_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -497,7 +487,7 @@ class _SpaceViewState extends State<SpaceView> {
PopupMenuItem( PopupMenuItem(
value: SpaceActions.leave, value: SpaceActions.leave,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.delete_outlined), const Icon(Icons.delete_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -510,12 +500,7 @@ class _SpaceViewState extends State<SpaceView> {
], ],
), ),
body: room == null body: room == null
? const Center( ? const Center(child: Icon(Icons.search_outlined, size: 80))
child: Icon(
Icons.search_outlined,
size: 80,
),
)
: StreamBuilder( : StreamBuilder(
stream: room.client.onSync.stream stream: room.client.onSync.stream
.where((s) => s.hasRoomUpdate) .where((s) => s.hasRoomUpdate)
@ -573,7 +558,8 @@ class _SpaceViewState extends State<SpaceView> {
); );
} }
final item = _discoveredChildren[i]; final item = _discoveredChildren[i];
final displayname = item.name ?? final displayname =
item.name ??
item.canonicalAlias ?? item.canonicalAlias ??
L10n.of(context).emptyChat; L10n.of(context).emptyChat;
if (!displayname.toLowerCase().contains(filter)) { if (!displayname.toLowerCase().contains(filter)) {
@ -589,27 +575,31 @@ class _SpaceViewState extends State<SpaceView> {
vertical: 1, vertical: 1,
), ),
child: Material( child: Material(
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(AppConfig.borderRadius), AppConfig.borderRadius,
),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
color: joinedRoom != null && color:
joinedRoom != null &&
widget.activeChat == joinedRoom.id widget.activeChat == joinedRoom.id
? theme.colorScheme.secondaryContainer ? theme.colorScheme.secondaryContainer
: Colors.transparent, : Colors.transparent,
child: HoverBuilder( child: HoverBuilder(
builder: (context, hovered) => ListTile( builder: (context, hovered) => ListTile(
visualDensity: visualDensity: const VisualDensity(
const VisualDensity(vertical: -0.5), vertical: -0.5,
contentPadding: ),
const EdgeInsets.symmetric(horizontal: 8), contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
),
onTap: joinedRoom != null onTap: joinedRoom != null
? () => widget.onChatTab(joinedRoom!) ? () => widget.onChatTab(joinedRoom!)
: () => _joinChildRoom(item), : () => _joinChildRoom(item),
onLongPress: isAdmin onLongPress: isAdmin
? () => _showSpaceChildEditMenu( ? () => _showSpaceChildEditMenu(
context, context,
item.roomId, item.roomId,
) )
: null, : null,
leading: hovered && isAdmin leading: hovered && isAdmin
? SizedBox.square( ? SizedBox.square(
@ -618,16 +608,18 @@ class _SpaceViewState extends State<SpaceView> {
splashRadius: avatarSize, splashRadius: avatarSize,
iconSize: 14, iconSize: 14,
style: IconButton.styleFrom( style: IconButton.styleFrom(
foregroundColor: theme.colorScheme foregroundColor: theme
.colorScheme
.onTertiaryContainer, .onTertiaryContainer,
backgroundColor: theme backgroundColor: theme
.colorScheme.tertiaryContainer, .colorScheme
.tertiaryContainer,
), ),
onPressed: () => onPressed: () =>
_showSpaceChildEditMenu( _showSpaceChildEditMenu(
context, context,
item.roomId, item.roomId,
), ),
icon: const Icon(Icons.edit_outlined), icon: const Icon(Icons.edit_outlined),
), ),
) )
@ -637,11 +629,13 @@ class _SpaceViewState extends State<SpaceView> {
name: '#', name: '#',
backgroundColor: backgroundColor:
theme.colorScheme.surfaceContainer, theme.colorScheme.surfaceContainer,
textColor: item.name?.darkColor ?? textColor:
item.name?.darkColor ??
theme.colorScheme.onSurface, theme.colorScheme.onSurface,
border: item.roomType == 'm.space' border: item.roomType == 'm.space'
? BorderSide( ? BorderSide(
color: theme.colorScheme color: theme
.colorScheme
.surfaceContainerHighest, .surfaceContainerHighest,
) )
: null, : null,

View file

@ -13,10 +13,7 @@ import '../../widgets/adaptive_dialogs/user_dialog.dart';
class StatusMessageList extends StatelessWidget { class StatusMessageList extends StatelessWidget {
final void Function() onStatusEdit; final void Function() onStatusEdit;
const StatusMessageList({ const StatusMessageList({required this.onStatusEdit, super.key});
required this.onStatusEdit,
super.key,
});
static const double height = 116; static const double height = 116;
@ -24,10 +21,7 @@ class StatusMessageList extends StatelessWidget {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
if (profile.userId == client.userID) return onStatusEdit(); if (profile.userId == client.userID) return onStatusEdit();
UserDialog.show( UserDialog.show(context: context, profile: profile);
context: context,
profile: profile,
);
return; return;
} }
@ -56,8 +50,9 @@ class StatusMessageList extends StatelessWidget {
), ),
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
final presences = final presences = snapshot.data
snapshot.data?.where(isInterestingPresence).toList(); ?.where(isInterestingPresence)
.toList();
// If no other presences than the own entry is interesting, we // If no other presences than the own entry is interesting, we
// hide the presence header. // hide the presence header.
@ -121,7 +116,8 @@ class PresenceAvatar extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final profile = snapshot.data; final profile = snapshot.data;
final displayName = profile?.displayName ?? final displayName =
profile?.displayName ??
presence.userid.localpart ?? presence.userid.localpart ??
presence.userid; presence.userid;
final statusMsg = presence.statusMsg; final statusMsg = presence.statusMsg;
@ -152,8 +148,9 @@ class PresenceAvatar extends StatelessWidget {
padding: const EdgeInsets.all(3), padding: const EdgeInsets.all(3),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: presence.gradient, gradient: presence.gradient,
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(avatarSize), avatarSize,
),
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Container( child: Container(
@ -161,8 +158,9 @@ class PresenceAvatar extends StatelessWidget {
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.surface, color: theme.colorScheme.surface,
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(avatarSize), avatarSize,
),
), ),
padding: const EdgeInsets.all(3.0), padding: const EdgeInsets.all(3.0),
child: Avatar( child: Avatar(
@ -202,9 +200,8 @@ class PresenceAvatar extends StatelessWidget {
right: 8, right: 8,
child: Column( child: Column(
spacing: 2, spacing: 2,
crossAxisAlignment: crossAxisAlignment: .start,
CrossAxisAlignment.start, mainAxisSize: .min,
mainAxisSize: MainAxisSize.min,
children: [ children: [
Material( Material(
elevation: statusMsgBubbleElevation, elevation: statusMsgBubbleElevation,
@ -230,8 +227,9 @@ class PresenceAvatar extends StatelessWidget {
), ),
), ),
Padding( Padding(
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(left: 8.0), left: 8.0,
),
child: Material( child: Material(
color: statusMsgBubbleColor, color: statusMsgBubbleColor,
elevation: statusMsgBubbleElevation, elevation: statusMsgBubbleElevation,
@ -246,8 +244,9 @@ class PresenceAvatar extends StatelessWidget {
), ),
), ),
Padding( Padding(
padding: padding: const EdgeInsets.only(
const EdgeInsets.only(left: 13.0), left: 13.0,
),
child: Material( child: Material(
color: statusMsgBubbleColor, color: statusMsgBubbleColor,
elevation: statusMsgBubbleElevation, elevation: statusMsgBubbleElevation,
@ -280,9 +279,7 @@ class PresenceAvatar extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(fontSize: 11),
fontSize: 11,
),
), ),
), ),
], ],
@ -296,10 +293,12 @@ class PresenceAvatar extends StatelessWidget {
extension on Client { extension on Client {
Set<String> get interestingPresences { Set<String> get interestingPresences {
final allHeroes = rooms.map((room) => room.summary.mHeroes).fold( final allHeroes = rooms
<String>{}, .map((room) => room.summary.mHeroes)
(previousValue, element) => previousValue..addAll(element ?? {}), .fold(
); <String>{},
(previousValue, element) => previousValue..addAll(element ?? {}),
);
allHeroes.add(userID!); allHeroes.add(userID!);
return allHeroes; return allHeroes;
} }
@ -317,31 +316,23 @@ extension on CachedPresence {
LinearGradient get gradient => presence.isOnline == true LinearGradient get gradient => presence.isOnline == true
? LinearGradient( ? LinearGradient(
colors: [ colors: [Colors.green, Colors.green.shade200, Colors.green.shade900],
Colors.green,
Colors.green.shade200,
Colors.green.shade900,
],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
) )
: presence.isUnavailable : presence.isUnavailable
? LinearGradient( ? LinearGradient(
colors: [ colors: [
Colors.yellow, Colors.yellow,
Colors.yellow.shade200, Colors.yellow.shade200,
Colors.yellow.shade900, Colors.yellow.shade900,
], ],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
) )
: LinearGradient( : LinearGradient(
colors: [ colors: [Colors.grey, Colors.grey.shade200, Colors.grey.shade900],
Colors.grey, begin: Alignment.topLeft,
Colors.grey.shade200, end: Alignment.bottomRight,
Colors.grey.shade900, );
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
} }

View file

@ -15,8 +15,8 @@ class UnreadBubble extends StatelessWidget {
final hasNotifications = room.notificationCount > 0; final hasNotifications = room.notificationCount > 0;
final unreadBubbleSize = unread || room.hasNewMessages final unreadBubbleSize = unread || room.hasNewMessages
? room.notificationCount > 0 ? room.notificationCount > 0
? 20.0 ? 20.0
: 14.0 : 14.0
: 0.0; : 0.0;
return AnimatedContainer( return AnimatedContainer(
duration: FluffyThemes.animationDuration, duration: FluffyThemes.animationDuration,
@ -27,13 +27,13 @@ class UnreadBubble extends StatelessWidget {
width: !hasNotifications && !unread && !room.hasNewMessages width: !hasNotifications && !unread && !room.hasNewMessages
? 0 ? 0
: (unreadBubbleSize - 9) * room.notificationCount.toString().length + : (unreadBubbleSize - 9) * room.notificationCount.toString().length +
9, 9,
decoration: BoxDecoration( decoration: BoxDecoration(
color: room.highlightCount > 0 color: room.highlightCount > 0
? theme.colorScheme.error ? theme.colorScheme.error
: hasNotifications || room.markedUnread : hasNotifications || room.markedUnread
? theme.colorScheme.primary ? theme.colorScheme.primary
: theme.colorScheme.primaryContainer, : theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(7), borderRadius: BorderRadius.circular(7),
), ),
child: hasNotifications child: hasNotifications
@ -43,8 +43,8 @@ class UnreadBubble extends StatelessWidget {
color: room.highlightCount > 0 color: room.highlightCount > 0
? theme.colorScheme.onError ? theme.colorScheme.onError
: hasNotifications : hasNotifications
? theme.colorScheme.onPrimary ? theme.colorScheme.onPrimary
: theme.colorScheme.onPrimaryContainer, : theme.colorScheme.onPrimaryContainer,
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),

View file

@ -32,8 +32,7 @@ class ChatMembersController extends State<ChatMembersPage> {
void setFilter([dynamic _]) async { void setFilter([dynamic _]) async {
final filter = filterController.text.toLowerCase().trim(); final filter = filterController.text.toLowerCase().trim();
final members = this final members = this.members
.members
?.where((member) => member.membership == membershipFilter) ?.where((member) => member.membership == membershipFilter)
.toList(); .toList();
@ -45,14 +44,15 @@ class ChatMembersController extends State<ChatMembersPage> {
return; return;
} }
setState(() { setState(() {
filteredMembers = members filteredMembers =
?.where( members
(user) => ?.where(
user.displayName?.toLowerCase().contains(filter) ?? (user) =>
user.id.toLowerCase().contains(filter), user.displayName?.toLowerCase().contains(filter) ??
) user.id.toLowerCase().contains(filter),
.toList() )
?..sort((b, a) => a.powerLevel.compareTo(b.powerLevel)); .toList()
?..sort((b, a) => a.powerLevel.compareTo(b.powerLevel));
}); });
} }
@ -62,8 +62,7 @@ class ChatMembersController extends State<ChatMembersPage> {
setState(() { setState(() {
error = null; error = null;
}); });
final participants = await Matrix.of(context) final participants = await Matrix.of(context).client
.client
.getRoomById(widget.roomId) .getRoomById(widget.roomId)
?.requestParticipants( ?.requestParticipants(
[...Membership.values]..remove(Membership.leave), [...Membership.values]..remove(Membership.leave),
@ -76,8 +75,11 @@ class ChatMembersController extends State<ChatMembersPage> {
}); });
setFilter(); setFilter();
} catch (e, s) { } catch (e, s) {
Logs() Logs().d(
.d('Unable to request participants. Try again in 3 seconds...', e, s); 'Unable to request participants. Try again in 3 seconds...',
e,
s,
);
setState(() { setState(() {
error = e; error = e;
}); });
@ -91,14 +93,12 @@ class ChatMembersController extends State<ChatMembersPage> {
super.initState(); super.initState();
refreshMembers(); refreshMembers();
_updateSub = Matrix.of(context) _updateSub = Matrix.of(context).client.onSync.stream
.client
.onSync
.stream
.where( .where(
(syncUpdate) => (syncUpdate) =>
syncUpdate.rooms?.join?[widget.roomId]?.timeline?.events syncUpdate.rooms?.join?[widget.roomId]?.timeline?.events?.any(
?.any((state) => state.type == EventTypes.RoomMember) ?? (state) => state.type == EventTypes.RoomMember,
) ??
false, false,
) )
.listen(refreshMembers); .listen(refreshMembers);

View file

@ -17,13 +17,12 @@ class ChatMembersView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final room = final room = Matrix.of(
Matrix.of(context).client.getRoomById(controller.widget.roomId); context,
).client.getRoomById(controller.widget.roomId);
if (room == null) { if (room == null) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: Text(L10n.of(context).oopsSomethingWentWrong)),
title: Text(L10n.of(context).oopsSomethingWentWrong),
),
body: Center( body: Center(
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
), ),
@ -32,7 +31,8 @@ class ChatMembersView extends StatelessWidget {
final members = controller.filteredMembers; final members = controller.filteredMembers;
final roomCount = (room.summary.mJoinedMemberCount ?? 0) + final roomCount =
(room.summary.mJoinedMemberCount ?? 0) +
(room.summary.mInvitedMemberCount ?? 0); (room.summary.mInvitedMemberCount ?? 0);
final error = controller.error; final error = controller.error;
@ -41,16 +41,12 @@ class ChatMembersView extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: const Center(child: BackButton()), leading: const Center(child: BackButton()),
title: Text( title: Text(L10n.of(context).countParticipants(roomCount)),
L10n.of(context).countParticipants(roomCount),
),
actions: [ actions: [
if (room.canInvite) if (room.canInvite)
IconButton( IconButton(
onPressed: () => context.go('/rooms/${room.id}/invite'), onPressed: () => context.go('/rooms/${room.id}/invite'),
icon: const Icon( icon: const Icon(Icons.person_add_outlined),
Icons.person_add_outlined,
),
), ),
], ],
), ),
@ -62,7 +58,7 @@ class ChatMembersView extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.error_outline), const Icon(Icons.error_outline),
Text(error.toLocalizedString(context)), Text(error.toLocalizedString(context)),
@ -77,120 +73,117 @@ class ChatMembersView extends StatelessWidget {
), ),
) )
: members == null : members == null
? const Center( ? const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator.adaptive(), child: CircularProgressIndicator.adaptive(),
), ),
) )
: ListView.builder( : ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemCount: members.length + 1, itemCount: members.length + 1,
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i == 0) { if (i == 0) {
final availableFilters = Membership.values final availableFilters = Membership.values
.where( .where(
(membership) => (membership) =>
controller.members?.any( controller.members?.any(
(member) => member.membership == membership, (member) => member.membership == membership,
) ?? ) ??
false, false,
) )
.toList(); .toList();
availableFilters availableFilters.sort(
.sort((a, b) => a == Membership.join ? -1 : 1); (a, b) => a == Membership.join ? -1 : 1,
return Column( );
mainAxisSize: MainAxisSize.min, return Column(
children: [ mainAxisSize: .min,
Padding( children: [
padding: const EdgeInsets.all(16.0), Padding(
child: TextField( padding: const EdgeInsets.all(16.0),
controller: controller.filterController, child: TextField(
onChanged: controller.setFilter, controller: controller.filterController,
decoration: InputDecoration( onChanged: controller.setFilter,
filled: true, decoration: InputDecoration(
fillColor: filled: true,
theme.colorScheme.secondaryContainer, fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide.none, borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(99), borderRadius: BorderRadius.circular(99),
), ),
hintStyle: TextStyle( hintStyle: TextStyle(
color: theme.colorScheme.onPrimaryContainer, color: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.normal, fontWeight: FontWeight.normal,
), ),
prefixIcon: const Icon(Icons.search_outlined), prefixIcon: const Icon(Icons.search_outlined),
hintText: L10n.of(context).search, hintText: L10n.of(context).search,
),
),
),
if (availableFilters.length > 1)
SizedBox(
height: 64,
child: ListView.builder(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 12.0,
),
scrollDirection: Axis.horizontal,
itemCount: availableFilters.length,
itemBuilder: (context, i) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: FilterChip(
label: Text(switch (availableFilters[i]) {
Membership.ban => L10n.of(context).banned,
Membership.invite =>
L10n.of(context).countInvited(
room.summary.mInvitedMemberCount ??
controller.members
?.where(
(member) =>
member.membership ==
Membership.invite,
)
.length ??
0,
),
Membership.join =>
L10n.of(context).countParticipants(
room.summary.mJoinedMemberCount ??
controller.members
?.where(
(member) =>
member.membership ==
Membership.join,
)
.length ??
0,
),
Membership.knock => L10n.of(
context,
).knocking,
Membership.leave => L10n.of(
context,
).leftTheChat,
}),
selected:
controller.membershipFilter ==
availableFilters[i],
onSelected: (_) => controller
.setMembershipFilter(availableFilters[i]),
), ),
), ),
), ),
if (availableFilters.length > 1) ),
SizedBox( ],
height: 64, );
child: ListView.builder( }
padding: const EdgeInsets.symmetric( i--;
horizontal: 12.0, return ParticipantListItem(members[i]);
vertical: 12.0, },
), ),
scrollDirection: Axis.horizontal,
itemCount: availableFilters.length,
itemBuilder: (context, i) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: FilterChip(
label: Text(
switch (availableFilters[i]) {
Membership.ban =>
L10n.of(context).banned,
Membership.invite =>
L10n.of(context).countInvited(
room.summary
.mInvitedMemberCount ??
controller.members
?.where(
(member) =>
member.membership ==
Membership.invite,
)
.length ??
0,
),
Membership.join =>
L10n.of(context).countParticipants(
room.summary.mJoinedMemberCount ??
controller.members
?.where(
(member) =>
member.membership ==
Membership.join,
)
.length ??
0,
),
Membership.knock =>
L10n.of(context).knocking,
Membership.leave =>
L10n.of(context).leftTheChat,
},
),
selected: controller.membershipFilter ==
availableFilters[i],
onSelected: (_) =>
controller.setMembershipFilter(
availableFilters[i],
),
),
),
),
),
],
);
}
i--;
return ParticipantListItem(members[i]);
},
),
), ),
); );
} }

View file

@ -30,9 +30,9 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
}) async { }) async {
final room = Matrix.of(context).client.getRoomById(roomId!)!; final room = Matrix.of(context).client.getRoomById(roomId!)!;
if (!room.canSendEvent(EventTypes.RoomPowerLevels)) { if (!room.canSendEvent(EventTypes.RoomPowerLevels)) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(L10n.of(context).noPermission)), context,
); ).showSnackBar(SnackBar(content: Text(L10n.of(context).noPermission)));
return; return;
} }
newLevel ??= await showPermissionChooser( newLevel ??= await showPermissionChooser(
@ -64,12 +64,13 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
} }
Stream get onChanged => Matrix.of(context).client.onSync.stream.where( Stream get onChanged => Matrix.of(context).client.onSync.stream.where(
(e) => (e) =>
(e.rooms?.join?.containsKey(roomId) ?? false) && (e.rooms?.join?.containsKey(roomId) ?? false) &&
(e.rooms!.join![roomId!]?.timeline?.events (e.rooms!.join![roomId!]?.timeline?.events?.any(
?.any((s) => s.type == EventTypes.RoomPowerLevels) ?? (s) => s.type == EventTypes.RoomPowerLevels,
false), ) ??
); false),
);
@override @override
Widget build(BuildContext context) => ChatPermissionsSettingsView(this); Widget build(BuildContext context) => ChatPermissionsSettingsView(this);

View file

@ -45,9 +45,7 @@ class ChatPermissionsSettingsView extends StatelessWidget {
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.info_outlined), leading: const Icon(Icons.info_outlined),
subtitle: Text( subtitle: Text(L10n.of(context).chatPermissionsDescription),
L10n.of(context).chatPermissionsDescription,
),
), ),
Divider(color: theme.dividerColor), Divider(color: theme.dividerColor),
ListTile( ListTile(
@ -60,7 +58,7 @@ class ChatPermissionsSettingsView extends StatelessWidget {
), ),
), ),
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
for (final entry in powerLevels.entries) for (final entry in powerLevels.entries)
PermissionsListTile( PermissionsListTile(
@ -87,12 +85,14 @@ class ChatPermissionsSettingsView extends StatelessWidget {
Builder( Builder(
builder: (context) { builder: (context) {
const key = 'rooms'; const key = 'rooms';
final value = powerLevelsContent final value =
.containsKey('notifications') powerLevelsContent.containsKey('notifications')
? powerLevelsContent ? powerLevelsContent
.tryGetMap<String, Object?>('notifications') .tryGetMap<String, Object?>(
?.tryGet<int>('rooms') ?? 'notifications',
0 )
?.tryGet<int>('rooms') ??
0
: 0; : 0;
return PermissionsListTile( return PermissionsListTile(
permissionKey: key, permissionKey: key,

View file

@ -76,8 +76,8 @@ class PermissionsListTile extends StatelessWidget {
final color = permission >= 100 final color = permission >= 100
? Colors.orangeAccent ? Colors.orangeAccent
: permission >= 50 : permission >= 50
? Colors.blueAccent ? Colors.blueAccent
: Colors.greenAccent; : Colors.greenAccent;
return ListTile( return ListTile(
title: Text( title: Text(
getLocalizedPowerLevelString(context), getLocalizedPowerLevelString(context),
@ -110,14 +110,12 @@ class PermissionsListTile extends StatelessWidget {
DropdownMenuItem( DropdownMenuItem(
value: permission >= 100 ? permission : 100, value: permission >= 100 ? permission : 100,
child: Text( child: Text(
L10n.of(context) L10n.of(
.adminLevel(permission >= 100 ? permission : 100), context,
).adminLevel(permission >= 100 ? permission : 100),
), ),
), ),
DropdownMenuItem( DropdownMenuItem(value: null, child: Text(L10n.of(context).custom)),
value: null,
child: Text(L10n.of(context).custom),
),
], ],
), ),
), ),

View file

@ -11,10 +11,8 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
class ChatSearchFilesTab extends StatelessWidget { class ChatSearchFilesTab extends StatelessWidget {
final Room room; final Room room;
final Stream<(List<Event>, String?)>? searchStream; final Stream<(List<Event>, String?)>? searchStream;
final void Function({ final void Function({String? prevBatch, List<Event>? previousSearchResult})
String? prevBatch, startSearch;
List<Event>? previousSearchResult,
}) startSearch;
const ChatSearchFilesTab({ const ChatSearchFilesTab({
required this.room, required this.room,
@ -32,15 +30,13 @@ class ChatSearchFilesTab extends StatelessWidget {
final events = snapshot.data?.$1; final events = snapshot.data?.$1;
if (searchStream == null || events == null) { if (searchStream == null || events == null) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
const CircularProgressIndicator.adaptive(strokeWidth: 2), const CircularProgressIndicator.adaptive(strokeWidth: 2),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
L10n.of(context).searchIn( L10n.of(context).searchIn(
room.getLocalizedDisplayname( room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
MatrixLocals(L10n.of(context)),
),
), ),
), ),
], ],
@ -49,7 +45,7 @@ class ChatSearchFilesTab extends StatelessWidget {
if (events.isEmpty) { if (events.isEmpty) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
const Icon(Icons.file_present_outlined, size: 64), const Icon(Icons.file_present_outlined, size: 64),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -68,9 +64,7 @@ class ChatSearchFilesTab extends StatelessWidget {
return const Padding( return const Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
child: Center( child: Center(
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(strokeWidth: 2),
strokeWidth: 2,
),
), ),
); );
} }
@ -90,35 +84,35 @@ class ChatSearchFilesTab extends StatelessWidget {
prevBatch: nextBatch, prevBatch: nextBatch,
previousSearchResult: events, previousSearchResult: events,
), ),
icon: const Icon( icon: const Icon(Icons.arrow_downward_outlined),
Icons.arrow_downward_outlined,
),
label: Text(L10n.of(context).searchMore), label: Text(L10n.of(context).searchMore),
), ),
), ),
); );
} }
final event = events[i]; final event = events[i];
final filename = event.content.tryGet<String>('filename') ?? final filename =
event.content.tryGet<String>('filename') ??
event.content.tryGet<String>('body') ?? event.content.tryGet<String>('body') ??
L10n.of(context).unknownEvent('File'); L10n.of(context).unknownEvent('File');
final filetype = (filename.contains('.') final filetype = (filename.contains('.')
? filename.split('.').last.toUpperCase() ? filename.split('.').last.toUpperCase()
: event.content : event.content
.tryGetMap<String, dynamic>('info') .tryGetMap<String, dynamic>('info')
?.tryGet<String>('mimetype') ?.tryGet<String>('mimetype')
?.toUpperCase() ?? ?.toUpperCase() ??
'UNKNOWN'); 'UNKNOWN');
final sizeString = event.sizeString; final sizeString = event.sizeString;
final prevEvent = i > 0 ? events[i - 1] : null; final prevEvent = i > 0 ? events[i - 1] : null;
final sameEnvironment = prevEvent == null final sameEnvironment = prevEvent == null
? false ? false
: prevEvent.originServerTs : prevEvent.originServerTs.sameEnvironment(
.sameEnvironment(event.originServerTs); event.originServerTs,
);
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
if (!sameEnvironment) ...[ if (!sameEnvironment) ...[
Row( Row(
@ -148,8 +142,9 @@ class ChatSearchFilesTab extends StatelessWidget {
const SizedBox(height: 4), const SizedBox(height: 4),
], ],
Material( Material(
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(AppConfig.borderRadius), AppConfig.borderRadius,
),
color: theme.colorScheme.onInverseSurface, color: theme.colorScheme.onInverseSurface,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: ListTile( child: ListTile(

View file

@ -13,10 +13,8 @@ import 'package:fluffychat/widgets/mxc_image.dart';
class ChatSearchImagesTab extends StatelessWidget { class ChatSearchImagesTab extends StatelessWidget {
final Room room; final Room room;
final Stream<(List<Event>, String?)>? searchStream; final Stream<(List<Event>, String?)>? searchStream;
final void Function({ final void Function({String? prevBatch, List<Event>? previousSearchResult})
String? prevBatch, startSearch;
List<Event>? previousSearchResult,
}) startSearch;
const ChatSearchImagesTab({ const ChatSearchImagesTab({
required this.room, required this.room,
@ -35,15 +33,13 @@ class ChatSearchImagesTab extends StatelessWidget {
final events = snapshot.data?.$1; final events = snapshot.data?.$1;
if (searchStream == null || events == null) { if (searchStream == null || events == null) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
const CircularProgressIndicator.adaptive(strokeWidth: 2), const CircularProgressIndicator.adaptive(strokeWidth: 2),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
L10n.of(context).searchIn( L10n.of(context).searchIn(
room.getLocalizedDisplayname( room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
MatrixLocals(L10n.of(context)),
),
), ),
), ),
], ],
@ -51,7 +47,7 @@ class ChatSearchImagesTab extends StatelessWidget {
} }
if (events.isEmpty) { if (events.isEmpty) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
const Icon(Icons.photo_outlined, size: 64), const Icon(Icons.photo_outlined, size: 64),
const SizedBox(height: 8), const SizedBox(height: 8),
@ -80,9 +76,7 @@ class ChatSearchImagesTab extends StatelessWidget {
return const Padding( return const Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
child: Center( child: Center(
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(strokeWidth: 2),
strokeWidth: 2,
),
), ),
); );
} }
@ -102,9 +96,7 @@ class ChatSearchImagesTab extends StatelessWidget {
prevBatch: nextBatch, prevBatch: nextBatch,
previousSearchResult: events, previousSearchResult: events,
), ),
icon: const Icon( icon: const Icon(Icons.arrow_downward_outlined),
Icons.arrow_downward_outlined,
),
label: Text(L10n.of(context).searchMore), label: Text(L10n.of(context).searchMore),
), ),
), ),
@ -113,16 +105,13 @@ class ChatSearchImagesTab extends StatelessWidget {
final monthEvents = eventsByMonthList[i].value; final monthEvents = eventsByMonthList[i].value;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: Container( child: Container(height: 1, color: theme.dividerColor),
height: 1,
color: theme.dividerColor,
),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@ -135,10 +124,7 @@ class ChatSearchImagesTab extends StatelessWidget {
), ),
), ),
Expanded( Expanded(
child: Container( child: Container(height: 1, color: theme.dividerColor),
height: 1,
color: theme.dividerColor,
),
), ),
], ],
), ),
@ -150,39 +136,35 @@ class ChatSearchImagesTab extends StatelessWidget {
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
padding: const EdgeInsets.all(padding), padding: const EdgeInsets.all(padding),
crossAxisCount: 3, crossAxisCount: 3,
children: monthEvents.map( children: monthEvents.map((event) {
(event) { if (event.messageType == MessageTypes.Video) {
if (event.messageType == MessageTypes.Video) { return Material(
return Material( clipBehavior: Clip.hardEdge,
clipBehavior: Clip.hardEdge,
borderRadius: borderRadius,
child: EventVideoPlayer(event),
);
}
return InkWell(
onTap: () => showDialog(
context: context,
builder: (_) => ImageViewer(
event,
outerContext: context,
),
),
borderRadius: borderRadius, borderRadius: borderRadius,
child: Material( child: EventVideoPlayer(event),
clipBehavior: Clip.hardEdge,
borderRadius: borderRadius,
child: MxcImage(
event: event,
width: 128,
height: 128,
fit: BoxFit.cover,
animated: true,
isThumbnail: true,
),
),
); );
}, }
).toList(), return InkWell(
onTap: () => showDialog(
context: context,
builder: (_) =>
ImageViewer(event, outerContext: context),
),
borderRadius: borderRadius,
child: Material(
clipBehavior: Clip.hardEdge,
borderRadius: borderRadius,
child: MxcImage(
event: event,
width: 128,
height: 128,
fit: BoxFit.cover,
animated: true,
isThumbnail: true,
),
),
);
}).toList(),
), ),
], ],
); );

View file

@ -14,10 +14,8 @@ class ChatSearchMessageTab extends StatelessWidget {
final String searchQuery; final String searchQuery;
final Room room; final Room room;
final Stream<(List<Event>, String?)>? searchStream; final Stream<(List<Event>, String?)>? searchStream;
final void Function({ final void Function({String? prevBatch, List<Event>? previousSearchResult})
String? prevBatch, startSearch;
List<Event>? previousSearchResult,
}) startSearch;
const ChatSearchMessageTab({ const ChatSearchMessageTab({
required this.searchQuery, required this.searchQuery,
@ -36,15 +34,13 @@ class ChatSearchMessageTab extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
if (searchStream == null) { if (searchStream == null) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
const Icon(Icons.search_outlined, size: 64), const Icon(Icons.search_outlined, size: 64),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
L10n.of(context).searchIn( L10n.of(context).searchIn(
room.getLocalizedDisplayname( room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
MatrixLocals(L10n.of(context)),
),
), ),
), ),
], ],
@ -55,19 +51,15 @@ class ChatSearchMessageTab extends StatelessWidget {
return SelectionArea( return SelectionArea(
child: ListView.separated( child: ListView.separated(
itemCount: events.length + 1, itemCount: events.length + 1,
separatorBuilder: (context, _) => Divider( separatorBuilder: (context, _) =>
color: theme.dividerColor, Divider(color: theme.dividerColor, height: 1),
height: 1,
),
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i == events.length) { if (i == events.length) {
if (snapshot.connectionState != ConnectionState.done) { if (snapshot.connectionState != ConnectionState.done) {
return const Padding( return const Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
child: Center( child: Center(
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(strokeWidth: 2),
strokeWidth: 2,
),
), ),
); );
} }
@ -87,9 +79,7 @@ class ChatSearchMessageTab extends StatelessWidget {
prevBatch: nextBatch, prevBatch: nextBatch,
previousSearchResult: events, previousSearchResult: events,
), ),
icon: const Icon( icon: const Icon(Icons.arrow_downward_outlined),
Icons.arrow_downward_outlined,
),
label: Text(L10n.of(context).searchMore), label: Text(L10n.of(context).searchMore),
), ),
), ),
@ -134,15 +124,9 @@ class _MessageSearchResultListTile extends StatelessWidget {
return ListTile( return ListTile(
title: Row( title: Row(
children: [ children: [
Avatar( Avatar(mxContent: sender.avatarUrl, name: displayname, size: 16),
mxContent: sender.avatarUrl,
name: displayname,
size: 16,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(displayname),
displayname,
),
Expanded( Expanded(
child: Text( child: Text(
' | ${event.originServerTs.localizedTimeShort(context)}', ' | ${event.originServerTs.localizedTimeShort(context)}',
@ -164,23 +148,16 @@ class _MessageSearchResultListTile extends StatelessWidget {
.calcLocalizedBodyFallback( .calcLocalizedBodyFallback(
plaintextBody: true, plaintextBody: true,
removeMarkdown: true, removeMarkdown: true,
MatrixLocals( MatrixLocals(L10n.of(context)),
L10n.of(context),
),
) )
.trim(), .trim(),
maxLines: 7, maxLines: 7,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
trailing: IconButton( trailing: IconButton(
icon: const Icon( icon: const Icon(Icons.chevron_right_outlined),
Icons.chevron_right_outlined,
),
onPressed: () => context.go( onPressed: () => context.go(
'/${Uri( '/${Uri(pathSegments: ['rooms', room.id], queryParameters: {'event': event.eventId})}',
pathSegments: ['rooms', room.id],
queryParameters: {'event': event.eventId},
)}',
), ),
), ),
); );

View file

@ -48,9 +48,7 @@ class ChatSearchView extends StatelessWidget {
if (FluffyThemes.isThreeColumnMode(context)) if (FluffyThemes.isThreeColumnMode(context))
const SizedBox(height: 16), const SizedBox(height: 16),
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 16),
horizontal: 16,
),
child: TextField( child: TextField(
controller: controller.searchController, controller: controller.searchController,
onSubmitted: (_) => controller.restartSearch(), onSubmitted: (_) => controller.restartSearch(),

View file

@ -59,10 +59,7 @@ class DevicesSettingsController extends State<DevicesSettings> {
.tryGetMap<String, Object?>('org.matrix.msc2965.authentication') .tryGetMap<String, Object?>('org.matrix.msc2965.authentication')
?.tryGet<String>('account'); ?.tryGet<String>('account');
if (accountManageUrl != null) { if (accountManageUrl != null) {
launchUrlString( launchUrlString(accountManageUrl, mode: LaunchMode.inAppBrowserView);
accountManageUrl,
mode: LaunchMode.inAppBrowserView,
);
return; return;
} }
if (await showOkCancelAlertDialog( if (await showOkCancelAlertDialog(
@ -86,10 +83,7 @@ class DevicesSettingsController extends State<DevicesSettings> {
context: context, context: context,
delay: false, delay: false,
future: () => matrix.client.uiaRequestBackground( future: () => matrix.client.uiaRequestBackground(
(auth) => matrix.client.deleteDevices( (auth) => matrix.client.deleteDevices(deviceIds, auth: auth),
deviceIds,
auth: auth,
),
), ),
); );
reload(); reload();
@ -106,9 +100,9 @@ class DevicesSettingsController extends State<DevicesSettings> {
if (displayName == null) return; if (displayName == null) return;
final success = await showFutureLoadingDialog( final success = await showFutureLoadingDialog(
context: context, context: context,
future: () => Matrix.of(context) future: () => Matrix.of(
.client context,
.updateDevice(device.deviceId, displayName: displayName), ).client.updateDevice(device.deviceId, displayName: displayName),
); );
if (success.error == null) { if (success.error == null) {
reload(); reload();
@ -130,8 +124,10 @@ class DevicesSettingsController extends State<DevicesSettings> {
.deviceKeys[device.deviceId]! .deviceKeys[device.deviceId]!
.startVerification(); .startVerification();
req.onUpdate = () { req.onUpdate = () {
if ({KeyVerificationState.error, KeyVerificationState.done} if ({
.contains(req.state)) { KeyVerificationState.error,
KeyVerificationState.done,
}.contains(req.state)) {
setState(() {}); setState(() {});
} }
}; };
@ -162,9 +158,7 @@ class DevicesSettingsController extends State<DevicesSettings> {
bool _isOwnDevice(Device userDevice) => bool _isOwnDevice(Device userDevice) =>
userDevice.deviceId == Matrix.of(context).client.deviceID; userDevice.deviceId == Matrix.of(context).client.deviceID;
Device? get thisDevice => devices!.firstWhereOrNull( Device? get thisDevice => devices!.firstWhereOrNull(_isOwnDevice);
_isOwnDevice,
);
List<Device> get notThisDevice => List<Device>.from(devices!) List<Device> get notThisDevice => List<Device>.from(devices!)
..removeWhere(_isOwnDevice) ..removeWhere(_isOwnDevice)

View file

@ -27,7 +27,7 @@ class DevicesSettingsView extends StatelessWidget {
if (snapshot.hasError) { if (snapshot.hasError) {
return Center( return Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: <Widget>[ children: <Widget>[
const Icon(Icons.error_outlined), const Icon(Icons.error_outlined),
Text(snapshot.error.toString()), Text(snapshot.error.toString()),
@ -47,7 +47,7 @@ class DevicesSettingsView extends StatelessWidget {
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
if (i == 0) { if (i == 0) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
if (controller.chatBackupEnabled == false) if (controller.chatBackupEnabled == false)
Padding( Padding(
@ -57,8 +57,9 @@ class DevicesSettingsView extends StatelessWidget {
child: Icon(Icons.info_outlined), child: Icon(Icons.info_outlined),
), ),
subtitle: Text( subtitle: Text(
L10n.of(context) L10n.of(
.noticeChatBackupDeviceVerification, context,
).noticeChatBackupDeviceVerification,
), ),
), ),
), ),

View file

@ -9,13 +9,7 @@ import '../../utils/date_time_extension.dart';
import '../../utils/matrix_sdk_extensions/device_extension.dart'; import '../../utils/matrix_sdk_extensions/device_extension.dart';
import '../../widgets/matrix.dart'; import '../../widgets/matrix.dart';
enum UserDeviceListItemAction { enum UserDeviceListItemAction { rename, remove, verify, block, unblock }
rename,
remove,
verify,
block,
unblock,
}
class UserDeviceListItem extends StatelessWidget { class UserDeviceListItem extends StatelessWidget {
final Device userDevice; final Device userDevice;
@ -38,7 +32,8 @@ class UserDeviceListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
final keys = client.userDeviceKeys[Matrix.of(context).client.userID] final keys = client
.userDeviceKeys[Matrix.of(context).client.userID]
?.deviceKeys[userDevice.deviceId]; ?.deviceKeys[userDevice.deviceId];
final isOwnDevice = userDevice.deviceId == client.deviceID; final isOwnDevice = userDevice.deviceId == client.deviceID;
@ -113,10 +108,10 @@ class UserDeviceListItem extends StatelessWidget {
backgroundColor: keys == null backgroundColor: keys == null
? Colors.grey[700] ? Colors.grey[700]
: keys.blocked : keys.blocked
? Colors.red ? Colors.red
: keys.verified : keys.verified
? Colors.green ? Colors.green
: Colors.orange, : Colors.orange,
child: Icon(userDevice.icon), child: Icon(userDevice.icon),
), ),
title: Text( title: Text(
@ -126,8 +121,9 @@ class UserDeviceListItem extends StatelessWidget {
), ),
subtitle: Text( subtitle: Text(
L10n.of(context).lastActiveAgo( L10n.of(context).lastActiveAgo(
DateTime.fromMillisecondsSinceEpoch(userDevice.lastSeenTs ?? 0) DateTime.fromMillisecondsSinceEpoch(
.localizedTimeShort(context), userDevice.lastSeenTs ?? 0,
).localizedTimeShort(context),
), ),
style: const TextStyle(fontWeight: FontWeight.w300), style: const TextStyle(fontWeight: FontWeight.w300),
), ),
@ -137,14 +133,14 @@ class UserDeviceListItem extends StatelessWidget {
keys.blocked keys.blocked
? L10n.of(context).blocked ? L10n.of(context).blocked
: keys.verified : keys.verified
? L10n.of(context).verified ? L10n.of(context).verified
: L10n.of(context).unverified, : L10n.of(context).unverified,
style: TextStyle( style: TextStyle(
color: keys.blocked color: keys.blocked
? Colors.red ? Colors.red
: keys.verified : keys.verified
? Colors.green ? Colors.green
: Colors.orange, : Colors.orange,
), ),
), ),
), ),

View file

@ -70,9 +70,7 @@ class _StreamView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(color: Colors.black54),
color: Colors.black54,
),
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: <Widget>[ children: <Widget>[
@ -133,9 +131,8 @@ class Calling extends StatefulWidget {
class MyCallingPage extends State<Calling> { class MyCallingPage extends State<Calling> {
Room? get room => call.room; Room? get room => call.room;
String get displayName => call.room.getLocalizedDisplayname( String get displayName =>
MatrixLocals(L10n.of(widget.context)), call.room.getLocalizedDisplayname(MatrixLocals(L10n.of(widget.context)));
);
String get callId => widget.callId; String get callId => widget.callId;
@ -219,10 +216,7 @@ class MyCallingPage extends State<Calling> {
} }
void cleanUp() { void cleanUp() {
Timer( Timer(const Duration(seconds: 2), () => widget.onClear?.call());
const Duration(seconds: 2),
() => widget.onClear?.call(),
);
if (call.type == CallType.kVideo) { if (call.type == CallType.kVideo) {
try { try {
unawaited(WakelockPlus.disable()); unawaited(WakelockPlus.disable());
@ -295,8 +289,9 @@ class MyCallingPage extends State<Calling> {
androidNotificationOptions: AndroidNotificationOptions( androidNotificationOptions: AndroidNotificationOptions(
channelId: 'notification_channel_id', channelId: 'notification_channel_id',
channelName: 'Foreground Notification', channelName: 'Foreground Notification',
channelDescription: channelDescription: L10n.of(
L10n.of(widget.context).foregroundServiceRunning, widget.context,
).foregroundServiceRunning,
), ),
iosNotificationOptions: const IOSNotificationOptions(), iosNotificationOptions: const IOSNotificationOptions(),
foregroundTaskOptions: ForegroundTaskOptions( foregroundTaskOptions: ForegroundTaskOptions(
@ -434,9 +429,7 @@ class MyCallingPage extends State<Calling> {
hangupButton, hangupButton,
]; ];
case CallState.kEnded: case CallState.kEnded:
return <Widget>[ return <Widget>[hangupButton];
hangupButton,
];
case CallState.kFledgling: case CallState.kFledgling:
case CallState.kWaitLocalMedia: case CallState.kWaitLocalMedia:
case CallState.kCreateOffer: case CallState.kCreateOffer:
@ -458,28 +451,20 @@ class MyCallingPage extends State<Calling> {
if (call.localHold || call.remoteOnHold) { if (call.localHold || call.remoteOnHold) {
var title = ''; var title = '';
if (call.localHold) { if (call.localHold) {
title = '${call.room.getLocalizedDisplayname( title =
MatrixLocals(L10n.of(widget.context)), '${call.room.getLocalizedDisplayname(MatrixLocals(L10n.of(widget.context)))} held the call.';
)} held the call.';
} else if (call.remoteOnHold) { } else if (call.remoteOnHold) {
title = 'You held the call.'; title = 'You held the call.';
} }
stackWidgets.add( stackWidgets.add(
Center( Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
const Icon( const Icon(Icons.pause, size: 48.0, color: Colors.white),
Icons.pause,
size: 48.0,
color: Colors.white,
),
Text( Text(
title, title,
style: const TextStyle( style: const TextStyle(color: Colors.white, fontSize: 24.0),
color: Colors.white,
fontSize: 24.0,
),
), ),
], ],
), ),
@ -488,7 +473,8 @@ class MyCallingPage extends State<Calling> {
return stackWidgets; return stackWidgets;
} }
var primaryStream = call.remoteScreenSharingStream ?? var primaryStream =
call.remoteScreenSharingStream ??
call.localScreenSharingStream ?? call.localScreenSharingStream ??
call.remoteUserMediaStream ?? call.remoteUserMediaStream ??
call.localUserMediaStream; call.localUserMediaStream;
@ -527,8 +513,10 @@ class MyCallingPage extends State<Calling> {
SizedBox( SizedBox(
width: _localVideoWidth, width: _localVideoWidth,
height: _localVideoHeight, height: _localVideoHeight,
child: child: _StreamView(
_StreamView(remoteUserMediaStream!, matrixClient: widget.client), remoteUserMediaStream!,
matrixClient: widget.client,
),
), ),
); );
secondaryStreamViews.add(const SizedBox(height: 10)); secondaryStreamViews.add(const SizedBox(height: 10));
@ -569,9 +557,7 @@ class MyCallingPage extends State<Calling> {
child: Container( child: Container(
width: _localVideoWidth, width: _localVideoWidth,
margin: _localVideoMargin, margin: _localVideoMargin,
child: Column( child: Column(children: secondaryStreamViews),
children: secondaryStreamViews,
),
), ),
), ),
); );
@ -592,16 +578,14 @@ class MyCallingPage extends State<Calling> {
width: 320.0, width: 320.0,
height: 150.0, height: 150.0,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: .spaceAround,
children: _buildActionButtons(isFloating), children: _buildActionButtons(isFloating),
), ),
), ),
body: OrientationBuilder( body: OrientationBuilder(
builder: (BuildContext context, Orientation orientation) { builder: (BuildContext context, Orientation orientation) {
return Container( return Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(color: Colors.black87),
color: Colors.black87,
),
child: Stack( child: Stack(
children: [ children: [
..._buildContent(orientation, isFloating), ..._buildContent(orientation, isFloating),

View file

@ -9,10 +9,7 @@ class PIPView extends StatefulWidget {
final double? floatingHeight; final double? floatingHeight;
final bool avoidKeyboard; final bool avoidKeyboard;
final Widget Function( final Widget Function(BuildContext context, bool isFloating) builder;
BuildContext context,
bool isFloating,
) builder;
const PIPView({ const PIPView({
super.key, super.key,
@ -95,10 +92,7 @@ class PIPViewState extends State<PIPView> with TickerProviderStateMixin {
void _onPanUpdate(DragUpdateDetails details) { void _onPanUpdate(DragUpdateDetails details) {
if (!_isDragging) return; if (!_isDragging) return;
setState(() { setState(() {
_dragOffset = _dragOffset.translate( _dragOffset = _dragOffset.translate(details.delta.dx, details.delta.dy);
details.delta.dx,
details.delta.dy,
);
}); });
} }
@ -182,9 +176,7 @@ class PIPViewState extends State<PIPView> with TickerProviderStateMixin {
_dragAnimationController, _dragAnimationController,
]), ]),
builder: (context, child) { builder: (context, child) {
final animationCurve = CurveTween( final animationCurve = CurveTween(curve: Curves.easeInOutQuad);
curve: Curves.easeInOutQuad,
);
final dragAnimationValue = animationCurve.transform( final dragAnimationValue = animationCurve.transform(
_dragAnimationController.value, _dragAnimationController.value,
); );
@ -265,21 +257,13 @@ class PIPViewState extends State<PIPView> with TickerProviderStateMixin {
} }
} }
enum PIPViewCorner { enum PIPViewCorner { topLeft, topRight, bottomLeft, bottomRight }
topLeft,
topRight,
bottomLeft,
bottomRight,
}
class _CornerDistance { class _CornerDistance {
final PIPViewCorner corner; final PIPViewCorner corner;
final double distance; final double distance;
_CornerDistance({ _CornerDistance({required this.corner, required this.distance});
required this.corner,
required this.distance,
});
} }
PIPViewCorner _calculateNearestCorner({ PIPViewCorner _calculateNearestCorner({
@ -288,15 +272,9 @@ PIPViewCorner _calculateNearestCorner({
}) { }) {
_CornerDistance calculateDistance(PIPViewCorner corner) { _CornerDistance calculateDistance(PIPViewCorner corner) {
final distance = offsets[corner]! final distance = offsets[corner]!
.translate( .translate(-offset.dx, -offset.dy)
-offset.dx,
-offset.dy,
)
.distanceSquared; .distanceSquared;
return _CornerDistance( return _CornerDistance(corner: corner, distance: distance);
corner: corner,
distance: distance,
);
} }
final distances = PIPViewCorner.values.map(calculateDistance).toList(); final distances = PIPViewCorner.values.map(calculateDistance).toList();

View file

@ -42,8 +42,10 @@ class HomeserverPickerController extends State<HomeserverPicker> {
/// well-known information and forwards to the login page depending on the /// well-known information and forwards to the login page depending on the
/// login type. /// login type.
Future<void> checkHomeserverAction({bool legacyPasswordLogin = false}) async { Future<void> checkHomeserverAction({bool legacyPasswordLogin = false}) async {
final homeserverInput = final homeserverInput = homeserverController.text
homeserverController.text.trim().toLowerCase().replaceAll(' ', '-'); .trim()
.toLowerCase()
.replaceAll(' ', '-');
if (homeserverInput.isEmpty) { if (homeserverInput.isEmpty) {
final client = await Matrix.of(context).getLoginClient(); final client = await Matrix.of(context).getLoginClient();
@ -113,14 +115,12 @@ class HomeserverPickerController extends State<HomeserverPicker> {
void ssoLoginAction() async { void ssoLoginAction() async {
final redirectUrl = kIsWeb final redirectUrl = kIsWeb
? Uri.parse(html.window.location.href) ? Uri.parse(
.resolveUri( html.window.location.href,
Uri(pathSegments: ['auth.html']), ).resolveUri(Uri(pathSegments: ['auth.html'])).toString()
)
.toString()
: isDefaultPlatform : isDefaultPlatform
? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login' ? '${AppConfig.appOpenUrlScheme.toLowerCase()}://login'
: 'http://localhost:3001//login'; : 'http://localhost:3001//login';
final client = await Matrix.of(context).getLoginClient(); final client = await Matrix.of(context).getLoginClient();
final url = client.homeserver!.replace( final url = client.homeserver!.replace(
path: '/_matrix/client/v3/login/sso/redirect', path: '/_matrix/client/v3/login/sso/redirect',

View file

@ -15,18 +15,16 @@ import 'homeserver_picker.dart';
class HomeserverPickerView extends StatelessWidget { class HomeserverPickerView extends StatelessWidget {
final HomeserverPickerController controller; final HomeserverPickerController controller;
const HomeserverPickerView( const HomeserverPickerView(this.controller, {super.key});
this.controller, {
super.key,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return LoginScaffold( return LoginScaffold(
enforceMobileMode: enforceMobileMode: Matrix.of(
Matrix.of(context).widget.clients.any((client) => client.isLogged()), context,
).widget.clients.any((client) => client.isLogged()),
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: Text( title: Text(
@ -42,7 +40,7 @@ class HomeserverPickerView extends StatelessWidget {
PopupMenuItem( PopupMenuItem(
value: MoreLoginActions.importBackup, value: MoreLoginActions.importBackup,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.import_export_outlined), const Icon(Icons.import_export_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -53,7 +51,7 @@ class HomeserverPickerView extends StatelessWidget {
PopupMenuItem( PopupMenuItem(
value: MoreLoginActions.privacy, value: MoreLoginActions.privacy,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.privacy_tip_outlined), const Icon(Icons.privacy_tip_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -64,7 +62,7 @@ class HomeserverPickerView extends StatelessWidget {
PopupMenuItem( PopupMenuItem(
value: MoreLoginActions.about, value: MoreLoginActions.about,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const Icon(Icons.info_outlined), const Icon(Icons.info_outlined),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -100,8 +98,9 @@ class HomeserverPickerView extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 32.0), padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: SelectableLinkify( child: SelectableLinkify(
text: L10n.of(context).appIntroduction, text: L10n.of(context).appIntroduction,
textScaleFactor: textScaleFactor: MediaQuery.textScalerOf(
MediaQuery.textScalerOf(context).scale(1), context,
).scale(1),
textAlign: TextAlign.center, textAlign: TextAlign.center,
linkStyle: TextStyle( linkStyle: TextStyle(
color: theme.colorScheme.secondary, color: theme.colorScheme.secondary,
@ -114,8 +113,8 @@ class HomeserverPickerView extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.all(32.0), padding: const EdgeInsets.all(32.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: .stretch,
children: [ children: [
TextField( TextField(
onSubmitted: (_) => onSubmitted: (_) =>
@ -147,11 +146,13 @@ class HomeserverPickerView extends StatelessWidget {
L10n.of(context).whatIsAHomeserver, L10n.of(context).whatIsAHomeserver,
), ),
content: Linkify( content: Linkify(
text: L10n.of(context) text: L10n.of(
.homeserverDescription, context,
).homeserverDescription,
textScaleFactor: textScaleFactor:
MediaQuery.textScalerOf(context) MediaQuery.textScalerOf(
.scale(1), context,
).scale(1),
options: const LinkifyOptions( options: const LinkifyOptions(
humanize: false, humanize: false,
), ),
@ -169,8 +170,9 @@ class HomeserverPickerView extends StatelessWidget {
Uri.https('servers.joinmatrix.org'), Uri.https('servers.joinmatrix.org'),
), ),
child: Text( child: Text(
L10n.of(context) L10n.of(
.discoverHomeservers, context,
).discoverHomeservers,
), ),
), ),
AdaptiveDialogAction( AdaptiveDialogAction(
@ -206,8 +208,8 @@ class HomeserverPickerView extends StatelessWidget {
onPressed: controller.isLoading onPressed: controller.isLoading
? null ? null
: () => controller.checkHomeserverAction( : () => controller.checkHomeserverAction(
legacyPasswordLogin: true, legacyPasswordLogin: true,
), ),
child: Text(L10n.of(context).loginWithMatrixId), child: Text(L10n.of(context).loginWithMatrixId),
), ),
], ],

View file

@ -32,7 +32,8 @@ class ImageViewerController extends State<ImageViewer> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
allEvents = widget.timeline?.events allEvents =
widget.timeline?.events
.where( .where(
(event) => { (event) => {
MessageTypes.Image, MessageTypes.Image,
@ -44,8 +45,9 @@ class ImageViewerController extends State<ImageViewer> {
.reversed .reversed
.toList() ?? .toList() ??
[widget.event]; [widget.event];
var index = var index = allEvents.indexWhere(
allEvents.indexWhere((event) => event.eventId == widget.event.eventId); (event) => event.eventId == widget.event.eventId,
);
if (index < 0) index = 0; if (index < 0) index = 0;
pageController = PageController(initialPage: index); pageController = PageController(initialPage: index);
} }
@ -93,11 +95,10 @@ class ImageViewerController extends State<ImageViewer> {
/// Forward this image to another room. /// Forward this image to another room.
void forwardAction() => showScaffoldDialog( void forwardAction() => showScaffoldDialog(
context: context, context: context,
builder: (context) => ShareScaffoldDialog( builder: (context) =>
items: [ContentShareItem(currentEvent.content)], ShareScaffoldDialog(items: [ContentShareItem(currentEvent.content)]),
), );
);
/// Save this file with a system call. /// Save this file with a system call.
void saveFileAction(BuildContext context) => currentEvent.saveFile(context); void saveFileAction(BuildContext context) => currentEvent.saveFile(context);

View file

@ -124,7 +124,7 @@ class ImageViewerView extends StatelessWidget {
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
if (controller.canGoBack) if (controller.canGoBack)
Padding( Padding(

View file

@ -19,10 +19,7 @@ import '../../widgets/mxc_image.dart';
class EventVideoPlayer extends StatefulWidget { class EventVideoPlayer extends StatefulWidget {
final Event event; final Event event;
const EventVideoPlayer( const EventVideoPlayer(this.event, {super.key});
this.event, {
super.key,
});
@override @override
EventVideoPlayerState createState() => EventVideoPlayerState(); EventVideoPlayerState createState() => EventVideoPlayerState();
@ -54,8 +51,9 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
: (progress) { : (progress) {
final progressPercentage = progress / fileSize; final progressPercentage = progress / fileSize;
setState(() { setState(() {
_downloadProgress = _downloadProgress = progressPercentage < 1
progressPercentage < 1 ? progressPercentage : null; ? progressPercentage
: null;
}); });
}, },
); );
@ -100,11 +98,9 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
); );
}); });
} on IOException catch (e) { } on IOException catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar( context,
content: Text(e.toLocalizedString(context)), ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
),
);
} catch (e, s) { } catch (e, s) {
ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s); ErrorReporter(context, 'Unable to play video').onErrorCallback(e, s);
} }
@ -136,8 +132,10 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hasThumbnail = widget.event.hasThumbnail; final hasThumbnail = widget.event.hasThumbnail;
final blurHash = (widget.event.infoMap as Map<String, dynamic>) final blurHash =
.tryGet<String>('xyz.amorgan.blurhash') ?? (widget.event.infoMap as Map<String, dynamic>).tryGet<String>(
'xyz.amorgan.blurhash',
) ??
fallbackBlurHash; fallbackBlurHash;
final infoMap = widget.event.content.tryGetMap<String, Object?>('info'); final infoMap = widget.event.content.tryGetMap<String, Object?>('info');
final videoWidth = infoMap?.tryGet<int>('w') ?? 400; final videoWidth = infoMap?.tryGet<int>('w') ?? 400;

View file

@ -12,10 +12,7 @@ import '../../utils/localized_exception_extension.dart';
class InvitationSelection extends StatefulWidget { class InvitationSelection extends StatefulWidget {
final String roomId; final String roomId;
const InvitationSelection({ const InvitationSelection({super.key, required this.roomId});
super.key,
required this.roomId,
});
@override @override
InvitationSelectionController createState() => InvitationSelectionController createState() =>
@ -47,8 +44,8 @@ class InvitationSelectionController extends State<InvitationSelection> {
.toList(); .toList();
contacts.sort( contacts.sort(
(a, b) => a.calcDisplayname().toLowerCase().compareTo( (a, b) => a.calcDisplayname().toLowerCase().compareTo(
b.calcDisplayname().toLowerCase(), b.calcDisplayname().toLowerCase(),
), ),
); );
return contacts; return contacts;
} }
@ -91,9 +88,9 @@ class InvitationSelectionController extends State<InvitationSelection> {
try { try {
response = await matrix.client.searchUserDirectory(text, limit: 10); response = await matrix.client.searchUserDirectory(text, limit: 10);
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text((e).toLocalizedString(context))), context,
); ).showSnackBar(SnackBar(content: Text((e).toLocalizedString(context))));
return; return;
} finally { } finally {
setState(() => loading = false); setState(() => loading = false);

View file

@ -16,13 +16,12 @@ class InvitationSelectionView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final room = final room = Matrix.of(
Matrix.of(context).client.getRoomById(controller.widget.roomId); context,
).client.getRoomById(controller.widget.roomId);
if (room == null) { if (room == null) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: Text(L10n.of(context).oopsSomethingWentWrong)),
title: Text(L10n.of(context).oopsSomethingWentWrong),
),
body: Center( body: Center(
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
), ),
@ -76,11 +75,14 @@ class InvitationSelectionView extends StatelessWidget {
), ),
), ),
StreamBuilder<Object>( StreamBuilder<Object>(
stream: room.client.onRoomState.stream stream: room.client.onRoomState.stream.where(
.where((update) => update.roomId == room.id), (update) => update.roomId == room.id,
),
builder: (context, snapshot) { builder: (context, snapshot) {
final participants = final participants = room
room.getParticipants().map((user) => user.id).toSet(); .getParticipants()
.map((user) => user.id)
.toSet();
return controller.foundProfiles.isNotEmpty return controller.foundProfiles.isNotEmpty
? ListView.builder( ? ListView.builder(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@ -88,17 +90,21 @@ class InvitationSelectionView extends StatelessWidget {
itemCount: controller.foundProfiles.length, itemCount: controller.foundProfiles.length,
itemBuilder: (BuildContext context, int i) => itemBuilder: (BuildContext context, int i) =>
_InviteContactListTile( _InviteContactListTile(
profile: controller.foundProfiles[i], profile: controller.foundProfiles[i],
isMember: participants isMember: participants.contains(
.contains(controller.foundProfiles[i].userId), controller.foundProfiles[i].userId,
onTap: () => controller.inviteAction( ),
context, onTap: () => controller.inviteAction(
controller.foundProfiles[i].userId, context,
controller.foundProfiles[i].displayName ?? controller.foundProfiles[i].userId,
controller.foundProfiles[i].userId.localpart ?? controller.foundProfiles[i].displayName ??
L10n.of(context).user, controller
), .foundProfiles[i]
), .userId
.localpart ??
L10n.of(context).user,
),
),
) )
: FutureBuilder<List<User>>( : FutureBuilder<List<User>>(
future: controller.getContacts(context), future: controller.getContacts(context),
@ -117,23 +123,26 @@ class InvitationSelectionView extends StatelessWidget {
itemCount: contacts.length, itemCount: contacts.length,
itemBuilder: (BuildContext context, int i) => itemBuilder: (BuildContext context, int i) =>
_InviteContactListTile( _InviteContactListTile(
user: contacts[i], user: contacts[i],
profile: Profile( profile: Profile(
avatarUrl: contacts[i].avatarUrl, avatarUrl: contacts[i].avatarUrl,
displayName: contacts[i].displayName ?? displayName:
contacts[i].id.localpart ?? contacts[i].displayName ??
L10n.of(context).user, contacts[i].id.localpart ??
userId: contacts[i].id, L10n.of(context).user,
), userId: contacts[i].id,
isMember: participants.contains(contacts[i].id), ),
onTap: () => controller.inviteAction( isMember: participants.contains(
context, contacts[i].id,
contacts[i].id, ),
contacts[i].displayName ?? onTap: () => controller.inviteAction(
contacts[i].id.localpart ?? context,
L10n.of(context).user, contacts[i].id,
), contacts[i].displayName ??
), contacts[i].id.localpart ??
L10n.of(context).user,
),
),
); );
}, },
); );
@ -169,10 +178,7 @@ class _InviteContactListTile extends StatelessWidget {
mxContent: profile.avatarUrl, mxContent: profile.avatarUrl,
name: profile.displayName, name: profile.displayName,
presenceUserId: profile.userId, presenceUserId: profile.userId,
onTap: () => UserDialog.show( onTap: () => UserDialog.show(context: context, profile: profile),
context: context,
profile: profile,
),
), ),
title: Text( title: Text(
profile.displayName ?? profile.userId.localpart ?? l10n.user, profile.displayName ?? profile.userId.localpart ?? l10n.user,
@ -183,9 +189,7 @@ class _InviteContactListTile extends StatelessWidget {
profile.userId, profile.userId,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(color: theme.colorScheme.secondary),
color: theme.colorScheme.secondary,
),
), ),
trailing: TextButton.icon( trailing: TextButton.icon(
onPressed: isMember ? null : onTap, onPressed: isMember ? null : onTap,

View file

@ -15,17 +15,14 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart';
class KeyVerificationDialog extends StatefulWidget { class KeyVerificationDialog extends StatefulWidget {
Future<bool?> show(BuildContext context) => showAdaptiveDialog<bool>( Future<bool?> show(BuildContext context) => showAdaptiveDialog<bool>(
context: context, context: context,
builder: (context) => this, builder: (context) => this,
barrierDismissible: false, barrierDismissible: false,
); );
final KeyVerification request; final KeyVerification request;
const KeyVerificationDialog({ const KeyVerificationDialog({super.key, required this.request});
super.key,
required this.request,
});
@override @override
KeyVerificationPageState createState() => KeyVerificationPageState(); KeyVerificationPageState createState() => KeyVerificationPageState();
@ -57,8 +54,10 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
void dispose() { void dispose() {
widget.request.onUpdate = widget.request.onUpdate =
originalOnUpdate; // don't want to get updates anymore originalOnUpdate; // don't want to get updates anymore
if (![KeyVerificationState.error, KeyVerificationState.done] if (![
.contains(widget.request.state)) { KeyVerificationState.error,
KeyVerificationState.done,
].contains(widget.request.state)) {
widget.request.cancel('m.user'); widget.request.cancel('m.user');
} }
super.dispose(); super.dispose();
@ -98,8 +97,9 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
final theme = Theme.of(context); final theme = Theme.of(context);
User? user; User? user;
final directChatId = final directChatId = widget.request.client.getDirectChatFromUserId(
widget.request.client.getDirectChatFromUserId(widget.request.userId); widget.request.userId,
);
if (directChatId != null) { if (directChatId != null) {
user = widget.request.client user = widget.request.client
.getRoomById(directChatId)! .getRoomById(directChatId)!
@ -122,7 +122,7 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
body = Container( body = Container(
margin: const EdgeInsets.only(left: 8.0, right: 8.0), margin: const EdgeInsets.only(left: 8.0, right: 8.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: <Widget>[ children: <Widget>[
Text( Text(
L10n.of(context).askSSSSSign, L10n.of(context).askSSSSSign,
@ -152,17 +152,13 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
); );
buttons.add( buttons.add(
AdaptiveDialogAction( AdaptiveDialogAction(
child: Text( child: Text(L10n.of(context).submit),
L10n.of(context).submit,
),
onPressed: () => checkInput(textEditingController.text), onPressed: () => checkInput(textEditingController.text),
), ),
); );
buttons.add( buttons.add(
AdaptiveDialogAction( AdaptiveDialogAction(
child: Text( child: Text(L10n.of(context).skip),
L10n.of(context).skip,
),
onPressed: () => widget.request.openSSSS(skip: true), onPressed: () => widget.request.openSSSS(skip: true),
), ),
); );
@ -170,7 +166,7 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
case KeyVerificationState.askAccept: case KeyVerificationState.askAccept:
title = Text(L10n.of(context).newVerificationRequest); title = Text(L10n.of(context).newVerificationRequest);
body = Column( body = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
const SizedBox(height: 16), const SizedBox(height: 16),
Avatar( Avatar(
@ -179,16 +175,14 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
size: Avatar.defaultSize * 2, size: Avatar.defaultSize * 2,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(L10n.of(context).askVerificationRequest(displayName)),
L10n.of(context).askVerificationRequest(displayName),
),
], ],
); );
buttons.add( buttons.add(
AdaptiveDialogAction( AdaptiveDialogAction(
onPressed: () => widget.request.rejectVerification().then( onPressed: () => widget.request.rejectVerification().then(
(_) => Navigator.of(context, rootNavigator: false).pop(false), (_) => Navigator.of(context, rootNavigator: false).pop(false),
), ),
child: Text( child: Text(
L10n.of(context).reject, L10n.of(context).reject,
style: TextStyle(color: theme.colorScheme.error), style: TextStyle(color: theme.colorScheme.error),
@ -211,10 +205,7 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
Stack( Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
Avatar( Avatar(mxContent: user?.avatarUrl, name: displayName),
mxContent: user?.avatarUrl,
name: displayName,
),
const SizedBox( const SizedBox(
width: Avatar.defaultSize + 2, width: Avatar.defaultSize + 2,
height: Avatar.defaultSize + 2, height: Avatar.defaultSize + 2,
@ -258,16 +249,15 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
title = Text(L10n.of(context).compareNumbersMatch); title = Text(L10n.of(context).compareNumbersMatch);
final numbers = widget.request.sasNumbers; final numbers = widget.request.sasNumbers;
final numbstr = '${numbers[0]}-${numbers[1]}-${numbers[2]}'; final numbstr = '${numbers[0]}-${numbers[1]}-${numbers[2]}';
compareWidget = compareWidget = TextSpan(
TextSpan(text: numbstr, style: const TextStyle(fontSize: 40)); text: numbstr,
style: const TextStyle(fontSize: 40),
);
} }
body = Column( body = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: <Widget>[ children: <Widget>[
Text.rich( Text.rich(compareWidget, textAlign: TextAlign.center),
compareWidget,
textAlign: TextAlign.center,
),
], ],
); );
buttons.add( buttons.add(
@ -291,15 +281,12 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
? L10n.of(context).waitingPartnerEmoji ? L10n.of(context).waitingPartnerEmoji
: L10n.of(context).waitingPartnerNumbers; : L10n.of(context).waitingPartnerNumbers;
body = Column( body = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: <Widget>[ children: <Widget>[
const SizedBox(height: 16), const SizedBox(height: 16),
const CircularProgressIndicator.adaptive(strokeWidth: 2), const CircularProgressIndicator.adaptive(strokeWidth: 2),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(acceptText, textAlign: TextAlign.center),
acceptText,
textAlign: TextAlign.center,
),
], ],
); );
break; break;
@ -315,9 +302,7 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
); );
buttons.add( buttons.add(
AdaptiveDialogAction( AdaptiveDialogAction(
child: Text( child: Text(L10n.of(context).close),
L10n.of(context).close,
),
onPressed: () => onPressed: () =>
Navigator.of(context, rootNavigator: false).pop(true), Navigator.of(context, rootNavigator: false).pop(true),
), ),
@ -326,7 +311,7 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
case KeyVerificationState.error: case KeyVerificationState.error:
title = const Text(''); title = const Text('');
body = Column( body = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: <Widget>[ children: <Widget>[
const SizedBox(height: 16), const SizedBox(height: 16),
Icon(Icons.cancel, color: theme.colorScheme.error, size: 64.0), Icon(Icons.cancel, color: theme.colorScheme.error, size: 64.0),
@ -340,9 +325,7 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
); );
buttons.add( buttons.add(
AdaptiveDialogAction( AdaptiveDialogAction(
child: Text( child: Text(L10n.of(context).close),
L10n.of(context).close,
),
onPressed: () => onPressed: () =>
Navigator.of(context, rootNavigator: false).pop(false), Navigator.of(context, rootNavigator: false).pop(false),
), ),
@ -355,9 +338,7 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
content: SizedBox( content: SizedBox(
height: 256, height: 256,
width: 256, width: 256,
child: ListView( child: ListView(children: [body]),
children: [body],
),
), ),
actions: buttons, actions: buttons,
); );
@ -399,7 +380,7 @@ class _Emoji extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: <Widget>[ children: <Widget>[
Text(emoji.emoji, style: const TextStyle(fontSize: 50)), Text(emoji.emoji, style: const TextStyle(fontSize: 50)),
Padding( Padding(

View file

@ -130,8 +130,9 @@ class LoginController extends State<Login> {
final dialogResult = await showOkCancelAlertDialog( final dialogResult = await showOkCancelAlertDialog(
context: context, context: context,
useRootNavigator: false, useRootNavigator: false,
title: L10n.of(context) title: L10n.of(
.noMatrixServer(newDomain.toString(), oldHomeserver.toString()), context,
).noMatrixServer(newDomain.toString(), oldHomeserver.toString()),
okLabel: L10n.of(context).ok, okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel, cancelLabel: L10n.of(context).cancel,
); );
@ -165,8 +166,9 @@ class LoginController extends State<Login> {
message: L10n.of(context).enterAnEmailAddress, message: L10n.of(context).enterAnEmailAddress,
okLabel: L10n.of(context).ok, okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel, cancelLabel: L10n.of(context).cancel,
initialText: initialText: usernameController.text.isEmail
usernameController.text.isEmail ? usernameController.text : '', ? usernameController.text
: '',
hintText: L10n.of(context).enterAnEmailAddress, hintText: L10n.of(context).enterAnEmailAddress,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
); );
@ -238,8 +240,9 @@ class LoginController extends State<Login> {
} }
extension on String { extension on String {
static final RegExp _phoneRegex = static final RegExp _phoneRegex = RegExp(
RegExp(r'^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$'); r'^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s\./0-9]*$',
);
static final RegExp _emailRegex = RegExp(r'(.+)@(.+)\.(.+)'); static final RegExp _emailRegex = RegExp(r'(.+)@(.+)\.(.+)');
bool get isEmail => _emailRegex.hasMatch(this); bool get isEmail => _emailRegex.hasMatch(this);

View file

@ -21,8 +21,9 @@ class LoginView extends StatelessWidget {
final titleParts = title.split(homeserver); final titleParts = title.split(homeserver);
return LoginScaffold( return LoginScaffold(
enforceMobileMode: enforceMobileMode: Matrix.of(
Matrix.of(context).widget.clients.any((client) => client.isLogged()), context,
).widget.clients.any((client) => client.isLogged()),
appBar: AppBar( appBar: AppBar(
leading: controller.loading ? null : const Center(child: BackButton()), leading: controller.loading ? null : const Center(child: BackButton()),
automaticallyImplyLeading: !controller.loading, automaticallyImplyLeading: !controller.loading,
@ -62,8 +63,9 @@ class LoginView extends StatelessWidget {
controller: controller.usernameController, controller: controller.usernameController,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
autofillHints: autofillHints: controller.loading
controller.loading ? null : [AutofillHints.username], ? null
: [AutofillHints.username],
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.account_box_outlined), prefixIcon: const Icon(Icons.account_box_outlined),
errorText: controller.usernameError, errorText: controller.usernameError,
@ -79,8 +81,9 @@ class LoginView extends StatelessWidget {
child: TextField( child: TextField(
readOnly: controller.loading, readOnly: controller.loading,
autocorrect: false, autocorrect: false,
autofillHints: autofillHints: controller.loading
controller.loading ? null : [AutofillHints.password], ? null
: [AutofillHints.password],
controller: controller.passwordController, controller: controller.passwordController,
textInputAction: TextInputAction.go, textInputAction: TextInputAction.go,
obscureText: !controller.showPassword, obscureText: !controller.showPassword,

View file

@ -13,10 +13,7 @@ import 'package:fluffychat/widgets/matrix.dart';
class NewGroup extends StatefulWidget { class NewGroup extends StatefulWidget {
final CreateGroupType createGroupType; final CreateGroupType createGroupType;
const NewGroup({ const NewGroup({this.createGroupType = CreateGroupType.group, super.key});
this.createGroupType = CreateGroupType.group,
super.key,
});
@override @override
NewGroupController createState() => NewGroupController(); NewGroupController createState() => NewGroupController();
@ -66,8 +63,9 @@ class NewGroupController extends State<NewGroup> {
Future<void> _createGroup() async { Future<void> _createGroup() async {
if (!mounted) return; if (!mounted) return;
final roomId = await Matrix.of(context).client.createGroupChat( final roomId = await Matrix.of(context).client.createGroupChat(
visibility: visibility: groupCanBeFound
groupCanBeFound ? sdk.Visibility.public : sdk.Visibility.private, ? sdk.Visibility.public
: sdk.Visibility.private,
preset: publicGroup preset: publicGroup
? sdk.CreateRoomPreset.publicChat ? sdk.CreateRoomPreset.publicChat
: sdk.CreateRoomPreset.privateChat, : sdk.CreateRoomPreset.privateChat,
@ -87,24 +85,24 @@ class NewGroupController extends State<NewGroup> {
Future<void> _createSpace() async { Future<void> _createSpace() async {
if (!mounted) return; if (!mounted) return;
final spaceId = await Matrix.of(context).client.createRoom( final spaceId = await Matrix.of(context).client.createRoom(
preset: publicGroup preset: publicGroup
? sdk.CreateRoomPreset.publicChat ? sdk.CreateRoomPreset.publicChat
: sdk.CreateRoomPreset.privateChat, : sdk.CreateRoomPreset.privateChat,
creationContent: {'type': RoomCreationTypes.mSpace}, creationContent: {'type': RoomCreationTypes.mSpace},
visibility: publicGroup ? sdk.Visibility.public : null, visibility: publicGroup ? sdk.Visibility.public : null,
roomAliasName: publicGroup roomAliasName: publicGroup
? nameController.text.trim().toLowerCase().replaceAll(' ', '_') ? nameController.text.trim().toLowerCase().replaceAll(' ', '_')
: null, : null,
name: nameController.text.trim(), name: nameController.text.trim(),
powerLevelContentOverride: {'events_default': 100}, powerLevelContentOverride: {'events_default': 100},
initialState: [ initialState: [
if (avatar != null) if (avatar != null)
sdk.StateEvent( sdk.StateEvent(
type: sdk.EventTypes.RoomAvatar, type: sdk.EventTypes.RoomAvatar,
content: {'url': avatarUrl.toString()}, content: {'url': avatarUrl.toString()},
), ),
], ],
); );
if (!mounted) return; if (!mounted) return;
context.pop<String>(spaceId); context.pop<String>(spaceId);
} }

View file

@ -33,7 +33,7 @@ class NewGroupView extends StatelessWidget {
), ),
body: MaxWidthBody( body: MaxWidthBody(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
@ -104,8 +104,9 @@ class NewGroupView extends StatelessWidget {
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,
child: controller.publicGroup child: controller.publicGroup
? SwitchListTile.adaptive( ? SwitchListTile.adaptive(
contentPadding: contentPadding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 32), horizontal: 32,
),
secondary: const Icon(Icons.search_outlined), secondary: const Icon(Icons.search_outlined),
title: Text(L10n.of(context).groupCanBeFoundViaSearch), title: Text(L10n.of(context).groupCanBeFoundViaSearch),
value: controller.groupCanBeFound, value: controller.groupCanBeFound,
@ -121,17 +122,16 @@ class NewGroupView extends StatelessWidget {
child: controller.createGroupType == CreateGroupType.space child: controller.createGroupType == CreateGroupType.space
? const SizedBox.shrink() ? const SizedBox.shrink()
: SwitchListTile.adaptive( : SwitchListTile.adaptive(
contentPadding: contentPadding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 32), horizontal: 32,
),
secondary: Icon( secondary: Icon(
Icons.lock_outlined, Icons.lock_outlined,
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),
title: Text( title: Text(
L10n.of(context).enableEncryption, L10n.of(context).enableEncryption,
style: TextStyle( style: TextStyle(color: theme.colorScheme.onSurface),
color: theme.colorScheme.onSurface,
),
), ),
value: !controller.publicGroup, value: !controller.publicGroup,
onChanged: null, onChanged: null,
@ -142,8 +142,9 @@ class NewGroupView extends StatelessWidget {
curve: FluffyThemes.animationCurve, curve: FluffyThemes.animationCurve,
child: controller.createGroupType == CreateGroupType.space child: controller.createGroupType == CreateGroupType.space
? ListTile( ? ListTile(
contentPadding: contentPadding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 32), horizontal: 32,
),
trailing: const Padding( trailing: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0), padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Icon(Icons.info_outlined), child: Icon(Icons.info_outlined),
@ -157,8 +158,9 @@ class NewGroupView extends StatelessWidget {
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: onPressed: controller.loading
controller.loading ? null : controller.submitAction, ? null
: controller.submitAction,
child: controller.loading child: controller.loading
? const LinearProgressIndicator() ? const LinearProgressIndicator()
: Text( : Text(
@ -181,9 +183,7 @@ class NewGroupView extends StatelessWidget {
), ),
title: Text( title: Text(
error.toLocalizedString(context), error.toLocalizedString(context),
style: TextStyle( style: TextStyle(color: theme.colorScheme.error),
color: theme.colorScheme.error,
),
), ),
), ),
), ),

View file

@ -52,8 +52,9 @@ class NewPrivateChatController extends State<NewPrivateChat> {
} }
Future<List<Profile>> _searchUser(String searchTerm) async { Future<List<Profile>> _searchUser(String searchTerm) async {
final result = final result = await Matrix.of(
await Matrix.of(context).client.searchUserDirectory(searchTerm); context,
).client.searchUserDirectory(searchTerm);
final profiles = result.results; final profiles = result.results;
if (searchTerm.isValidMatrixId && if (searchTerm.isValidMatrixId &&
@ -73,9 +74,7 @@ class NewPrivateChatController extends State<NewPrivateChat> {
if (info.version.sdkInt < 21) { if (info.version.sdkInt < 21) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(L10n.of(context).unsupportedAndroidVersionLong),
L10n.of(context).unsupportedAndroidVersionLong,
),
), ),
); );
return; return;
@ -93,15 +92,13 @@ class NewPrivateChatController extends State<NewPrivateChat> {
await Clipboard.setData( await Clipboard.setData(
ClipboardData(text: Matrix.of(context).client.userID!), ClipboardData(text: Matrix.of(context).client.userID!),
); );
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(L10n.of(context).copiedToClipboard)), context,
); ).showSnackBar(SnackBar(content: Text(L10n.of(context).copiedToClipboard)));
} }
void openUserModal(Profile profile) => UserDialog.show( void openUserModal(Profile profile) =>
context: context, UserDialog.show(context: context, profile: profile);
profile: profile,
);
@override @override
Widget build(BuildContext context) => NewPrivateChatView(this); Widget build(BuildContext context) => NewPrivateChatView(this);

View file

@ -35,8 +35,10 @@ class NewPrivateChatView extends StatelessWidget {
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed: UrlLauncher(
UrlLauncher(context, AppConfig.startChatTutorial).launchUrl, context,
AppConfig.startChatTutorial,
).launchUrl,
child: Text(L10n.of(context).help), child: Text(L10n.of(context).help),
), ),
], ],
@ -105,8 +107,9 @@ class NewPrivateChatView extends StatelessWidget {
? ListView( ? ListView(
children: [ children: [
Padding( Padding(
padding: padding: const EdgeInsets.symmetric(
const EdgeInsets.symmetric(horizontal: 18.0), horizontal: 18.0,
),
child: SelectableText.rich( child: SelectableText.rich(
TextSpan( TextSpan(
children: [ children: [
@ -157,8 +160,9 @@ class NewPrivateChatView extends StatelessWidget {
theme.colorScheme.primaryContainer, theme.colorScheme.primaryContainer,
foregroundColor: foregroundColor:
theme.colorScheme.onPrimaryContainer, theme.colorScheme.onPrimaryContainer,
child: child: const Icon(
const Icon(Icons.qr_code_scanner_outlined), Icons.qr_code_scanner_outlined,
),
), ),
title: Text(L10n.of(context).scanQrCode), title: Text(L10n.of(context).scanQrCode),
onTap: controller.openScannerAction, onTap: controller.openScannerAction,
@ -185,15 +189,14 @@ class NewPrivateChatView extends StatelessWidget {
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
AppConfig.borderRadius, AppConfig.borderRadius,
), ),
onTap: () => showQrCodeViewer( onTap: () =>
context, showQrCodeViewer(context, userId),
userId,
),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: ConstrainedBox( child: ConstrainedBox(
constraints: constraints: const BoxConstraints(
const BoxConstraints(maxWidth: 200), maxWidth: 200,
),
child: PrettyQrView.data( child: PrettyQrView.data(
data: 'https://matrix.to/#/$userId', data: 'https://matrix.to/#/$userId',
decoration: PrettyQrDecoration( decoration: PrettyQrDecoration(
@ -218,7 +221,7 @@ class NewPrivateChatView extends StatelessWidget {
final error = snapshot.error; final error = snapshot.error;
if (error != null) { if (error != null) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
Text( Text(
error.toLocalizedString(context), error.toLocalizedString(context),
@ -243,7 +246,7 @@ class NewPrivateChatView extends StatelessWidget {
} }
if (result.isEmpty) { if (result.isEmpty) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
const Icon(Icons.search_outlined, size: 86), const Icon(Icons.search_outlined, size: 86),
Padding( Padding(
@ -265,7 +268,8 @@ class NewPrivateChatView extends StatelessWidget {
itemCount: result.length, itemCount: result.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {
final contact = result[i]; final contact = result[i];
final displayname = contact.displayName ?? final displayname =
contact.displayName ??
contact.userId.localpart ?? contact.userId.localpart ??
contact.userId; contact.userId;
return ListTile( return ListTile(

View file

@ -29,9 +29,9 @@ class SettingsController extends State<Settings> {
bool profileUpdated = false; bool profileUpdated = false;
void updateProfile() => setState(() { void updateProfile() => setState(() {
profileUpdated = true; profileUpdated = true;
profileFuture = null; profileFuture = null;
}); });
void setDisplaynameAction() async { void setDisplaynameAction() async {
final profile = await profileFuture; final profile = await profileFuture;
@ -132,15 +132,9 @@ class SettingsController extends State<Settings> {
imageQuality: 50, imageQuality: 50,
); );
if (result == null) return; if (result == null) return;
file = MatrixFile( file = MatrixFile(bytes: await result.readAsBytes(), name: result.path);
bytes: await result.readAsBytes(),
name: result.path,
);
} else { } else {
final result = await selectFiles( final result = await selectFiles(context, type: FileSelectorType.images);
context,
type: FileSelectorType.images,
);
final pickedFile = result.firstOrNull; final pickedFile = result.firstOrNull;
if (pickedFile == null) return; if (pickedFile == null) return;
file = MatrixFile( file = MatrixFile(
@ -176,8 +170,8 @@ class SettingsController extends State<Settings> {
await client.encryption?.crossSigning.isCached() ?? false; await client.encryption?.crossSigning.isCached() ?? false;
final needsBootstrap = final needsBootstrap =
await client.encryption?.keyManager.isCached() == false || await client.encryption?.keyManager.isCached() == false ||
client.encryption?.crossSigning.enabled == false || client.encryption?.crossSigning.enabled == false ||
crossSigning == false; crossSigning == false;
final isUnknownSession = client.isUnknownSession; final isUnknownSession = client.isUnknownSession;
setState(() { setState(() {
showChatBackupBanner = needsBootstrap || isUnknownSession; showChatBackupBanner = needsBootstrap || isUnknownSession;
@ -204,9 +198,7 @@ class SettingsController extends State<Settings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
profileFuture ??= client.getProfileFromUserId( profileFuture ??= client.getProfileFromUserId(client.userID!);
client.userID!,
);
return SettingsView(this); return SettingsView(this);
} }
} }

View file

@ -25,8 +25,9 @@ class SettingsView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final showChatBackupBanner = controller.showChatBackupBanner; final showChatBackupBanner = controller.showChatBackupBanner;
final activeRoute = final activeRoute = GoRouter.of(
GoRouter.of(context).routeInformationProvider.value.uri.path; context,
).routeInformationProvider.value.uri.path;
final accountManageUrl = Matrix.of(context) final accountManageUrl = Matrix.of(context)
.client .client
.wellKnown .wellKnown
@ -41,10 +42,7 @@ class SettingsView extends StatelessWidget {
onGoToChats: () => context.go('/rooms'), onGoToChats: () => context.go('/rooms'),
onGoToSpaceId: (spaceId) => context.go('/rooms?spaceId=$spaceId'), onGoToSpaceId: (spaceId) => context.go('/rooms?spaceId=$spaceId'),
), ),
Container( Container(color: Theme.of(context).dividerColor, width: 1),
color: Theme.of(context).dividerColor,
width: 1,
),
], ],
Expanded( Expanded(
child: Scaffold( child: Scaffold(
@ -53,9 +51,7 @@ class SettingsView extends StatelessWidget {
: AppBar( : AppBar(
title: Text(L10n.of(context).settings), title: Text(L10n.of(context).settings),
leading: Center( leading: Center(
child: BackButton( child: BackButton(onPressed: () => context.go('/rooms')),
onPressed: () => context.go('/rooms'),
),
), ),
), ),
body: ListTileTheme( body: ListTileTheme(
@ -68,7 +64,8 @@ class SettingsView extends StatelessWidget {
builder: (context, snapshot) { builder: (context, snapshot) {
final profile = snapshot.data; final profile = snapshot.data;
final avatar = profile?.avatarUrl; final avatar = profile?.avatarUrl;
final mxid = Matrix.of(context).client.userID ?? final mxid =
Matrix.of(context).client.userID ??
L10n.of(context).user; L10n.of(context).user;
final displayname = final displayname =
profile?.displayName ?? mxid.localpart ?? mxid; profile?.displayName ?? mxid.localpart ?? mxid;
@ -84,10 +81,10 @@ class SettingsView extends StatelessWidget {
size: Avatar.defaultSize * 2.5, size: Avatar.defaultSize * 2.5,
onTap: avatar != null onTap: avatar != null
? () => showDialog( ? () => showDialog(
context: context, context: context,
builder: (_) => builder: (_) =>
MxcImageViewer(avatar), MxcImageViewer(avatar),
) )
: null, : null,
), ),
if (profile != null) if (profile != null)
@ -108,8 +105,8 @@ class SettingsView extends StatelessWidget {
), ),
Expanded( Expanded(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: .start,
children: [ children: [
TextButton.icon( TextButton.icon(
onPressed: controller.setDisplaynameAction, onPressed: controller.setDisplaynameAction,
@ -126,9 +123,7 @@ class SettingsView extends StatelessWidget {
displayname, displayname,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(fontSize: 18),
fontSize: 18,
),
), ),
), ),
TextButton.icon( TextButton.icon(
@ -182,9 +177,7 @@ class SettingsView extends StatelessWidget {
title: Text(L10n.of(context).chatBackup), title: Text(L10n.of(context).chatBackup),
onChanged: controller.firstRunBootstrapAction, onChanged: controller.firstRunBootstrapAction,
), ),
Divider( Divider(color: theme.dividerColor),
color: theme.dividerColor,
),
ListTile( ListTile(
leading: const Icon(Icons.format_paint_outlined), leading: const Icon(Icons.format_paint_outlined),
title: Text(L10n.of(context).changeTheme), title: Text(L10n.of(context).changeTheme),
@ -198,8 +191,8 @@ class SettingsView extends StatelessWidget {
title: Text(L10n.of(context).notifications), title: Text(L10n.of(context).notifications),
tileColor: tileColor:
activeRoute.startsWith('/rooms/settings/notifications') activeRoute.startsWith('/rooms/settings/notifications')
? theme.colorScheme.surfaceContainerHigh ? theme.colorScheme.surfaceContainerHigh
: null, : null,
onTap: () => context.go('/rooms/settings/notifications'), onTap: () => context.go('/rooms/settings/notifications'),
), ),
ListTile( ListTile(
@ -224,8 +217,8 @@ class SettingsView extends StatelessWidget {
onTap: () => context.go('/rooms/settings/security'), onTap: () => context.go('/rooms/settings/security'),
tileColor: tileColor:
activeRoute.startsWith('/rooms/settings/security') activeRoute.startsWith('/rooms/settings/security')
? theme.colorScheme.surfaceContainerHigh ? theme.colorScheme.surfaceContainerHigh
: null, : null,
), ),
Divider(color: theme.dividerColor), Divider(color: theme.dividerColor),
ListTile( ListTile(
@ -239,8 +232,8 @@ class SettingsView extends StatelessWidget {
onTap: () => context.go('/rooms/settings/homeserver'), onTap: () => context.go('/rooms/settings/homeserver'),
tileColor: tileColor:
activeRoute.startsWith('/rooms/settings/homeserver') activeRoute.startsWith('/rooms/settings/homeserver')
? theme.colorScheme.surfaceContainerHigh ? theme.colorScheme.surfaceContainerHigh
: null, : null,
), ),
ListTile( ListTile(
leading: const Icon(Icons.privacy_tip_outlined), leading: const Icon(Icons.privacy_tip_outlined),

View file

@ -34,10 +34,10 @@ class Settings3PidController extends State<Settings3Pid> {
final response = await showFutureLoadingDialog( final response = await showFutureLoadingDialog(
context: context, context: context,
future: () => Matrix.of(context).client.requestTokenToRegisterEmail( future: () => Matrix.of(context).client.requestTokenToRegisterEmail(
clientSecret, clientSecret,
input, input,
Settings3Pid.sendAttempt++, Settings3Pid.sendAttempt++,
), ),
); );
if (response.error != null) return; if (response.error != null) return;
final ok = await showOkAlertDialog( final ok = await showOkAlertDialog(
@ -52,12 +52,10 @@ class Settings3PidController extends State<Settings3Pid> {
context: context, context: context,
delay: false, delay: false,
future: () => Matrix.of(context).client.uiaRequestBackground( future: () => Matrix.of(context).client.uiaRequestBackground(
(auth) => Matrix.of(context).client.add3PID( (auth) => Matrix.of(
clientSecret, context,
response.result!.sid, ).client.add3PID(clientSecret, response.result!.sid, auth: auth),
auth: auth, ),
),
),
); );
if (success.error != null) return; if (success.error != null) return;
setState(() => request = null); setState(() => request = null);
@ -78,10 +76,9 @@ class Settings3PidController extends State<Settings3Pid> {
} }
final success = await showFutureLoadingDialog( final success = await showFutureLoadingDialog(
context: context, context: context,
future: () => Matrix.of(context).client.delete3pidFromAccount( future: () => Matrix.of(
identifier.address, context,
identifier.medium, ).client.delete3pidFromAccount(identifier.address, identifier.medium),
),
); );
if (success.error != null) return; if (success.error != null) return;
setState(() => request = null); setState(() => request = null);

View file

@ -33,67 +33,71 @@ class Settings3PidView extends StatelessWidget {
withScrolling: false, withScrolling: false,
child: FutureBuilder<List<ThirdPartyIdentifier>?>( child: FutureBuilder<List<ThirdPartyIdentifier>?>(
future: controller.request, future: controller.request,
builder: ( builder:
BuildContext context, (
AsyncSnapshot<List<ThirdPartyIdentifier>?> snapshot, BuildContext context,
) { AsyncSnapshot<List<ThirdPartyIdentifier>?> snapshot,
if (snapshot.hasError) { ) {
return Center( if (snapshot.hasError) {
child: Text( return Center(
snapshot.error.toString(), child: Text(
textAlign: TextAlign.center, snapshot.error.toString(),
), textAlign: TextAlign.center,
);
}
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
);
}
final identifier = snapshot.data!;
return Column(
children: [
ListTile(
leading: CircleAvatar(
backgroundColor: theme.scaffoldBackgroundColor,
foregroundColor:
identifier.isEmpty ? Colors.orange : Colors.grey,
child: Icon(
identifier.isEmpty
? Icons.warning_outlined
: Icons.info_outlined,
), ),
), );
title: Text( }
identifier.isEmpty if (!snapshot.hasData) {
? L10n.of(context).noPasswordRecoveryDescription return const Center(
: L10n.of(context) child: CircularProgressIndicator.adaptive(strokeWidth: 2),
.withTheseAddressesRecoveryDescription, );
), }
), final identifier = snapshot.data!;
const Divider(), return Column(
Expanded( children: [
child: ListView.builder( ListTile(
itemCount: identifier.length,
itemBuilder: (BuildContext context, int i) => ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
foregroundColor: Colors.grey, foregroundColor: identifier.isEmpty
child: Icon(identifier[i].iconData), ? Colors.orange
: Colors.grey,
child: Icon(
identifier.isEmpty
? Icons.warning_outlined
: Icons.info_outlined,
),
), ),
title: Text(identifier[i].address), title: Text(
trailing: IconButton( identifier.isEmpty
tooltip: L10n.of(context).delete, ? L10n.of(context).noPasswordRecoveryDescription
icon: const Icon(Icons.delete_forever_outlined), : L10n.of(
color: Colors.red, context,
onPressed: () => controller.delete3Pid(identifier[i]), ).withTheseAddressesRecoveryDescription,
), ),
), ),
), const Divider(),
), Expanded(
], child: ListView.builder(
); itemCount: identifier.length,
}, itemBuilder: (BuildContext context, int i) => ListTile(
leading: CircleAvatar(
backgroundColor: theme.scaffoldBackgroundColor,
foregroundColor: Colors.grey,
child: Icon(identifier[i].iconData),
),
title: Text(identifier[i].address),
trailing: IconButton(
tooltip: L10n.of(context).delete,
icon: const Icon(Icons.delete_forever_outlined),
color: Colors.red,
onPressed: () =>
controller.delete3Pid(identifier[i]),
),
),
),
),
],
);
},
), ),
), ),
); );

View file

@ -46,11 +46,7 @@ class _ImportEmoteArchiveDialogState extends State<ImportEmoteArchiveDialog> {
return AlertDialog( return AlertDialog(
title: Text(L10n.of(context).importEmojis), title: Text(L10n.of(context).importEmojis),
content: _loading content: _loading
? Center( ? Center(child: CircularProgressIndicator(value: _progress))
child: CircularProgressIndicator(
value: _progress,
),
)
: SingleChildScrollView( : SingleChildScrollView(
child: Wrap( child: Wrap(
alignment: WrapAlignment.spaceEvenly, alignment: WrapAlignment.spaceEvenly,
@ -79,8 +75,8 @@ class _ImportEmoteArchiveDialogState extends State<ImportEmoteArchiveDialog> {
onPressed: _loading onPressed: _loading
? null ? null
: _importMap.isNotEmpty : _importMap.isNotEmpty
? _addEmotePack ? _addEmotePack
: null, : null,
child: Text(L10n.of(context).importNow), child: Text(L10n.of(context).importNow),
), ),
], ],
@ -91,12 +87,8 @@ class _ImportEmoteArchiveDialogState extends State<ImportEmoteArchiveDialog> {
_importMap = Map.fromEntries( _importMap = Map.fromEntries(
widget.archive.files widget.archive.files
.where((e) => e.isFile) .where((e) => e.isFile)
.map( .map((e) => MapEntry(e, e.name.emoteNameFromPath))
(e) => MapEntry(e, e.name.emoteNameFromPath), .sorted((a, b) => a.value.compareTo(b.value)),
)
.sorted(
(a, b) => a.value.compareTo(b.value),
),
); );
} }
@ -148,10 +140,7 @@ class _ImportEmoteArchiveDialogState extends State<ImportEmoteArchiveDialog> {
final imageCode = entry.value; final imageCode = entry.value;
try { try {
var mxcFile = MatrixImageFile( var mxcFile = MatrixImageFile(bytes: file.content, name: file.name);
bytes: file.content,
name: file.name,
);
final thumbnail = (await mxcFile.generateThumbnail( final thumbnail = (await mxcFile.generateThumbnail(
nativeImplementations: ClientManager.nativeImplementations, nativeImplementations: ClientManager.nativeImplementations,
@ -162,14 +151,12 @@ class _ImportEmoteArchiveDialogState extends State<ImportEmoteArchiveDialog> {
mxcFile = thumbnail; mxcFile = thumbnail;
} }
final uri = await Matrix.of(context).client.uploadContent( final uri = await Matrix.of(context).client.uploadContent(
mxcFile.bytes, mxcFile.bytes,
filename: mxcFile.name, filename: mxcFile.name,
contentType: mxcFile.mimeType, contentType: mxcFile.mimeType,
); );
final info = <String, dynamic>{ final info = <String, dynamic>{...mxcFile.info};
...mxcFile.info,
};
// normalize width / height to 256, required for stickers // normalize width / height to 256, required for stickers
if (info['w'] is int && info['h'] is int) { if (info['w'] is int && info['h'] is int) {
@ -184,9 +171,9 @@ class _ImportEmoteArchiveDialogState extends State<ImportEmoteArchiveDialog> {
} }
widget.controller.pack!.images[imageCode] = widget.controller.pack!.images[imageCode] =
ImagePackImageContent.fromJson(<String, dynamic>{ ImagePackImageContent.fromJson(<String, dynamic>{
'url': uri.toString(), 'url': uri.toString(),
'info': info, 'info': info,
}); });
successfulUploads.add(file.name); successfulUploads.add(file.name);
} catch (e) { } catch (e) {
Logs().d('Could not upload emote $imageCode'); Logs().d('Could not upload emote $imageCode');
@ -204,8 +191,9 @@ class _ImportEmoteArchiveDialogState extends State<ImportEmoteArchiveDialog> {
// in case we have unhandled / duplicated emotes left, don't pop // in case we have unhandled / duplicated emotes left, don't pop
if (mounted) setState(() {}); if (mounted) setState(() {});
if (_importMap.isEmpty) { if (_importMap.isEmpty) {
WidgetsBinding.instance WidgetsBinding.instance.addPostFrameCallback(
.addPostFrameCallback((_) => Navigator.of(context).pop()); (_) => Navigator.of(context).pop(),
);
} }
} }
} }
@ -250,21 +238,20 @@ class _EmojiImportPreviewState extends State<_EmojiImportPreview> {
if (hasError) return _ImageFileError(name: widget.entry.key.name); if (hasError) return _ImageFileError(name: widget.entry.key.name);
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: .center,
children: [ children: [
Image.memory( Image.memory(
widget.entry.key.content, widget.entry.key.content,
height: 64, height: 64,
width: 64, width: 64,
errorBuilder: (context, e, s) { errorBuilder: (context, e, s) {
WidgetsBinding.instance WidgetsBinding.instance.addPostFrameCallback(
.addPostFrameCallback((_) => _setRenderError()); (_) => _setRenderError(),
return _ImageFileError(
name: widget.entry.key.name,
); );
return _ImageFileError(name: widget.entry.key.name);
}, },
), ),
SizedBox( SizedBox(
@ -323,9 +310,9 @@ class _ImageFileError extends StatelessWidget {
child: Tooltip( child: Tooltip(
message: name, message: name,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: .start,
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: .center,
children: [ children: [
const Icon(Icons.error), const Icon(Icons.error),
Text( Text(
@ -347,8 +334,7 @@ extension on String {
/// Used to compute emote name proposal based on file name /// Used to compute emote name proposal based on file name
String get emoteNameFromPath { String get emoteNameFromPath {
// ... removing leading path // ... removing leading path
return split(RegExp(r'[/\\]')) return split(RegExp(r'[/\\]')).last
.last
// ... removing file extension // ... removing file extension
.split('.') .split('.')
.first .first

View file

@ -55,10 +55,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
final event = key == null final event = key == null
? null ? null
: room?.getState( : room?.getState('im.ponies.room_emotes', key);
'im.ponies.room_emotes',
key,
);
final eventPack = event?.content.tryGetMap<String, Object?>('pack'); final eventPack = event?.content.tryGetMap<String, Object?>('pack');
packDisplayNameController.text = packDisplayNameController.text =
eventPack?.tryGet<String>('display_name') ?? ''; eventPack?.tryGet<String>('display_name') ?? '';
@ -71,13 +68,11 @@ class EmotesSettingsController extends State<EmotesSettings> {
ImagePackContent _getPack() { ImagePackContent _getPack() {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
final event = (room != null final event =
(room != null
? room!.getState('im.ponies.room_emotes', stateKey ?? '') ? room!.getState('im.ponies.room_emotes', stateKey ?? '')
: client.accountData['im.ponies.user_emotes']) ?? : client.accountData['im.ponies.user_emotes']) ??
BasicEvent( BasicEvent(type: 'm.dummy', content: {});
type: 'm.dummy',
content: {},
);
// make sure we work on a *copy* of the event // make sure we work on a *copy* of the event
return BasicEvent.fromJson(event.toJson()).parsedImagePackContent; return BasicEvent.fromJson(event.toJson()).parsedImagePackContent;
} }
@ -124,7 +119,8 @@ class EmotesSettingsController extends State<EmotesSettings> {
return; return;
} }
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
final content = client.accountData['im.ponies.emote_rooms']?.content ?? final content =
client.accountData['im.ponies.emote_rooms']?.content ??
<String, dynamic>{}; <String, dynamic>{};
if (active) { if (active) {
if (content['rooms'] is! Map) { if (content['rooms'] is! Map) {
@ -158,14 +154,15 @@ class EmotesSettingsController extends State<EmotesSettings> {
TextEditingController(); TextEditingController();
void removeImageAction(String oldImageCode) => setState(() { void removeImageAction(String oldImageCode) => setState(() {
pack!.images.remove(oldImageCode); pack!.images.remove(oldImageCode);
showSave = true; showSave = true;
}); });
void toggleUsage(String imageCode, ImagePackUsage usage) { void toggleUsage(String imageCode, ImagePackUsage usage) {
setState(() { setState(() {
final usages = final usages = pack!.images[imageCode]!.usage ??= List.from(
pack!.images[imageCode]!.usage ??= List.from(ImagePackUsage.values); ImagePackUsage.values,
);
if (!usages.remove(usage)) usages.add(usage); if (!usages.remove(usage)) usages.add(usage);
showSave = true; showSave = true;
}); });
@ -265,9 +262,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
if (packKeys?.contains(name) ?? false) { if (packKeys?.contains(name) ?? false) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(content: Text(L10n.of(context).stickerPackNameAlreadyExists)),
content: Text(L10n.of(context).stickerPackNameAlreadyExists),
),
); );
return; return;
} }
@ -316,20 +311,19 @@ class EmotesSettingsController extends State<EmotesSettings> {
bytes: await pickedFile.readAsBytes(), bytes: await pickedFile.readAsBytes(),
name: pickedFile.name, name: pickedFile.name,
); );
file = await file.generateThumbnail( file =
await file.generateThumbnail(
nativeImplementations: ClientManager.nativeImplementations, nativeImplementations: ClientManager.nativeImplementations,
) ?? ) ??
file; file;
final uri = await Matrix.of(context).client.uploadContent( final uri = await Matrix.of(context).client.uploadContent(
file.bytes, file.bytes,
filename: file.name, filename: file.name,
contentType: file.mimeType, contentType: file.mimeType,
); );
setState(() { setState(() {
final info = <String, dynamic>{ final info = <String, dynamic>{...file.info};
...file.info,
};
// normalize width / height to 256, required for stickers // normalize width / height to 256, required for stickers
if (info['w'] is int && info['h'] is int) { if (info['w'] is int && info['h'] is int) {
final ratio = info['w'] / info['h']; final ratio = info['w'] / info['h'];
@ -342,11 +336,9 @@ class EmotesSettingsController extends State<EmotesSettings> {
} }
} }
final imageCode = pickedFile.name.split('.').first; final imageCode = pickedFile.name.split('.').first;
pack!.images[imageCode] = pack!.images[imageCode] = ImagePackImageContent.fromJson(
ImagePackImageContent.fromJson(<String, dynamic>{ <String, dynamic>{'url': uri.toString(), 'info': info},
'url': uri.toString(), );
'info': info,
});
}); });
} }
}, },
@ -363,10 +355,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
} }
Future<void> importEmojiZip() async { Future<void> importEmojiZip() async {
final result = await selectFiles( final result = await selectFiles(context, type: FileSelectorType.zip);
context,
type: FileSelectorType.zip,
);
if (result.isEmpty) return; if (result.isEmpty) return;
@ -378,10 +367,8 @@ class EmotesSettingsController extends State<EmotesSettings> {
context: context, context: context,
// breaks [Matrix.of] calls otherwise // breaks [Matrix.of] calls otherwise
useRootNavigator: false, useRootNavigator: false,
builder: (context) => ImportEmoteArchiveDialog( builder: (context) =>
controller: this, ImportEmoteArchiveDialog(controller: this, archive: archive),
archive: archive,
),
); );
setState(() {}); setState(() {});
} }
@ -404,11 +391,7 @@ class EmotesSettingsController extends State<EmotesSettings> {
); );
archive.addFile( archive.addFile(
ArchiveFile( ArchiveFile(name, response.bodyBytes.length, response.bodyBytes),
name,
response.bodyBytes.length,
response.bodyBytes,
),
); );
} }
final fileName = final fileName =

View file

@ -23,9 +23,7 @@ class EmotesSettingsView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (controller.widget.roomId != null && controller.room == null) { if (controller.widget.roomId != null && controller.room == null) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: Text(L10n.of(context).oopsSomethingWentWrong)),
title: Text(L10n.of(context).oopsSomethingWentWrong),
),
body: Center( body: Center(
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat), child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
), ),
@ -110,10 +108,7 @@ class EmotesSettingsView extends StatelessWidget {
horizontal: 4.0, horizontal: 4.0,
), ),
child: FilterChip( child: FilterChip(
label: const Icon( label: const Icon(Icons.add_outlined, size: 20),
Icons.add_outlined,
size: 20,
),
onSelected: controller.showSave onSelected: controller.showSave
? null ? null
: (_) => controller.createImagePack(), : (_) => controller.createImagePack(),
@ -122,23 +117,24 @@ class EmotesSettingsView extends StatelessWidget {
} }
i--; i--;
final key = packKeys[i]; final key = packKeys[i];
final event = controller.room final event = controller.room?.getState(
?.getState('im.ponies.room_emotes', packKeys[i]); 'im.ponies.room_emotes',
packKeys[i],
);
final eventPack = final eventPack = event?.content
event?.content.tryGetMap<String, Object?>('pack'); .tryGetMap<String, Object?>('pack');
final packName = final packName =
eventPack?.tryGet<String>('display_name') ?? eventPack?.tryGet<String>('display_name') ??
eventPack?.tryGet<String>('name') ?? eventPack?.tryGet<String>('name') ??
(key.isNotEmpty ? key : 'Default'); (key.isNotEmpty ? key : 'Default');
return Padding( return Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(horizontal: 4.0),
horizontal: 4.0,
),
child: FilterChip( child: FilterChip(
label: Text(packName), label: Text(packName),
selected: controller.stateKey == key || selected:
controller.stateKey == key ||
(controller.stateKey == null && key.isEmpty), (controller.stateKey == null && key.isEmpty),
onSelected: controller.showSave onSelected: controller.showSave
? null ? null
@ -153,8 +149,8 @@ class EmotesSettingsView extends StatelessWidget {
), ),
body: MaxWidthBody( body: MaxWidthBody(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: .stretch,
children: <Widget>[ children: <Widget>[
if (controller.room != null) ...[ if (controller.room != null) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
@ -188,9 +184,10 @@ class EmotesSettingsView extends StatelessWidget {
? null ? null
: IconButton( : IconButton(
icon: const Icon(Icons.link_outlined), icon: const Icon(Icons.link_outlined),
onPressed: () => onPressed: () => UrlLauncher(
UrlLauncher(context, attributionUrl.toString()) context,
.launchUrl(), attributionUrl.toString(),
).launchUrl(),
), ),
), ),
), ),
@ -283,25 +280,23 @@ class EmotesSettingsView extends StatelessWidget {
), ),
onSubmitted: (s) => onSubmitted: (s) =>
controller.submitImageAction( controller.submitImageAction(
imageCode, imageCode,
image, image,
textEditingController, textEditingController,
), ),
), ),
), ),
), ),
), ),
if (!controller.readonly) if (!controller.readonly)
PopupMenuButton<ImagePackUsage>( PopupMenuButton<ImagePackUsage>(
onSelected: (usage) => controller.toggleUsage( onSelected: (usage) =>
imageCode, controller.toggleUsage(imageCode, usage),
usage,
),
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
value: ImagePackUsage.sticker, value: ImagePackUsage.sticker,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
if (image.usage?.contains( if (image.usage?.contains(
ImagePackUsage.sticker, ImagePackUsage.sticker,
@ -316,7 +311,7 @@ class EmotesSettingsView extends StatelessWidget {
PopupMenuItem( PopupMenuItem(
value: ImagePackUsage.emoticon, value: ImagePackUsage.emoticon,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
if (image.usage?.contains( if (image.usage?.contains(
ImagePackUsage.emoticon, ImagePackUsage.emoticon,
@ -363,10 +358,8 @@ class _EmoteImage extends StatelessWidget {
final key = 'sticker_preview_$mxc'; final key = 'sticker_preview_$mxc';
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
onTap: () => showDialog( onTap: () =>
context: context, showDialog(context: context, builder: (_) => MxcImageViewer(mxc)),
builder: (_) => MxcImageViewer(mxc),
),
child: MxcImage( child: MxcImage(
key: ValueKey(key), key: ValueKey(key),
cacheKey: key, cacheKey: key,

View file

@ -17,7 +17,7 @@ class SettingsHomeserver extends StatefulWidget {
class SettingsHomeserverController extends State<SettingsHomeserver> { class SettingsHomeserverController extends State<SettingsHomeserver> {
Future<({String name, String version, Uri federationBaseUrl})> Future<({String name, String version, Uri federationBaseUrl})>
fetchServerInfo() async { fetchServerInfo() async {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
final domain = client.userID!.domain!; final domain = client.userID!.domain!;
final httpClient = client.httpClient; final httpClient = client.httpClient;
@ -37,15 +37,10 @@ class SettingsHomeserverController extends State<SettingsHomeserver> {
} }
final serverVersionResult = await http.get( final serverVersionResult = await http.get(
federationBaseUrl.resolveUri( federationBaseUrl.resolveUri(Uri(path: '/_matrix/federation/v1/version')),
Uri(path: '/_matrix/federation/v1/version'),
),
); );
final { final {
'server': { 'server': {'name': String name, 'version': String version},
'name': String name,
'version': String version,
},
} = Map<String, Map<String, dynamic>>.from( } = Map<String, Map<String, dynamic>>.from(
jsonDecode(serverVersionResult.body), jsonDecode(serverVersionResult.body),
); );

View file

@ -30,15 +30,16 @@ class SettingsHomeserverView extends StatelessWidget {
automaticallyImplyLeading: !FluffyThemes.isColumnMode(context), automaticallyImplyLeading: !FluffyThemes.isColumnMode(context),
centerTitle: FluffyThemes.isColumnMode(context), centerTitle: FluffyThemes.isColumnMode(context),
title: Text( title: Text(
L10n.of(context) L10n.of(
.aboutHomeserver(client.userID?.domain ?? 'Homeserver'), context,
).aboutHomeserver(client.userID?.domain ?? 'Homeserver'),
), ),
), ),
body: MaxWidthBody( body: MaxWidthBody(
withScrolling: true, withScrolling: true,
child: SelectionArea( child: SelectionArea(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
ListTile( ListTile(
title: Text( title: Text(
@ -68,9 +69,7 @@ class SettingsHomeserverView extends StatelessWidget {
} }
if (data == null) { if (data == null) {
return const Center( return const Center(
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(strokeWidth: 2),
strokeWidth: 2,
),
); );
} }
final supportPage = data.supportPage; final supportPage = data.supportPage;
@ -85,7 +84,7 @@ class SettingsHomeserverView extends StatelessWidget {
); );
} }
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
if (supportPage != null) if (supportPage != null)
ListTile( ListTile(
@ -93,32 +92,28 @@ class SettingsHomeserverView extends StatelessWidget {
subtitle: Text(supportPage.toString()), subtitle: Text(supportPage.toString()),
), ),
if (contacts != null) if (contacts != null)
...contacts.map( ...contacts.map((contact) {
(contact) { return ListTile(
return ListTile( title: Text(
title: Text( contact.role.localizedString(L10n.of(context)),
contact.role.localizedString( ),
L10n.of(context), subtitle: Column(
), mainAxisSize: .min,
), children: [
subtitle: Column( if (contact.emailAddress != null)
mainAxisSize: MainAxisSize.min, TextButton(
children: [ onPressed: () {},
if (contact.emailAddress != null) child: Text(contact.emailAddress!),
TextButton( ),
onPressed: () {}, if (contact.matrixId != null)
child: Text(contact.emailAddress!), TextButton(
), onPressed: () {},
if (contact.matrixId != null) child: Text(contact.matrixId!),
TextButton( ),
onPressed: () {}, ],
child: Text(contact.matrixId!), ),
), );
], }),
),
);
},
),
], ],
); );
}, },
@ -129,7 +124,7 @@ class SettingsHomeserverView extends StatelessWidget {
final error = snapshot.error; final error = snapshot.error;
if (error != null) { if (error != null) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
Icon( Icon(
Icons.error_outlined, Icons.error_outlined,
@ -139,9 +134,7 @@ class SettingsHomeserverView extends StatelessWidget {
Text( Text(
error.toLocalizedString(context), error.toLocalizedString(context),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(color: theme.colorScheme.error),
color: theme.colorScheme.error,
),
), ),
], ],
); );
@ -149,13 +142,11 @@ class SettingsHomeserverView extends StatelessWidget {
final data = snapshot.data; final data = snapshot.data;
if (data == null) { if (data == null) {
return const Center( return const Center(
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(strokeWidth: 2),
strokeWidth: 2,
),
); );
} }
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
ListTile( ListTile(
title: Text(L10n.of(context).name), title: Text(L10n.of(context).name),
@ -169,8 +160,9 @@ class SettingsHomeserverView extends StatelessWidget {
title: const Text('Federation Base URL'), title: const Text('Federation Base URL'),
subtitle: Linkify( subtitle: Linkify(
text: data.federationBaseUrl.toString(), text: data.federationBaseUrl.toString(),
textScaleFactor: textScaleFactor: MediaQuery.textScalerOf(
MediaQuery.textScalerOf(context).scale(1), context,
).scale(1),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@ -191,7 +183,7 @@ class SettingsHomeserverView extends StatelessWidget {
final error = snapshot.error; final error = snapshot.error;
if (error != null) { if (error != null) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: .center,
children: [ children: [
Icon( Icon(
Icons.error_outlined, Icons.error_outlined,
@ -201,9 +193,7 @@ class SettingsHomeserverView extends StatelessWidget {
Text( Text(
error.toLocalizedString(context), error.toLocalizedString(context),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(color: theme.colorScheme.error),
color: theme.colorScheme.error,
),
), ),
], ],
); );
@ -211,14 +201,12 @@ class SettingsHomeserverView extends StatelessWidget {
final wellKnown = snapshot.data; final wellKnown = snapshot.data;
if (wellKnown == null) { if (wellKnown == null) {
return const Center( return const Center(
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(strokeWidth: 2),
strokeWidth: 2,
),
); );
} }
final identityServer = wellKnown.mIdentityServer; final identityServer = wellKnown.mIdentityServer;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
ListTile( ListTile(
title: Text( title: Text(
@ -233,8 +221,9 @@ class SettingsHomeserverView extends StatelessWidget {
title: const Text('Base URL'), title: const Text('Base URL'),
subtitle: Linkify( subtitle: Linkify(
text: wellKnown.mHomeserver.baseUrl.toString(), text: wellKnown.mHomeserver.baseUrl.toString(),
textScaleFactor: textScaleFactor: MediaQuery.textScalerOf(
MediaQuery.textScalerOf(context).scale(1), context,
).scale(1),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@ -248,8 +237,9 @@ class SettingsHomeserverView extends StatelessWidget {
title: const Text('Identity Server:'), title: const Text('Identity Server:'),
subtitle: Linkify( subtitle: Linkify(
text: identityServer.baseUrl.toString(), text: identityServer.baseUrl.toString(),
textScaleFactor: textScaleFactor: MediaQuery.textScalerOf(
MediaQuery.textScalerOf(context).scale(1), context,
).scale(1),
options: const LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
linkStyle: TextStyle( linkStyle: TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@ -262,15 +252,17 @@ class SettingsHomeserverView extends StatelessWidget {
(entry) => ListTile( (entry) => ListTile(
title: Text(entry.key), title: Text(entry.key),
subtitle: Material( subtitle: Material(
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(AppConfig.borderRadius), AppConfig.borderRadius,
),
color: theme.colorScheme.surfaceContainer, color: theme.colorScheme.surfaceContainer,
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Text( child: Text(
const JsonEncoder.withIndent(' ') const JsonEncoder.withIndent(
.convert(entry.value), ' ',
).convert(entry.value),
style: TextStyle( style: TextStyle(
color: theme.colorScheme.onSurface, color: theme.colorScheme.onSurface,
), ),

View file

@ -36,12 +36,12 @@ class SettingsIgnoreListView extends StatelessWidget {
return const Center(child: CircularProgressIndicator.adaptive()); return const Center(child: CircularProgressIndicator.adaptive());
} }
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: .min,
children: [ children: [
TextField( TextField(
controller: controller.controller, controller: controller.controller,
@ -68,9 +68,7 @@ class SettingsIgnoreListView extends StatelessWidget {
], ],
), ),
), ),
Divider( Divider(color: theme.dividerColor),
color: theme.dividerColor,
),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: client.ignoredUsers.length, itemCount: client.ignoredUsers.length,

View file

@ -45,11 +45,8 @@ class SettingsNotificationsController extends State<SettingsNotifications> {
final success = await showFutureLoadingDialog( final success = await showFutureLoadingDialog(
context: context, context: context,
future: () => Matrix.of(context).client.deletePusher( future: () => Matrix.of(context).client.deletePusher(
PusherId( PusherId(appId: pusher.appId, pushkey: pusher.pushkey),
appId: pusher.appId, ),
pushkey: pusher.pushkey,
),
),
); );
if (success.error != null) return; if (success.error != null) return;
@ -66,10 +63,7 @@ class SettingsNotificationsController extends State<SettingsNotifications> {
isLoading = true; isLoading = true;
}); });
try { try {
final updateFromSync = Matrix.of(context) final updateFromSync = Matrix.of(context).client.onSync.stream
.client
.onSync
.stream
.where( .where(
(syncUpdate) => (syncUpdate) =>
syncUpdate.accountData?.any( syncUpdate.accountData?.any(
@ -78,17 +72,16 @@ class SettingsNotificationsController extends State<SettingsNotifications> {
false, false,
) )
.first; .first;
await Matrix.of(context).client.setPushRuleEnabled( await Matrix.of(
kind, context,
pushRule.ruleId, ).client.setPushRuleEnabled(kind, pushRule.ruleId, !pushRule.enabled);
!pushRule.enabled,
);
await updateFromSync; await updateFromSync;
} catch (e, s) { } catch (e, s) {
Logs().w('Unable to toggle push rule', e, s); Logs().w('Unable to toggle push rule', e, s);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context) ScaffoldMessenger.of(
.showSnackBar(SnackBar(content: Text(e.toLocalizedString(context)))); context,
).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
} finally { } finally {
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -116,9 +109,7 @@ class SettingsNotificationsController extends State<SettingsNotifications> {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: SelectableText( child: SelectableText(
prettyJson(rule.toJson()), prettyJson(rule.toJson()),
style: TextStyle( style: TextStyle(color: theme.colorScheme.onSurface),
color: theme.colorScheme.onSurface,
),
), ),
), ),
), ),
@ -158,10 +149,7 @@ class SettingsNotificationsController extends State<SettingsNotifications> {
isLoading = true; isLoading = true;
}); });
try { try {
final updateFromSync = Matrix.of(context) final updateFromSync = Matrix.of(context).client.onSync.stream
.client
.onSync
.stream
.where( .where(
(syncUpdate) => (syncUpdate) =>
syncUpdate.accountData?.any( syncUpdate.accountData?.any(
@ -170,17 +158,14 @@ class SettingsNotificationsController extends State<SettingsNotifications> {
false, false,
) )
.first; .first;
await Matrix.of(context).client.deletePushRule( await Matrix.of(context).client.deletePushRule(kind, rule.ruleId);
kind,
rule.ruleId,
);
await updateFromSync; await updateFromSync;
} catch (e, s) { } catch (e, s) {
Logs().w('Unable to delete push rule', e, s); Logs().w('Unable to delete push rule', e, s);
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(
SnackBar(content: Text(e.toLocalizedString(context))), context,
); ).showSnackBar(SnackBar(content: Text(e.toLocalizedString(context))));
} finally { } finally {
if (mounted) { if (mounted) {
setState(() { setState(() {

View file

@ -37,12 +37,12 @@ class SettingsNotificationsView extends StatelessWidget {
body: MaxWidthBody( body: MaxWidthBody(
child: StreamBuilder( child: StreamBuilder(
stream: Matrix.of(context).client.onSync.stream.where( stream: Matrix.of(context).client.onSync.stream.where(
(syncUpdate) => (syncUpdate) =>
syncUpdate.accountData?.any( syncUpdate.accountData?.any(
(accountData) => accountData.type == 'm.push_rules', (accountData) => accountData.type == 'm.push_rules',
) ?? ) ??
false, false,
), ),
builder: (BuildContext context, _) { builder: (BuildContext context, _) {
final theme = Theme.of(context); final theme = Theme.of(context);
return SelectionArea( return SelectionArea(
@ -96,14 +96,14 @@ class SettingsNotificationsView extends StatelessWidget {
onChanged: controller.isLoading onChanged: controller.isLoading
? null ? null
: rule.ruleId != '.m.rule.master' && : rule.ruleId != '.m.rule.master' &&
Matrix.of(context) Matrix.of(
.client context,
.allPushNotificationsMuted ).client.allPushNotificationsMuted
? null ? null
: (_) => controller.togglePushRule( : (_) => controller.togglePushRule(
category.kind, category.kind,
rule, rule,
), ),
), ),
), ),
Divider(color: theme.dividerColor), Divider(color: theme.dividerColor),
@ -118,8 +118,9 @@ class SettingsNotificationsView extends StatelessWidget {
), ),
), ),
FutureBuilder<List<Pusher>?>( FutureBuilder<List<Pusher>?>(
future: controller.pusherFuture ??= future: controller.pusherFuture ??= Matrix.of(
Matrix.of(context).client.getPushers(), context,
).client.getPushers(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
Center( Center(

View file

@ -55,13 +55,11 @@ class SettingsPasswordController extends State<SettingsPassword> {
try { try {
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
await Matrix.of(context).client.changePassword( await Matrix.of(context).client.changePassword(
newPassword1Controller.text, newPassword1Controller.text,
oldPassword: oldPasswordController.text, oldPassword: oldPasswordController.text,
); );
scaffoldMessenger.showSnackBar( scaffoldMessenger.showSnackBar(
SnackBar( SnackBar(content: Text(L10n.of(context).passwordHasBeenChanged)),
content: Text(L10n.of(context).passwordHasBeenChanged),
),
); );
if (mounted) context.pop(); if (mounted) context.pop();
} catch (e) { } catch (e) {

View file

@ -15,9 +15,7 @@ class SettingsPasswordView extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: Text(L10n.of(context).changePassword)),
title: Text(L10n.of(context).changePassword),
),
body: ListTileTheme( body: ListTileTheme(
iconColor: theme.colorScheme.onSurface, iconColor: theme.colorScheme.onSurface,
child: MaxWidthBody( child: MaxWidthBody(
@ -69,8 +67,9 @@ class SettingsPasswordView extends StatelessWidget {
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: onPressed: controller.loading
controller.loading ? null : controller.changePassword, ? null
: controller.changePassword,
child: controller.loading child: controller.loading
? const LinearProgressIndicator() ? const LinearProgressIndicator()
: Text(L10n.of(context).changePassword), : Text(L10n.of(context).changePassword),

View file

@ -90,13 +90,13 @@ class SettingsSecurityController extends State<SettingsSecurity> {
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () => Matrix.of(context).client.deactivateAccount( future: () => Matrix.of(context).client.deactivateAccount(
auth: AuthenticationPassword( auth: AuthenticationPassword(
password: input, password: input,
identifier: AuthenticationUserIdentifier( identifier: AuthenticationUserIdentifier(
user: Matrix.of(context).client.userID!, user: Matrix.of(context).client.userID!,
),
),
), ),
),
),
); );
} }
@ -104,9 +104,7 @@ class SettingsSecurityController extends State<SettingsSecurity> {
void changeShareKeysWith(ShareKeysWith? shareKeysWith) async { void changeShareKeysWith(ShareKeysWith? shareKeysWith) async {
if (shareKeysWith == null) return; if (shareKeysWith == null) return;
AppSettings.shareKeysWith.setItem( AppSettings.shareKeysWith.setItem(shareKeysWith.name);
shareKeysWith.name,
);
Matrix.of(context).client.shareKeysWith = shareKeysWith; Matrix.of(context).client.shareKeysWith = shareKeysWith;
setState(() {}); setState(() {});
} }

View file

@ -33,10 +33,9 @@ class SettingsSecurityView extends StatelessWidget {
iconColor: theme.colorScheme.onSurface, iconColor: theme.colorScheme.onSurface,
child: MaxWidthBody( child: MaxWidthBody(
child: FutureBuilder( child: FutureBuilder(
future: Matrix.of(context) future: Matrix.of(
.client context,
.getCapabilities() ).client.getCapabilities().timeout(const Duration(seconds: 10)),
.timeout(const Duration(seconds: 10)),
builder: (context, snapshot) { builder: (context, snapshot) {
final capabilities = snapshot.data; final capabilities = snapshot.data;
final error = snapshot.error; final error = snapshot.error;
@ -61,8 +60,9 @@ class SettingsSecurityView extends StatelessWidget {
), ),
SettingsSwitchListTile.adaptive( SettingsSwitchListTile.adaptive(
title: L10n.of(context).sendTypingNotifications, title: L10n.of(context).sendTypingNotifications,
subtitle: subtitle: L10n.of(
L10n.of(context).sendTypingNotificationsDescription, context,
).sendTypingNotificationsDescription,
setting: AppSettings.sendTypingNotifications, setting: AppSettings.sendTypingNotifications,
), ),
SettingsSwitchListTile.adaptive( SettingsSwitchListTile.adaptive(
@ -103,14 +103,16 @@ class SettingsSecurityView extends StatelessWidget {
), ),
ListTile( ListTile(
title: Material( title: Material(
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(AppConfig.borderRadius / 2), AppConfig.borderRadius / 2,
),
color: theme.colorScheme.onInverseSurface, color: theme.colorScheme.onInverseSurface,
child: DropdownButton<ShareKeysWith>( child: DropdownButton<ShareKeysWith>(
isExpanded: true, isExpanded: true,
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular(AppConfig.borderRadius / 2), AppConfig.borderRadius / 2,
),
underline: const SizedBox.shrink(), underline: const SizedBox.shrink(),
value: Matrix.of(context).client.shareKeysWith, value: Matrix.of(context).client.shareKeysWith,
items: ShareKeysWith.values items: ShareKeysWith.values

Some files were not shown because too many files have changed in this diff Show more