refactor: Added and applied require_trailing_commas linter rule

This commit is contained in:
Malin Errenst 2023-03-02 10:57:52 +01:00
parent ed9e58d0bf
commit 5212d7ce4d
112 changed files with 3509 additions and 2898 deletions

View File

@ -8,6 +8,7 @@ linter:
- prefer_final_locals - prefer_final_locals
- prefer_final_in_for_each - prefer_final_in_for_each
- sort_pub_dependencies - sort_pub_dependencies
- require_trailing_commas
analyzer: analyzer:
errors: errors:

View File

@ -144,13 +144,15 @@ void main() {
await tester.waitFor( await tester.waitFor(
find.descendant( find.descendant(
of: find.byType(InvitationSelectionView), of: find.byType(InvitationSelectionView),
matching: find.byType(TextField)), matching: find.byType(TextField),
),
); );
await tester.enterText( await tester.enterText(
find.descendant( find.descendant(
of: find.byType(InvitationSelectionView), of: find.byType(InvitationSelectionView),
matching: find.byType(TextField)), matching: find.byType(TextField),
),
Users.user2.name, Users.user2.name,
); );
@ -160,14 +162,17 @@ void main() {
await Future.delayed(const Duration(milliseconds: 1000)); await Future.delayed(const Duration(milliseconds: 1000));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tap(find await tester.tap(
.descendant( find
.descendant(
of: find.descendant( of: find.descendant(
of: find.byType(InvitationSelectionView), of: find.byType(InvitationSelectionView),
matching: find.byType(ListTile), matching: find.byType(ListTile),
), ),
matching: find.text(Users.user2.name)) matching: find.text(Users.user2.name),
.last); )
.last,
);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.waitFor(find.maybeUppercaseText('Yes')); await tester.waitFor(find.maybeUppercaseText('Yes'));

View File

@ -144,7 +144,8 @@ extension DefaultFlowExtensions on WidgetTester {
do { do {
if (DateTime.now().isAfter(end)) { if (DateTime.now().isAfter(end)) {
throw Exception( throw Exception(
'Timed out waiting for HomeserverPicker or ChatListViewBody'); 'Timed out waiting for HomeserverPicker or ChatListViewBody',
);
} }
await pumpAndSettle(); await pumpAndSettle();

View File

@ -70,8 +70,9 @@ abstract class AppConfig {
colorSchemeSeed = Color(json['chat_color']); colorSchemeSeed = Color(json['chat_color']);
} catch (e) { } catch (e) {
Logs().w( Logs().w(
'Invalid color in config.json! Please make sure to define the color in this format: "0xffdd0000"', 'Invalid color in config.json! Please make sure to define the color in this format: "0xffdd0000"',
e); e,
);
} }
} }
if (json['application_name'] is String) { if (json['application_name'] is String) {

View File

@ -70,21 +70,25 @@ class AppRoutes {
widget: const ChatDetails(), widget: const ChatDetails(),
stackedRoutes: _chatDetailsRoutes, stackedRoutes: _chatDetailsRoutes,
), ),
VWidget(path: ':roomid', widget: const Chat(), stackedRoutes: [ VWidget(
VWidget( path: ':roomid',
path: 'encryption', widget: const Chat(),
widget: const ChatEncryptionSettings(), stackedRoutes: [
), VWidget(
VWidget( path: 'encryption',
path: 'invite', widget: const ChatEncryptionSettings(),
widget: const InvitationSelection(), ),
), VWidget(
VWidget( path: 'invite',
path: 'details', widget: const InvitationSelection(),
widget: const ChatDetails(), ),
stackedRoutes: _chatDetailsRoutes, VWidget(
), path: 'details',
]), widget: const ChatDetails(),
stackedRoutes: _chatDetailsRoutes,
),
],
),
VWidget( VWidget(
path: '/settings', path: '/settings',
widget: const Settings(), widget: const Settings(),
@ -263,21 +267,22 @@ class AppRoutes {
buildTransition: _fadeTransition, buildTransition: _fadeTransition,
), ),
VWidget( VWidget(
path: 'connect', path: 'connect',
widget: const ConnectPage(), widget: const ConnectPage(),
buildTransition: _fadeTransition, buildTransition: _fadeTransition,
stackedRoutes: [ stackedRoutes: [
VWidget( VWidget(
path: 'login', path: 'login',
widget: const Login(), widget: const Login(),
buildTransition: _fadeTransition, buildTransition: _fadeTransition,
), ),
VWidget( VWidget(
path: 'signup', path: 'signup',
widget: const SignupPage(), widget: const SignupPage(),
buildTransition: _fadeTransition, buildTransition: _fadeTransition,
), ),
]), ],
),
VWidget( VWidget(
path: 'logs', path: 'logs',
widget: const LogViewer(), widget: const LogViewer(),
@ -354,21 +359,22 @@ class AppRoutes {
buildTransition: _fadeTransition, buildTransition: _fadeTransition,
), ),
VWidget( VWidget(
path: 'connect', path: 'connect',
widget: const ConnectPage(), widget: const ConnectPage(),
buildTransition: _fadeTransition, buildTransition: _fadeTransition,
stackedRoutes: [ stackedRoutes: [
VWidget( VWidget(
path: 'login', path: 'login',
widget: const Login(), widget: const Login(),
buildTransition: _fadeTransition, buildTransition: _fadeTransition,
), ),
VWidget( VWidget(
path: 'signup', path: 'signup',
widget: const SignupPage(), widget: const SignupPage(),
buildTransition: _fadeTransition, buildTransition: _fadeTransition,
), ),
]), ],
),
], ],
), ),
VWidget( VWidget(

View File

@ -88,14 +88,15 @@ class AddStoryController extends State<AddStoryPage> {
); );
if (picked == null) return; if (picked == null) return;
final matrixFile = await showFutureLoadingDialog( final matrixFile = await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
final bytes = await picked.readAsBytes(); final bytes = await picked.readAsBytes();
return MatrixImageFile( return MatrixImageFile(
bytes: bytes, bytes: bytes,
name: picked.name, name: picked.name,
); );
}); },
);
setState(() { setState(() {
image = matrixFile.result; image = matrixFile.result;

View File

@ -92,34 +92,39 @@ class InviteStoryPageState extends State<InviteStoryPage> {
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: FutureBuilder<List<User>>( child: FutureBuilder<List<User>>(
future: loadContacts, future: loadContacts,
builder: (context, snapshot) { builder: (context, snapshot) {
final contacts = snapshot.data; final contacts = snapshot.data;
if (contacts == null) { if (contacts == null) {
final error = snapshot.error; final error = snapshot.error;
if (error != null) { if (error != null) {
return Center( return Center(
child: Text(error.toLocalizedString(context))); child: Text(error.toLocalizedString(context)),
} );
return const Center(
child: CircularProgressIndicator.adaptive());
} }
_undecided = contacts.map((u) => u.id).toSet(); return const Center(
return ListView.builder( child: CircularProgressIndicator.adaptive(),
itemCount: contacts.length,
itemBuilder: (context, i) => SwitchListTile.adaptive(
value: _invite.contains(contacts[i].id),
onChanged: (b) => setState(() => b
? _invite.add(contacts[i].id)
: _invite.remove(contacts[i].id)),
secondary: Avatar(
mxContent: contacts[i].avatarUrl,
name: contacts[i].calcDisplayname(),
),
title: Text(contacts[i].calcDisplayname()),
),
); );
}), }
_undecided = contacts.map((u) => u.id).toSet();
return ListView.builder(
itemCount: contacts.length,
itemBuilder: (context, i) => SwitchListTile.adaptive(
value: _invite.contains(contacts[i].id),
onChanged: (b) => setState(
() => b
? _invite.add(contacts[i].id)
: _invite.remove(contacts[i].id),
),
secondary: Avatar(
mxContent: contacts[i].avatarUrl,
name: contacts[i].calcDisplayname(),
),
title: Text(contacts[i].calcDisplayname()),
),
);
},
),
), ),
], ],
), ),

View File

@ -36,19 +36,22 @@ class ArchiveView extends StatelessWidget {
builder: (BuildContext context) { builder: (BuildContext context) {
if (snapshot.hasError) { if (snapshot.hasError) {
return Center( return Center(
child: Text( child: Text(
L10n.of(context)!.oopsSomethingWentWrong, L10n.of(context)!.oopsSomethingWentWrong,
textAlign: TextAlign.center, textAlign: TextAlign.center,
)); ),
);
} }
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Center( return const Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2)); child: CircularProgressIndicator.adaptive(strokeWidth: 2),
);
} else { } else {
archive = snapshot.data; archive = snapshot.data;
if (archive == null || archive!.isEmpty) { if (archive == null || archive!.isEmpty) {
return const Center( return const Center(
child: Icon(Icons.archive_outlined, size: 80)); child: Icon(Icons.archive_outlined, size: 80),
);
} }
return ListView.builder( return ListView.builder(
itemCount: archive!.length, itemCount: archive!.length,

View File

@ -246,7 +246,8 @@ class BootstrapDialogState extends State<BootstrapDialog> {
body: Center( body: Center(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints( constraints: 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: [
@ -258,7 +259,8 @@ class BootstrapDialogState extends State<BootstrapDialog> {
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
subtitle: Text( subtitle: Text(
L10n.of(context)!.pleaseEnterRecoveryKeyDescription), L10n.of(context)!.pleaseEnterRecoveryKeyDescription,
),
), ),
const Divider(height: 32), const Divider(height: 32),
TextField( TextField(
@ -274,64 +276,68 @@ class BootstrapDialogState extends State<BootstrapDialog> {
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(16), contentPadding: const EdgeInsets.all(16),
hintStyle: TextStyle( hintStyle: TextStyle(
fontFamily: Theme.of(context) fontFamily:
.textTheme Theme.of(context).textTheme.bodyLarge?.fontFamily,
.bodyLarge ),
?.fontFamily),
hintText: L10n.of(context)!.recoveryKey, hintText: L10n.of(context)!.recoveryKey,
errorText: _recoveryKeyInputError, errorText: _recoveryKeyInputError,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
foregroundColor: foregroundColor:
Theme.of(context).colorScheme.onPrimary, Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
),
icon: _recoveryKeyInputLoading
? const CircularProgressIndicator.adaptive()
: const Icon(Icons.lock_open_outlined),
label: Text(L10n.of(context)!.unlockOldMessages),
onPressed: _recoveryKeyInputLoading
? null
: () async {
setState(() {
_recoveryKeyInputError = null;
_recoveryKeyInputLoading = true;
});
try {
final key =
_recoveryKeyTextEditingController.text;
await bootstrap.newSsssKey!.unlock(
keyOrPassphrase: key,
);
Logs().d('SSSS unlocked');
await bootstrap
.client.encryption!.crossSigning
.selfSign(
keyOrPassphrase: key,
);
Logs().d('Successful elfsigned');
await bootstrap.openExistingSsss();
} catch (e, s) {
Logs().w('Unable to unlock SSSS', e, s);
setState(() => _recoveryKeyInputError =
L10n.of(context)!.oopsSomethingWentWrong);
} finally {
setState(
() => _recoveryKeyInputLoading = false);
}
}),
const SizedBox(height: 16),
Row(children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(L10n.of(context)!.or),
), ),
const Expanded(child: Divider()), icon: _recoveryKeyInputLoading
]), ? const CircularProgressIndicator.adaptive()
: const Icon(Icons.lock_open_outlined),
label: Text(L10n.of(context)!.unlockOldMessages),
onPressed: _recoveryKeyInputLoading
? null
: () async {
setState(() {
_recoveryKeyInputError = null;
_recoveryKeyInputLoading = true;
});
try {
final key =
_recoveryKeyTextEditingController.text;
await bootstrap.newSsssKey!.unlock(
keyOrPassphrase: key,
);
Logs().d('SSSS unlocked');
await bootstrap.client.encryption!.crossSigning
.selfSign(
keyOrPassphrase: key,
);
Logs().d('Successful elfsigned');
await bootstrap.openExistingSsss();
} catch (e, s) {
Logs().w('Unable to unlock SSSS', e, s);
setState(
() => _recoveryKeyInputError =
L10n.of(context)!.oopsSomethingWentWrong,
);
} finally {
setState(
() => _recoveryKeyInputLoading = false,
);
}
},
),
const SizedBox(height: 16),
Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(L10n.of(context)!.or),
),
const Expanded(child: Divider()),
],
),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.cast_connected_outlined), icon: const Icon(Icons.cast_connected_outlined),
@ -408,11 +414,13 @@ class BootstrapDialogState extends State<BootstrapDialog> {
case BootstrapState.error: case BootstrapState.error:
titleText = L10n.of(context)!.oopsSomethingWentWrong; titleText = L10n.of(context)!.oopsSomethingWentWrong;
body = const Icon(Icons.error_outline, color: Colors.red, size: 40); body = const Icon(Icons.error_outline, color: Colors.red, size: 40);
buttons.add(AdaptiveFlatButton( buttons.add(
label: L10n.of(context)!.close, AdaptiveFlatButton(
onPressed: () => label: L10n.of(context)!.close,
Navigator.of(context, rootNavigator: false).pop<bool>(false), onPressed: () =>
)); Navigator.of(context, rootNavigator: false).pop<bool>(false),
),
);
break; break;
case BootstrapState.done: case BootstrapState.done:
titleText = L10n.of(context)!.everythingReady; titleText = L10n.of(context)!.everythingReady;
@ -423,11 +431,13 @@ class BootstrapDialogState extends State<BootstrapDialog> {
Text(L10n.of(context)!.yourChatBackupHasBeenSetUp), Text(L10n.of(context)!.yourChatBackupHasBeenSetUp),
], ],
); );
buttons.add(AdaptiveFlatButton( buttons.add(
label: L10n.of(context)!.close, AdaptiveFlatButton(
onPressed: () => label: L10n.of(context)!.close,
Navigator.of(context, rootNavigator: false).pop<bool>(false), onPressed: () =>
)); Navigator.of(context, rootNavigator: false).pop<bool>(false),
),
);
break; break;
} }
} }

View File

@ -75,7 +75,8 @@ class AddWidgetTileState extends State<AddWidgetTile> {
Navigator.of(context).pop(); Navigator.of(context).pop();
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context)!.errorAddingWidget))); SnackBar(content: Text(L10n.of(context)!.errorAddingWidget)),
);
} }
} }

View File

@ -26,12 +26,15 @@ class AddWidgetTileView extends StatelessWidget {
'm.jitsi': Text(L10n.of(context)!.widgetJitsi), 'm.jitsi': Text(L10n.of(context)!.widgetJitsi),
'm.video': Text(L10n.of(context)!.widgetVideo), 'm.video': Text(L10n.of(context)!.widgetVideo),
'm.custom': Text(L10n.of(context)!.widgetCustom), 'm.custom': Text(L10n.of(context)!.widgetCustom),
}.map((key, value) => MapEntry( }.map(
(key, value) => MapEntry(
key, key,
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: value, child: value,
))), ),
),
),
onValueChanged: controller.setWidgetType, onValueChanged: controller.setWidgetType,
), ),
Padding( Padding(

View File

@ -82,10 +82,12 @@ class ChatController extends State<Chat> {
final matrixFiles = <MatrixFile>[]; final matrixFiles = <MatrixFile>[];
for (var i = 0; i < bytesList.result!.length; i++) { for (var i = 0; i < bytesList.result!.length; i++) {
matrixFiles.add(MatrixFile( matrixFiles.add(
bytes: bytesList.result![i], MatrixFile(
name: details.files[i].name, bytes: bytesList.result![i],
).detectFileType); name: details.files[i].name,
).detectFileType,
);
} }
await showDialog( await showDialog(
@ -139,18 +141,20 @@ class ChatController extends State<Chat> {
final userId = room?.directChatMatrixID; final userId = room?.directChatMatrixID;
if (room == null || userId == null) { if (room == null || userId == null) {
throw Exception( throw Exception(
'Try to recreate a room with is not a DM room. This should not be possible from the UI!'); 'Try to recreate a room with is not a DM room. This should not be possible from the UI!',
);
} }
final success = await showFutureLoadingDialog( final success = await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
final client = room.client; final client = room.client;
final waitForSync = client.onSync.stream final waitForSync = client.onSync.stream
.firstWhere((s) => s.rooms?.leave?.containsKey(room.id) ?? false); .firstWhere((s) => s.rooms?.leave?.containsKey(room.id) ?? false);
await room.leave(); await room.leave();
await waitForSync; await waitForSync;
return await client.startDirectChat(userId); return await client.startDirectChat(userId);
}); },
);
final roomId = success.result; final roomId = success.result;
if (roomId == null) return; if (roomId == null) return;
VRouter.of(context).toSegments(['rooms', roomId]); VRouter.of(context).toSegments(['rooms', roomId]);
@ -160,7 +164,8 @@ class ChatController extends State<Chat> {
final room = this.room; final room = this.room;
if (room == null) { if (room == null) {
throw Exception( throw Exception(
'Leave room button clicked while room is null. This should not be possible from the UI!'); 'Leave room button clicked while room is null. This should not be possible from the UI!',
);
} }
final success = await showFutureLoadingDialog( final success = await showFutureLoadingDialog(
context: context, context: context,
@ -327,10 +332,12 @@ class ChatController extends State<Chat> {
} }
// ignore: unawaited_futures // ignore: unawaited_futures
room!.sendTextEvent(sendController.text, room!.sendTextEvent(
inReplyTo: replyEvent, sendController.text,
editEventId: editEvent?.eventId, inReplyTo: replyEvent,
parseCommands: parseCommands); editEventId: editEvent?.eventId,
parseCommands: parseCommands,
);
sendController.value = TextEditingValue( sendController.value = TextEditingValue(
text: pendingText, text: pendingText,
selection: const TextSelection.collapsed(offset: 0), selection: const TextSelection.collapsed(offset: 0),
@ -354,10 +361,12 @@ class ChatController extends State<Chat> {
useRootNavigator: false, useRootNavigator: false,
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: result files: result
.map((xfile) => MatrixFile( .map(
bytes: xfile.toUint8List(), (xfile) => MatrixFile(
name: xfile.fileName!, bytes: xfile.toUint8List(),
).detectFileType) name: xfile.fileName!,
).detectFileType,
)
.toList(), .toList(),
room: room!, room: room!,
), ),
@ -375,10 +384,12 @@ class ChatController extends State<Chat> {
useRootNavigator: false, useRootNavigator: false,
builder: (c) => SendFileDialog( builder: (c) => SendFileDialog(
files: result files: result
.map((xfile) => MatrixFile( .map(
bytes: xfile.toUint8List(), (xfile) => MatrixFile(
name: xfile.fileName!, bytes: xfile.toUint8List(),
).detectFileType) name: xfile.fileName!,
).detectFileType,
)
.toList(), .toList(),
room: room!, room: room!,
), ),
@ -537,8 +548,9 @@ class ChatController extends State<Chat> {
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,
);
} }
return copyString; return copyString;
} }
@ -554,33 +566,35 @@ class ChatController extends State<Chat> {
void reportEventAction() async { void reportEventAction() async {
final event = selectedEvents.single; final event = selectedEvents.single;
final score = await showConfirmationDialog<int>( final score = await showConfirmationDialog<int>(
context: context, context: context,
title: L10n.of(context)!.reportMessage, title: L10n.of(context)!.reportMessage,
message: L10n.of(context)!.howOffensiveIsThisContent, message: L10n.of(context)!.howOffensiveIsThisContent,
cancelLabel: L10n.of(context)!.cancel, cancelLabel: L10n.of(context)!.cancel,
okLabel: L10n.of(context)!.ok, okLabel: L10n.of(context)!.ok,
actions: [ actions: [
AlertDialogAction( AlertDialogAction(
key: -100, key: -100,
label: L10n.of(context)!.extremeOffensive, label: L10n.of(context)!.extremeOffensive,
), ),
AlertDialogAction( AlertDialogAction(
key: -50, key: -50,
label: L10n.of(context)!.offensive, label: L10n.of(context)!.offensive,
), ),
AlertDialogAction( AlertDialogAction(
key: 0, key: 0,
label: L10n.of(context)!.inoffensive, label: L10n.of(context)!.inoffensive,
), ),
]); ],
);
if (score == null) return; if (score == null) return;
final reason = await showTextInputDialog( final reason = await showTextInputDialog(
useRootNavigator: false, useRootNavigator: false,
context: context, context: context,
title: L10n.of(context)!.whyDoYouWantToReportThis, title: L10n.of(context)!.whyDoYouWantToReportThis,
okLabel: L10n.of(context)!.ok, okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel, cancelLabel: L10n.of(context)!.cancel,
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)]); textFields: [DialogTextField(hintText: L10n.of(context)!.reason)],
);
if (reason == null || reason.single.isEmpty) return; if (reason == null || reason.single.isEmpty) return;
final result = await showFutureLoadingDialog( final result = await showFutureLoadingDialog(
context: context, context: context,
@ -597,7 +611,8 @@ class ChatController extends State<Chat> {
selectedEvents.clear(); selectedEvents.clear();
}); });
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported))); SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)),
);
} }
void redactEventsAction() async { void redactEventsAction() async {
@ -612,25 +627,27 @@ class ChatController extends State<Chat> {
if (!confirmed) return; if (!confirmed) return;
for (final event in selectedEvents) { for (final event in selectedEvents) {
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
if (event.status.isSent) { if (event.status.isSent) {
if (event.canRedact) { if (event.canRedact) {
await event.redactEvent(); await event.redactEvent();
} else {
final client = currentRoomBundle.firstWhere(
(cl) => selectedEvents.first.senderId == cl!.userID,
orElse: () => null);
if (client == null) {
return;
}
final room = client.getRoomById(roomId!)!;
await Event.fromJson(event.toJson(), room).redactEvent();
}
} else { } else {
await event.remove(); final client = currentRoomBundle.firstWhere(
(cl) => selectedEvents.first.senderId == cl!.userID,
orElse: () => null,
);
if (client == null) {
return;
}
final room = client.getRoomById(roomId!)!;
await Event.fromJson(event.toJson(), room).redactEvent();
} }
}); } else {
await event.remove();
}
},
);
} }
setState(() { setState(() {
showEmojiPicker = false; showEmojiPicker = false;
@ -706,41 +723,42 @@ class ChatController extends State<Chat> {
// event id not found...maybe we can fetch it? // event id not found...maybe we can fetch it?
// the try...finally is here to start and close the loading dialog reliably // the try...finally is here to start and close the loading dialog reliably
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
// okay, we first have to fetch if the event is in the room // okay, we first have to fetch if the event is in the room
try {
final event = await timeline!.getEventById(eventId);
if (event == null) {
// event is null...meaning something is off
return;
}
} catch (err) {
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') {
// event wasn't found, as the server gave a 404 or something
return;
}
rethrow;
}
// okay, we know that the event *is* in the room
while (eventIndex == -1) {
if (!canLoadMore) {
// we can't load any more events but still haven't found ours yet...better stop here
return;
}
try { try {
final event = await timeline!.getEventById(eventId); await timeline!.requestHistory(historyCount: _loadHistoryCount);
if (event == null) {
// event is null...meaning something is off
return;
}
} catch (err) { } catch (err) {
if (err is MatrixException && err.errcode == 'M_NOT_FOUND') { if (err is TimeoutException) {
// event wasn't found, as the server gave a 404 or something // loading the history timed out...so let's do nothing
return; return;
} }
rethrow; rethrow;
} }
// okay, we know that the event *is* in the room eventIndex =
while (eventIndex == -1) { timeline!.events.indexWhere((e) => e.eventId == eventId);
if (!canLoadMore) { }
// we can't load any more events but still haven't found ours yet...better stop here },
return; );
}
try {
await timeline!.requestHistory(historyCount: _loadHistoryCount);
} catch (err) {
if (err is TimeoutException) {
// loading the history timed out...so let's do nothing
return;
}
rethrow;
}
eventIndex =
timeline!.events.indexWhere((e) => e.eventId == eventId);
}
});
} }
if (!mounted) { if (!mounted) {
return; return;
@ -811,7 +829,8 @@ class ChatController extends State<Chat> {
sendController sendController
..text = sendController.text.characters.skipLast(1).toString() ..text = sendController.text.characters.skipLast(1).toString()
..selection = TextSelection.fromPosition( ..selection = TextSelection.fromPosition(
TextPosition(offset: sendController.text.length)); TextPosition(offset: sendController.text.length),
);
break; break;
} }
} }
@ -846,8 +865,9 @@ class ChatController extends State<Chat> {
void editSelectedEventAction() { void editSelectedEventAction() {
final client = currentRoomBundle.firstWhere( final client = currentRoomBundle.firstWhere(
(cl) => selectedEvents.first.senderId == cl!.userID, (cl) => selectedEvents.first.senderId == cl!.userID,
orElse: () => null); orElse: () => null,
);
if (client == null) { if (client == null) {
return; return;
} }
@ -855,10 +875,12 @@ class ChatController extends State<Chat> {
setState(() { setState(() {
pendingText = sendController.text; pendingText = sendController.text;
editEvent = selectedEvents.first; editEvent = selectedEvents.first;
inputText = sendController.text = editEvent! inputText = sendController.text =
.getDisplayEvent(timeline!) editEvent!.getDisplayEvent(timeline!).calcLocalizedBodyFallback(
.calcLocalizedBodyFallback(MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false, hideReply: true); withSenderNamePrefix: false,
hideReply: true,
);
selectedEvents.clear(); selectedEvents.clear();
}); });
inputFocus.requestFocus(); inputFocus.requestFocus();
@ -881,10 +903,12 @@ class ChatController extends State<Chat> {
} }
final result = await showFutureLoadingDialog( final result = await showFutureLoadingDialog(
context: context, context: context,
future: () => room!.client.joinRoom(room! future: () => room!.client.joinRoom(
.getState(EventTypes.RoomTombstone)! room!
.parsedTombstoneContent .getState(EventTypes.RoomTombstone)!
.replacementRoom), .parsedTombstoneContent
.replacementRoom,
),
); );
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
@ -1077,9 +1101,10 @@ class ChatController extends State<Chat> {
if (callType == null) return; if (callType == null) return;
final success = await showFutureLoadingDialog( final success = await showFutureLoadingDialog(
context: context, context: context,
future: () => future: () =>
Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials()); Matrix.of(context).voipPlugin!.voip.requestTurnServerCredentials(),
);
if (success.result != null) { if (success.result != null) {
final voipPlugin = Matrix.of(context).voipPlugin; final voipPlugin = Matrix.of(context).voipPlugin;
try { try {

View File

@ -86,19 +86,20 @@ class ChatEventList extends StatelessWidget {
index: i - 1, index: i - 1,
controller: controller.scrollController, controller: controller.scrollController,
child: event.isVisibleInGui child: event.isVisibleInGui
? Message(event, ? Message(
event,
onSwipe: (direction) => onSwipe: (direction) =>
controller.replyAction(replyTo: event), controller.replyAction(replyTo: event),
onInfoTab: controller.showEventInfo, onInfoTab: controller.showEventInfo,
onAvatarTab: (Event event) => showAdaptiveBottomSheet( onAvatarTab: (Event event) => showAdaptiveBottomSheet(
context: context, context: context,
builder: (c) => UserBottomSheet( builder: (c) => UserBottomSheet(
user: event.senderFromMemoryOrFallback, user: event.senderFromMemoryOrFallback,
outerContext: context, outerContext: context,
onMention: () => controller.sendController.text += onMention: () => controller.sendController.text +=
'${event.senderFromMemoryOrFallback.mention} ', '${event.senderFromMemoryOrFallback.mention} ',
), ),
), ),
onSelect: controller.onSelectMessage, onSelect: controller.onSelectMessage,
scrollToEventId: (String eventId) => scrollToEventId: (String eventId) =>
controller.scrollToEventId(eventId), controller.scrollToEventId(eventId),
@ -108,7 +109,8 @@ class ChatEventList extends StatelessWidget {
timeline: controller.timeline!, timeline: controller.timeline!,
nextEvent: i < controller.timeline!.events.length nextEvent: i < controller.timeline!.events.length
? controller.timeline!.events[i] ? controller.timeline!.events[i]
: null) : null,
)
: Container(), : Container(),
); );
}, },

View File

@ -300,23 +300,24 @@ class _ChatAccountPicker extends StatelessWidget {
builder: (context, snapshot) => PopupMenuButton<String>( builder: (context, snapshot) => PopupMenuButton<String>(
onSelected: _popupMenuButtonSelected, onSelected: _popupMenuButtonSelected,
itemBuilder: (BuildContext context) => clients itemBuilder: (BuildContext context) => clients
.map((client) => PopupMenuItem<String>( .map(
value: client!.userID, (client) => PopupMenuItem<String>(
child: FutureBuilder<Profile>( value: client!.userID,
future: client.fetchOwnProfile(), child: FutureBuilder<Profile>(
builder: (context, snapshot) => ListTile( future: client.fetchOwnProfile(),
leading: Avatar( builder: (context, snapshot) => ListTile(
mxContent: snapshot.data?.avatarUrl, leading: Avatar(
name: snapshot.data?.displayName ?? mxContent: snapshot.data?.avatarUrl,
client.userID!.localpart, name: snapshot.data?.displayName ??
size: 20, client.userID!.localpart,
), size: 20,
title:
Text(snapshot.data?.displayName ?? client.userID!),
contentPadding: const EdgeInsets.all(0),
), ),
title: Text(snapshot.data?.displayName ?? client.userID!),
contentPadding: const EdgeInsets.all(0),
), ),
)) ),
),
)
.toList(), .toList(),
child: Avatar( child: Avatar(
mxContent: snapshot.data?.avatarUrl, mxContent: snapshot.data?.avatarUrl,

View File

@ -49,11 +49,12 @@ class ChatView extends StatelessWidget {
if (controller.canSaveSelectedEvent) if (controller.canSaveSelectedEvent)
// Use builder context to correctly position the share dialog on iPad // Use builder context to correctly position the share dialog on iPad
Builder( Builder(
builder: (context) => IconButton( builder: (context) => IconButton(
icon: Icon(Icons.adaptive.share), icon: Icon(Icons.adaptive.share),
tooltip: L10n.of(context)!.share, tooltip: L10n.of(context)!.share,
onPressed: () => controller.saveSelectedEvent(context), onPressed: () => controller.saveSelectedEvent(context),
)), ),
),
if (controller.canRedactSelectedEvents) if (controller.canRedactSelectedEvents)
IconButton( IconButton(
icon: const Icon(Icons.delete_outlined), icon: const Icon(Icons.delete_outlined),
@ -155,7 +156,9 @@ class ChatView extends StatelessWidget {
if (controller.room!.membership == Membership.invite) { if (controller.room!.membership == Membership.invite) {
showFutureLoadingDialog( showFutureLoadingDialog(
context: context, future: () => controller.room!.join()); context: context,
future: () => controller.room!.join(),
);
} }
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0; final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
@ -249,21 +252,23 @@ class ChatView extends StatelessWidget {
PinnedEvents(controller), PinnedEvents(controller),
Expanded( Expanded(
child: GestureDetector( child: GestureDetector(
onTap: controller.clearSingleSelectedEvent, onTap: controller.clearSingleSelectedEvent,
child: Builder( child: Builder(
builder: (context) { builder: (context) {
if (controller.timeline == null) { if (controller.timeline == null) {
return const Center( return const Center(
child: CircularProgressIndicator child:
.adaptive(strokeWidth: 2), CircularProgressIndicator.adaptive(
); strokeWidth: 2,),
}
return ChatEventList(
controller: controller,
); );
}, }
)),
return ChatEventList(
controller: controller,
);
},
),
),
), ),
if (controller.room!.canSendDefaultMessages && if (controller.room!.canSendDefaultMessages &&
controller.room!.membership == Membership.join) controller.room!.membership == Membership.join)
@ -274,7 +279,8 @@ class ChatView extends StatelessWidget {
right: bottomSheetPadding, right: bottomSheetPadding,
), ),
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 2.5), maxWidth: FluffyThemes.columnWidth * 2.5,
),
alignment: Alignment.center, alignment: Alignment.center,
child: Material( child: Material(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@ -324,7 +330,8 @@ class ChatView extends StatelessWidget {
onPressed: onPressed:
controller.recreateChat, controller.recreateChat,
label: Text( label: Text(
L10n.of(context)!.reopenChat), L10n.of(context)!.reopenChat,
),
), ),
], ],
) )

View File

@ -15,15 +15,18 @@ class EditWidgetsDialog extends StatelessWidget {
return SimpleDialog( return SimpleDialog(
title: Text(L10n.of(context)!.editWidgets), title: Text(L10n.of(context)!.editWidgets),
children: [ children: [
...room.widgets.map((e) => ListTile( ...room.widgets.map(
title: Text(e.name ?? e.type), (e) => ListTile(
leading: IconButton( title: Text(e.name ?? e.type),
onPressed: () { leading: IconButton(
room.deleteWidget(e.id!); onPressed: () {
Navigator.of(context).pop(); room.deleteWidget(e.id!);
}, Navigator.of(context).pop();
icon: const Icon(Icons.delete)), },
)), icon: const Icon(Icons.delete),
),
),
),
AddWidgetTile(room: room), AddWidgetTile(room: room),
], ],
); );

View File

@ -13,34 +13,34 @@ class EncryptionButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<SyncUpdate>( return StreamBuilder<SyncUpdate>(
stream: Matrix.of(context) stream: Matrix.of(context)
.client .client
.onSync .onSync
.stream .stream
.where((s) => s.deviceLists != null), .where((s) => s.deviceLists != null),
builder: (context, snapshot) { builder: (context, snapshot) {
return FutureBuilder<EncryptionHealthState>( return FutureBuilder<EncryptionHealthState>(
future: room.calcEncryptionHealthState(), future: room.calcEncryptionHealthState(),
builder: (BuildContext context, snapshot) => IconButton( builder: (BuildContext context, snapshot) => IconButton(
tooltip: room.encrypted tooltip: room.encrypted
? L10n.of(context)!.encrypted ? L10n.of(context)!.encrypted
: L10n.of(context)!.encryptionNotEnabled, : L10n.of(context)!.encryptionNotEnabled,
icon: Icon( icon: Icon(
room.encrypted room.encrypted ? Icons.lock_outlined : Icons.lock_open_outlined,
? Icons.lock_outlined size: 20,
: Icons.lock_open_outlined, color: room.joinRules != JoinRules.public && !room.encrypted
size: 20, ? Colors.red
color: room.joinRules != JoinRules.public && : room.joinRules != JoinRules.public &&
!room.encrypted snapshot.data ==
? Colors.red EncryptionHealthState.unverifiedDevices
: room.joinRules != JoinRules.public && ? Colors.orange
snapshot.data == : null,
EncryptionHealthState.unverifiedDevices ),
? Colors.orange onPressed: () => VRouter.of(context)
: null), .toSegments(['rooms', room.id, 'encryption']),
onPressed: () => VRouter.of(context) ),
.toSegments(['rooms', room.id, 'encryption']), );
)); },
}); );
} }
} }

View File

@ -54,7 +54,8 @@ class EventInfoDialog extends StatelessWidget {
), ),
title: Text(L10n.of(context)!.sender), title: Text(L10n.of(context)!.sender),
subtitle: Text( subtitle: Text(
'${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]'), '${event.senderFromMemoryOrFallback.calcDisplayname()} [${event.senderId}]',
),
), ),
ListTile( ListTile(
title: Text(L10n.of(context)!.time), title: Text(L10n.of(context)!.time),

View File

@ -69,7 +69,8 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
if (!kIsWeb) { if (!kIsWeb) {
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final fileName = Uri.encodeComponent( final fileName = Uri.encodeComponent(
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last); widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
);
file = File('${tempDir.path}/${fileName}_${matrixFile.name}'); file = File('${tempDir.path}/${fileName}_${matrixFile.name}');
await file.writeAsBytes(matrixFile.bytes); await file.writeAsBytes(matrixFile.bytes);
} }
@ -224,23 +225,27 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
for (var i = 0; i < AudioPlayerWidget.wavesCount; i++) for (var i = 0; i < AudioPlayerWidget.wavesCount; i++)
Expanded( Expanded(
child: InkWell( child: InkWell(
onTap: () => audioPlayer?.seek(Duration( onTap: () => audioPlayer?.seek(
Duration(
milliseconds: milliseconds:
(maxPosition / AudioPlayerWidget.wavesCount) (maxPosition / AudioPlayerWidget.wavesCount)
.round() * .round() *
i)), i,
),
),
child: Container( child: Container(
height: 32, height: 32,
alignment: Alignment.center, alignment: Alignment.center,
child: Opacity( child: Opacity(
opacity: currentPosition > i ? 1 : 0.5, opacity: currentPosition > i ? 1 : 0.5,
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 1), margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration( decoration: BoxDecoration(
color: widget.color, color: widget.color,
borderRadius: BorderRadius.circular(64), borderRadius: BorderRadius.circular(64),
), ),
height: 32 * (waveform[i] / 1024)), height: 32 * (waveform[i] / 1024),
),
), ),
), ),
), ),

View File

@ -38,9 +38,14 @@ class HtmlMessage extends StatelessWidget {
// miss-matching tags, and this way we actually correctly identify what we want to strip and, well, // miss-matching tags, and this way we actually correctly identify what we want to strip and, well,
// strip it. // strip it.
final renderHtml = html.replaceAll( final renderHtml = html.replaceAll(
RegExp('<mx-reply>.*</mx-reply>', RegExp(
caseSensitive: false, multiLine: false, dotAll: true), '<mx-reply>.*</mx-reply>',
''); caseSensitive: false,
multiLine: false,
dotAll: true,
),
'',
);
// there is no need to pre-validate the html, as we validate it while rendering // there is no need to pre-validate the html, as we validate it while rendering
@ -61,8 +66,12 @@ class HtmlMessage extends StatelessWidget {
maxLines: maxLines, maxLines: maxLines,
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(), onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
onPillTap: (url) => UrlLauncher(context, url).launchUrl(), onPillTap: (url) => UrlLauncher(context, url).launchUrl(),
getMxcUrl: (String mxc, double? width, double? height, getMxcUrl: (
{bool? animated = false}) { String mxc,
double? width,
double? height, {
bool? animated = false,
}) {
final ratio = MediaQuery.of(context).devicePixelRatio; final ratio = MediaQuery.of(context).devicePixelRatio;
return Uri.parse(mxc) return Uri.parse(mxc)
.getThumbnail( .getThumbnail(
@ -75,19 +84,23 @@ class HtmlMessage extends StatelessWidget {
.toString(); .toString();
}, },
onImageTap: (String mxc) => showDialog( onImageTap: (String mxc) => showDialog(
context: Matrix.of(context).navigatorContext, context: Matrix.of(context).navigatorContext,
useRootNavigator: false, useRootNavigator: false,
builder: (_) => ImageViewer(Event( builder: (_) => ImageViewer(
type: EventTypes.Message, Event(
content: <String, dynamic>{ type: EventTypes.Message,
'body': mxc, content: <String, dynamic>{
'url': mxc, 'body': mxc,
'msgtype': MessageTypes.Image, 'url': mxc,
}, 'msgtype': MessageTypes.Image,
senderId: room.client.userID!, },
originServerTs: DateTime.now(), senderId: room.client.userID!,
eventId: 'fake_event', originServerTs: DateTime.now(),
room: room))), eventId: 'fake_event',
room: room,
),
),
),
setCodeLanguage: (String key, String value) async { setCodeLanguage: (String key, String value) async {
await matrix.store.setItem('${SettingKeys.codeLanguage}.$key', value); await matrix.store.setItem('${SettingKeys.codeLanguage}.$key', value);
}, },

View File

@ -27,18 +27,19 @@ class Message extends StatelessWidget {
final bool selected; final bool selected;
final Timeline timeline; final Timeline timeline;
const Message(this.event, const Message(
{this.nextEvent, this.event, {
this.longPressSelect = false, this.nextEvent,
this.onSelect, this.longPressSelect = false,
this.onInfoTab, this.onSelect,
this.onAvatarTab, this.onInfoTab,
this.scrollToEventId, this.onAvatarTab,
required this.onSwipe, this.scrollToEventId,
this.selected = false, required this.onSwipe,
required this.timeline, this.selected = false,
Key? key}) required this.timeline,
: super(key: key); Key? key,
}) : super(key: key);
/// Indicates wheither the user may use a mouse instead /// Indicates wheither the user may use a mouse instead
/// of touchscreen. /// of touchscreen.
@ -126,13 +127,15 @@ class Message extends StatelessWidget {
height: 16 * AppConfig.bubbleSizeFactor, height: 16 * AppConfig.bubbleSizeFactor,
child: event.status == EventStatus.sending child: event.status == EventStatus.sending
? const CircularProgressIndicator.adaptive( ? const CircularProgressIndicator.adaptive(
strokeWidth: 2) strokeWidth: 2,
)
: event.status == EventStatus.error : event.status == EventStatus.error
? const Icon(Icons.error, color: Colors.red) ? const Icon(Icons.error, color: Colors.red)
: null, : null,
), ),
), ),
)) ),
)
: FutureBuilder<User?>( : FutureBuilder<User?>(
future: event.fetchSenderUser(), future: event.fetchSenderUser(),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -142,7 +145,8 @@ class Message extends StatelessWidget {
name: user.calcDisplayname(), name: user.calcDisplayname(),
onTap: () => onAvatarTab!(event), onTap: () => onAvatarTab!(event),
); );
}), },
),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -171,7 +175,8 @@ class Message extends StatelessWidget {
: displayname.lightColorText), : displayname.lightColorText),
), ),
); );
}), },
),
), ),
Container( Container(
alignment: alignment, alignment: alignment,
@ -198,7 +203,8 @@ class Message extends StatelessWidget {
? EdgeInsets.zero ? EdgeInsets.zero
: EdgeInsets.all(16 * AppConfig.bubbleSizeFactor), : EdgeInsets.all(16 * AppConfig.bubbleSizeFactor),
constraints: const BoxConstraints( constraints: const BoxConstraints(
maxWidth: FluffyThemes.columnWidth * 1.5), maxWidth: FluffyThemes.columnWidth * 1.5,
),
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
Column( Column(
@ -233,11 +239,14 @@ class Message extends StatelessWidget {
child: AbsorbPointer( child: AbsorbPointer(
child: Container( child: Container(
margin: EdgeInsets.symmetric( margin: EdgeInsets.symmetric(
vertical: 4.0 * vertical:
AppConfig.bubbleSizeFactor), 4.0 * AppConfig.bubbleSizeFactor,
child: ReplyContent(replyEvent, ),
ownMessage: ownMessage, child: ReplyContent(
timeline: timeline), replyEvent,
ownMessage: ownMessage,
timeline: timeline,
),
), ),
), ),
); );
@ -249,10 +258,13 @@ class Message extends StatelessWidget {
onInfoTab: onInfoTab, onInfoTab: onInfoTab,
), ),
if (event.hasAggregatedEvents( if (event.hasAggregatedEvents(
timeline, RelationshipTypes.edit)) timeline,
RelationshipTypes.edit,
))
Padding( Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: 4.0 * AppConfig.bubbleSizeFactor), top: 4.0 * AppConfig.bubbleSizeFactor,
),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -301,26 +313,29 @@ class Message extends StatelessWidget {
Padding( Padding(
padding: displayTime padding: displayTime
? EdgeInsets.symmetric( ? EdgeInsets.symmetric(
vertical: 8.0 * AppConfig.bubbleSizeFactor) vertical: 8.0 * AppConfig.bubbleSizeFactor,
)
: EdgeInsets.zero, : EdgeInsets.zero,
child: Center( child: Center(
child: Material( child: Material(
color: displayTime color: displayTime
? Theme.of(context).colorScheme.background ? Theme.of(context).colorScheme.background
: Theme.of(context) : Theme.of(context)
.colorScheme .colorScheme
.background .background
.withOpacity(0.33), .withOpacity(0.33),
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), borderRadius:
clipBehavior: Clip.antiAlias, BorderRadius.circular(AppConfig.borderRadius / 2),
child: Padding( clipBehavior: Clip.antiAlias,
padding: const EdgeInsets.all(6.0), child: Padding(
child: Text( padding: const EdgeInsets.all(6.0),
event.originServerTs.localizedTime(context), child: Text(
style: TextStyle(fontSize: 14 * AppConfig.fontSizeFactor), event.originServerTs.localizedTime(context),
style: TextStyle(fontSize: 14 * AppConfig.fontSizeFactor),
),
), ),
), ),
)), ),
), ),
row, row,
if (event.hasAggregatedEvents(timeline, RelationshipTypes.reaction)) if (event.hasAggregatedEvents(timeline, RelationshipTypes.reaction))

View File

@ -27,21 +27,27 @@ class MessageContent extends StatelessWidget {
final Color textColor; final Color textColor;
final void Function(Event)? onInfoTab; final void Function(Event)? onInfoTab;
const MessageContent(this.event, const MessageContent(
{this.onInfoTab, Key? key, required this.textColor}) this.event, {
: super(key: key); this.onInfoTab,
Key? key,
required this.textColor,
}) : super(key: key);
void _verifyOrRequestKey(BuildContext context) async { void _verifyOrRequestKey(BuildContext context) async {
final l10n = L10n.of(context)!; final l10n = L10n.of(context)!;
if (event.content['can_request_session'] != true) { if (event.content['can_request_session'] != true) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text( content: Text(
event.type == EventTypes.Encrypted event.type == EventTypes.Encrypted
? l10n.needPantalaimonWarning ? l10n.needPantalaimonWarning
: event.calcLocalizedBodyFallback( : event.calcLocalizedBodyFallback(
MatrixLocals(l10n), MatrixLocals(l10n),
), ),
))); ),
),
);
return; return;
} }
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
@ -213,73 +219,83 @@ class MessageContent extends StatelessWidget {
default: default:
if (event.redacted) { if (event.redacted) {
return FutureBuilder<User?>( return FutureBuilder<User?>(
future: event.redactedBecause?.fetchSenderUser(), future: event.redactedBecause?.fetchSenderUser(),
builder: (context, snapshot) { builder: (context, snapshot) {
return _ButtonContent( return _ButtonContent(
label: L10n.of(context)!.redactedAnEvent(snapshot.data label: L10n.of(context)!.redactedAnEvent(
?.calcDisplayname() ?? snapshot.data?.calcDisplayname() ??
event.senderFromMemoryOrFallback.calcDisplayname()), event.senderFromMemoryOrFallback.calcDisplayname(),
icon: const Icon(Icons.delete_outlined), ),
textColor: buttonTextColor, icon: const Icon(Icons.delete_outlined),
onPressed: () => onInfoTab!(event), textColor: buttonTextColor,
); onPressed: () => onInfoTab!(event),
}); );
},
);
} }
final bigEmotes = event.onlyEmotes && final bigEmotes = event.onlyEmotes &&
event.numberEmotes > 0 && event.numberEmotes > 0 &&
event.numberEmotes <= 10; event.numberEmotes <= 10;
return FutureBuilder<String>( return FutureBuilder<String>(
future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!), future: event.calcLocalizedBody(
hideReply: true), MatrixLocals(L10n.of(context)!),
builder: (context, snapshot) { hideReply: true,
return LinkText( ),
text: snapshot.data ?? builder: (context, snapshot) {
event.calcLocalizedBodyFallback( return LinkText(
MatrixLocals(L10n.of(context)!), text: snapshot.data ??
hideReply: true), event.calcLocalizedBodyFallback(
textStyle: TextStyle( MatrixLocals(L10n.of(context)!),
color: textColor, hideReply: true,
fontSize: bigEmotes ? fontSize * 3 : fontSize, ),
decoration: textStyle: TextStyle(
event.redacted ? TextDecoration.lineThrough : null, color: textColor,
), fontSize: bigEmotes ? fontSize * 3 : fontSize,
linkStyle: TextStyle( decoration:
color: textColor.withAlpha(150), event.redacted ? TextDecoration.lineThrough : null,
fontSize: bigEmotes ? fontSize * 3 : fontSize, ),
decoration: TextDecoration.underline, linkStyle: TextStyle(
decorationColor: textColor.withAlpha(150), color: textColor.withAlpha(150),
), fontSize: bigEmotes ? fontSize * 3 : fontSize,
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(), decoration: TextDecoration.underline,
); decorationColor: textColor.withAlpha(150),
}); ),
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
);
},
);
} }
case EventTypes.CallInvite: case EventTypes.CallInvite:
return FutureBuilder<User?>( return FutureBuilder<User?>(
future: event.fetchSenderUser(), future: event.fetchSenderUser(),
builder: (context, snapshot) { builder: (context, snapshot) {
return _ButtonContent( return _ButtonContent(
label: L10n.of(context)!.startedACall( label: L10n.of(context)!.startedACall(
snapshot.data?.calcDisplayname() ?? snapshot.data?.calcDisplayname() ??
event.senderFromMemoryOrFallback.calcDisplayname()), event.senderFromMemoryOrFallback.calcDisplayname(),
icon: const Icon(Icons.phone_outlined), ),
textColor: buttonTextColor, icon: const Icon(Icons.phone_outlined),
onPressed: () => onInfoTab!(event), textColor: buttonTextColor,
); onPressed: () => onInfoTab!(event),
}); );
},
);
default: default:
return FutureBuilder<User?>( return FutureBuilder<User?>(
future: event.fetchSenderUser(), future: event.fetchSenderUser(),
builder: (context, snapshot) { builder: (context, snapshot) {
return _ButtonContent( return _ButtonContent(
label: L10n.of(context)!.userSentUnknownEvent( label: L10n.of(context)!.userSentUnknownEvent(
snapshot.data?.calcDisplayname() ?? snapshot.data?.calcDisplayname() ??
event.senderFromMemoryOrFallback.calcDisplayname(), event.senderFromMemoryOrFallback.calcDisplayname(),
event.type), event.type,
icon: const Icon(Icons.info_outlined), ),
textColor: buttonTextColor, icon: const Icon(Icons.info_outlined),
onPressed: () => onInfoTab!(event), textColor: buttonTextColor,
); onPressed: () => onInfoTab!(event),
}); );
},
);
} }
} }
} }

View File

@ -46,45 +46,51 @@ class MessageReactions extends StatelessWidget {
final reactionList = reactionMap.values.toList(); final reactionList = reactionMap.values.toList();
reactionList.sort((a, b) => b.count - a.count > 0 ? 1 : -1); reactionList.sort((a, b) => b.count - a.count > 0 ? 1 : -1);
return Wrap(spacing: 4.0, runSpacing: 4.0, children: [ return Wrap(
...reactionList spacing: 4.0,
.map( runSpacing: 4.0,
(r) => _Reaction( children: [
reactionKey: r.key, ...reactionList
count: r.count, .map(
reacted: r.reacted, (r) => _Reaction(
onTap: () { reactionKey: r.key,
if (r.reacted) { count: r.count,
final evt = allReactionEvents.firstWhereOrNull((e) => reacted: r.reacted,
e.senderId == e.room.client.userID && onTap: () {
e.content['m.relates_to']['key'] == r.key); if (r.reacted) {
if (evt != null) { final evt = allReactionEvents.firstWhereOrNull(
showFutureLoadingDialog( (e) =>
context: context, e.senderId == e.room.client.userID &&
future: () => evt.redactEvent(), e.content['m.relates_to']['key'] == r.key,
); );
if (evt != null) {
showFutureLoadingDialog(
context: context,
future: () => evt.redactEvent(),
);
}
} else {
event.room.sendReaction(event.eventId, r.key!);
} }
} else { },
event.room.sendReaction(event.eventId, r.key!); onLongPress: () async => await _AdaptableReactorsDialog(
} client: client,
}, reactionEntry: r,
onLongPress: () async => await _AdaptableReactorsDialog( ).show(context),
client: client, ),
reactionEntry: r, )
).show(context), .toList(),
if (allReactionEvents.any((e) => e.status.isSending))
const SizedBox(
width: 28,
height: 28,
child: Padding(
padding: EdgeInsets.all(4.0),
child: CircularProgressIndicator.adaptive(strokeWidth: 1),
), ),
)
.toList(),
if (allReactionEvents.any((e) => e.status.isSending))
const SizedBox(
width: 28,
height: 28,
child: Padding(
padding: EdgeInsets.all(4.0),
child: CircularProgressIndicator.adaptive(strokeWidth: 1),
), ),
), ],
]); );
} }
} }
@ -121,11 +127,13 @@ class _Reaction extends StatelessWidget {
height: fontSize, height: fontSize,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text(count.toString(), Text(
style: TextStyle( count.toString(),
color: textColor, style: TextStyle(
fontSize: DefaultTextStyle.of(context).style.fontSize, color: textColor,
)), fontSize: DefaultTextStyle.of(context).style.fontSize,
),
),
], ],
); );
} else { } else {
@ -133,11 +141,13 @@ class _Reaction extends StatelessWidget {
if (renderKey.length > 10) { if (renderKey.length > 10) {
renderKey = renderKey.getRange(0, 9) + Characters(''); renderKey = renderKey.getRange(0, 9) + Characters('');
} }
content = Text('$renderKey $count', content = Text(
style: TextStyle( '$renderKey $count',
color: textColor, style: TextStyle(
fontSize: DefaultTextStyle.of(context).style.fontSize, color: textColor,
)); fontSize: DefaultTextStyle.of(context).style.fontSize,
),
);
} }
return InkWell( return InkWell(
onTap: () => onTap != null ? onTap!() : null, onTap: () => onTap != null ? onTap!() : null,

View File

@ -84,21 +84,22 @@ class ReplyContent extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
FutureBuilder<User?>( FutureBuilder<User?>(
future: displayEvent.fetchSenderUser(), future: displayEvent.fetchSenderUser(),
builder: (context, snapshot) { builder: (context, snapshot) {
return Text( return Text(
'${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:', '${snapshot.data?.calcDisplayname() ?? displayEvent.senderFromMemoryOrFallback.calcDisplayname()}:',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: ownMessage color: ownMessage
? Theme.of(context).colorScheme.onPrimary ? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.onBackground, : Theme.of(context).colorScheme.onBackground,
fontSize: fontSize, fontSize: fontSize,
), ),
); );
}), },
),
replyBody, replyBody,
], ],
), ),

View File

@ -25,22 +25,23 @@ class StateMessage extends StatelessWidget {
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
), ),
child: FutureBuilder<String>( child: FutureBuilder<String>(
future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!)), future: event.calcLocalizedBody(MatrixLocals(L10n.of(context)!)),
builder: (context, snapshot) { builder: (context, snapshot) {
return Text( return Text(
snapshot.data ?? snapshot.data ??
event.calcLocalizedBodyFallback( event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 14 * AppConfig.fontSizeFactor, fontSize: 14 * AppConfig.fontSizeFactor,
color: Theme.of(context).colorScheme.onSecondaryContainer, color: Theme.of(context).colorScheme.onSecondaryContainer,
decoration: decoration:
event.redacted ? TextDecoration.lineThrough : null, event.redacted ? TextDecoration.lineThrough : null,
), ),
); );
}), },
),
), ),
), ),
); );

View File

@ -9,9 +9,11 @@ class VerificationRequestContent extends StatelessWidget {
final Event event; final Event event;
final Timeline timeline; final Timeline timeline;
const VerificationRequestContent( const VerificationRequestContent({
{required this.event, required this.timeline, Key? key}) required this.event,
: super(key: key); required this.timeline,
Key? key,
}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -43,18 +45,22 @@ class VerificationRequestContent extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Icon(Icons.lock_outlined, Icon(
color: canceled Icons.lock_outlined,
? Colors.red color: canceled
: (fullyDone ? Colors.green : Colors.grey)), ? Colors.red
: (fullyDone ? Colors.green : Colors.grey),
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(canceled Text(
? 'Error ${cancel.first.content.tryGet<String>('code')}: ${cancel.first.content.tryGet<String>('reason')}' canceled
: (fullyDone ? 'Error ${cancel.first.content.tryGet<String>('code')}: ${cancel.first.content.tryGet<String>('reason')}'
? L10n.of(context)!.verifySuccess : (fullyDone
: (started ? L10n.of(context)!.verifySuccess
? L10n.of(context)!.loadingPleaseWait : (started
: L10n.of(context)!.newVerificationRequest))) ? L10n.of(context)!.loadingPleaseWait
: L10n.of(context)!.newVerificationRequest)),
)
], ],
), ),
), ),

View File

@ -39,7 +39,8 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
} else { } else {
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final fileName = Uri.encodeComponent( final fileName = Uri.encodeComponent(
widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last); widget.event.attachmentOrThumbnailMxcUrl()!.pathSegments.last,
);
final file = File('${tempDir.path}/${fileName}_${videoFile.name}'); final file = File('${tempDir.path}/${fileName}_${videoFile.name}');
if (await file.exists() == false) { if (await file.exists() == false) {
await file.writeAsBytes(videoFile.bytes); await file.writeAsBytes(videoFile.bytes);
@ -62,13 +63,17 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
); );
} }
} on MatrixConnectionException catch (e) { } on MatrixConnectionException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(
content: Text(e.toLocalizedString(context)), SnackBar(
)); content: Text(e.toLocalizedString(context)),
),
);
} catch (e, s) { } catch (e, s) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(
content: Text(e.toLocalizedString(context)), SnackBar(
)); content: Text(e.toLocalizedString(context)),
),
);
Logs().w('Error while playing video', e, s); Logs().w('Error while playing video', e, s);
} finally { } finally {
// Workaround for Chewie needs time to get the aspectRatio // Workaround for Chewie needs time to get the aspectRatio
@ -120,14 +125,16 @@ class EventVideoPlayerState extends State<EventVideoPlayer> {
width: 24, width: 24,
height: 24, height: 24,
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(
strokeWidth: 2), strokeWidth: 2,
),
) )
: const Icon(Icons.download_outlined), : const Icon(Icons.download_outlined),
label: Text( label: Text(
_isDownloading _isDownloading
? L10n.of(context)!.loadingPleaseWait ? L10n.of(context)!.loadingPleaseWait
: L10n.of(context)!.videoWithSize( : L10n.of(context)!.videoWithSize(
widget.event.sizeString ?? '?MB'), widget.event.sizeString ?? '?MB',
),
), ),
onPressed: _isDownloading ? null : _downloadAction, onPressed: _isDownloading ? null : _downloadAction,
), ),

View File

@ -117,8 +117,10 @@ class InputBar extends StatelessWidget {
} }
// aside of emote packs, also propose normal (tm) unicode emojis // aside of emote packs, also propose normal (tm) unicode emojis
final matchingUnicodeEmojis = Emoji.all() final matchingUnicodeEmojis = Emoji.all()
.where((element) => [element.name, ...element.keywords] .where(
.any((element) => element.toLowerCase().contains(emoteSearch))) (element) => [element.name, ...element.keywords]
.any((element) => element.toLowerCase().contains(emoteSearch)),
)
.toList(); .toList();
// sort by the index of the search term in the name in order to have // sort by the index of the search term in the name in order to have
// best matches first // best matches first
@ -186,12 +188,14 @@ class InputBar extends StatelessWidget {
.toLowerCase() .toLowerCase()
.contains(roomSearch)) || .contains(roomSearch)) ||
(state.content['alt_aliases'] is List && (state.content['alt_aliases'] is List &&
state.content['alt_aliases'].any((l) => state.content['alt_aliases'].any(
l is String && (l) =>
l l is String &&
.split(':')[0] l
.toLowerCase() .split(':')[0]
.contains(roomSearch))))) || .toLowerCase()
.contains(roomSearch),
)))) ||
(r.name.toLowerCase().contains(roomSearch))) { (r.name.toLowerCase().contains(roomSearch))) {
ret.add({ ret.add({
'type': 'room', 'type': 'room',
@ -226,8 +230,10 @@ class InputBar extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('/$command', Text(
style: const TextStyle(fontFamily: 'monospace')), '/$command',
style: const TextStyle(fontFamily: 'monospace'),
),
Text( Text(
hint, hint,
maxLines: 1, maxLines: 1,
@ -273,8 +279,8 @@ class InputBar extends StatelessWidget {
child: suggestion['pack_avatar_url'] != null child: suggestion['pack_avatar_url'] != null
? Avatar( ? Avatar(
mxContent: Uri.tryParse( mxContent: Uri.tryParse(
suggestion.tryGet<String>('pack_avatar_url') ?? suggestion.tryGet<String>('pack_avatar_url') ?? '',
''), ),
name: suggestion.tryGet<String>('pack_display_name'), name: suggestion.tryGet<String>('pack_display_name'),
size: size * 0.9, size: size * 0.9,
client: client, client: client,
@ -397,23 +403,27 @@ class InputBar extends StatelessWidget {
actions: !useShortCuts actions: !useShortCuts
? {} ? {}
: { : {
NewLineIntent: CallbackAction(onInvoke: (i) { NewLineIntent: CallbackAction(
final val = controller!.value; onInvoke: (i) {
final selection = val.selection.start; final val = controller!.value;
final messageWithoutNewLine = final selection = val.selection.start;
'${controller!.text.substring(0, val.selection.start)}\n${controller!.text.substring(val.selection.end)}'; final messageWithoutNewLine =
controller!.value = TextEditingValue( '${controller!.text.substring(0, val.selection.start)}\n${controller!.text.substring(val.selection.end)}';
text: messageWithoutNewLine, controller!.value = TextEditingValue(
selection: TextSelection.fromPosition( text: messageWithoutNewLine,
TextPosition(offset: selection + 1), selection: TextSelection.fromPosition(
), TextPosition(offset: selection + 1),
); ),
return null; );
}), return null;
SubmitLineIntent: CallbackAction(onInvoke: (i) { },
onSubmitted!(controller!.text); ),
return null; SubmitLineIntent: CallbackAction(
}), onInvoke: (i) {
onSubmitted!(controller!.text);
return null;
},
),
}, },
child: TypeAheadField<Map<String, String?>>( child: TypeAheadField<Map<String, String?>>(
direction: AxisDirection.up, direction: AxisDirection.up,

View File

@ -18,23 +18,28 @@ class PinnedEvents extends StatelessWidget {
const PinnedEvents(this.controller, {Key? key}) : super(key: key); const PinnedEvents(this.controller, {Key? key}) : super(key: key);
Future<void> _displayPinnedEventsDialog( Future<void> _displayPinnedEventsDialog(
BuildContext context, List<Event?> events) async { BuildContext context,
List<Event?> events,
) async {
final eventId = events.length == 1 final eventId = events.length == 1
? events.single?.eventId ? events.single?.eventId
: await showConfirmationDialog<String>( : await showConfirmationDialog<String>(
context: context, context: context,
title: L10n.of(context)!.pinMessage, title: L10n.of(context)!.pinMessage,
actions: events actions: events
.map((event) => AlertDialogAction( .map(
key: event?.eventId ?? '', (event) => AlertDialogAction(
label: event?.calcLocalizedBodyFallback( key: event?.eventId ?? '',
MatrixLocals(L10n.of(context)!), label: event?.calcLocalizedBodyFallback(
withSenderNamePrefix: true, MatrixLocals(L10n.of(context)!),
hideReply: true, withSenderNamePrefix: true,
) ?? hideReply: true,
'UNKNOWN', ) ??
)) 'UNKNOWN',
.toList()); ),
)
.toList(),
);
if (eventId != null) controller.scrollToEventId(eventId); if (eventId != null) controller.scrollToEventId(eventId);
} }
@ -54,87 +59,86 @@ class PinnedEvents extends StatelessWidget {
return completer; return completer;
}); });
return FutureBuilder<List<Event?>>( return FutureBuilder<List<Event?>>(
future: Future.wait(completers.map((e) => e.future).toList()), future: Future.wait(completers.map((e) => e.future).toList()),
builder: (context, snapshot) { builder: (context, snapshot) {
final pinnedEvents = snapshot.data; final pinnedEvents = snapshot.data;
final event = (pinnedEvents != null && pinnedEvents.isNotEmpty) final event = (pinnedEvents != null && pinnedEvents.isNotEmpty)
? snapshot.data?.last ? snapshot.data?.last
: null; : null;
if (event == null || pinnedEvents == null) { if (event == null || pinnedEvents == null) {
return Container(); return Container();
} }
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
return Material( return Material(
color: Theme.of(context).colorScheme.surfaceVariant, color: Theme.of(context).colorScheme.surfaceVariant,
child: InkWell( child: InkWell(
onTap: () => _displayPinnedEventsDialog( onTap: () => _displayPinnedEventsDialog(
context, context,
pinnedEvents, pinnedEvents,
), ),
child: Row( child: Row(
children: [ children: [
IconButton( IconButton(
splashRadius: 20, splashRadius: 20,
iconSize: 20, iconSize: 20,
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
icon: const Icon(Icons.push_pin), icon: const Icon(Icons.push_pin),
tooltip: L10n.of(context)!.unpin, tooltip: L10n.of(context)!.unpin,
onPressed: controller.room onPressed: controller.room
?.canSendEvent(EventTypes.RoomPinnedEvents) ?? ?.canSendEvent(EventTypes.RoomPinnedEvents) ??
false false
? () => controller.unpinEvent(event.eventId) ? () => controller.unpinEvent(event.eventId)
: null, : null,
), ),
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: FutureBuilder<String>( child: FutureBuilder<String>(
future: event.calcLocalizedBody( future: event.calcLocalizedBody(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true, withSenderNamePrefix: true,
hideReply: true, hideReply: true,
),
builder: (context, snapshot) {
return LinkText(
text: snapshot.data ??
event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true,
hideReply: true,
),
maxLines: 2,
textStyle: TextStyle(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
overflow: TextOverflow.ellipsis,
fontSize: fontSize,
decoration: event.redacted
? TextDecoration.lineThrough
: null,
), ),
builder: (context, snapshot) { linkStyle: TextStyle(
return LinkText( color:
text: snapshot.data ?? Theme.of(context).colorScheme.onSurfaceVariant,
event.calcLocalizedBodyFallback( fontSize: fontSize,
MatrixLocals(L10n.of(context)!), decoration: TextDecoration.underline,
withSenderNamePrefix: true, decorationColor:
hideReply: true, Theme.of(context).colorScheme.onSurfaceVariant,
), ),
maxLines: 2, onLinkTap: (url) =>
textStyle: TextStyle( UrlLauncher(context, url).launchUrl(),
color: Theme.of(context) );
.colorScheme },
.onSurfaceVariant,
overflow: TextOverflow.ellipsis,
fontSize: fontSize,
decoration: event.redacted
? TextDecoration.lineThrough
: null,
),
linkStyle: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
fontSize: fontSize,
decoration: TextDecoration.underline,
decorationColor: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
onLinkTap: (url) =>
UrlLauncher(context, url).launchUrl(),
);
}),
), ),
), ),
], ),
), ],
), ),
); ),
}); );
},
);
} }
} }

View File

@ -26,37 +26,45 @@ class ReactionsPicker extends StatelessWidget {
height: (display) ? 56 : 0, height: (display) ? 56 : 0,
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: Builder(builder: (context) { child: Builder(
if (!display) { builder: (context) {
return Container(); if (!display) {
} return Container();
final proposals = proposeEmojis( }
final proposals = proposeEmojis(
controller.selectedEvents.first.plaintextBody, controller.selectedEvents.first.plaintextBody,
number: 25, number: 25,
languageCodes: EmojiProposalLanguageCodes.values.toSet()); languageCodes: EmojiProposalLanguageCodes.values.toSet(),
final emojis = proposals.isNotEmpty );
? proposals.map((e) => e.char).toList() final emojis = proposals.isNotEmpty
: List<String>.from(AppEmojis.emojis); ? proposals.map((e) => e.char).toList()
final allReactionEvents = controller.selectedEvents.first : List<String>.from(AppEmojis.emojis);
.aggregatedEvents( final allReactionEvents = controller.selectedEvents.first
controller.timeline!, RelationshipTypes.reaction) .aggregatedEvents(
.where((event) => controller.timeline!,
event.senderId == event.room.client.userID && RelationshipTypes.reaction,
event.type == 'm.reaction'); )
.where(
(event) =>
event.senderId == event.room.client.userID &&
event.type == 'm.reaction',
);
for (final event in allReactionEvents) { for (final event in allReactionEvents) {
try { try {
emojis.remove(event.content['m.relates_to']['key']); emojis.remove(event.content['m.relates_to']['key']);
} catch (_) {} } catch (_) {}
} }
return Row(children: [ return Row(
Expanded( children: [
child: Container( Expanded(
child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).secondaryHeaderColor, color: Theme.of(context).secondaryHeaderColor,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
bottomRight: bottomRight: Radius.circular(AppConfig.borderRadius),
Radius.circular(AppConfig.borderRadius))), ),
),
padding: const EdgeInsets.only(right: 1), padding: const EdgeInsets.only(right: 1),
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
@ -74,23 +82,28 @@ class ReactionsPicker extends StatelessWidget {
), ),
), ),
), ),
))), ),
InkWell(
borderRadius: BorderRadius.circular(8),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
width: 36,
height: 56,
decoration: BoxDecoration(
color: Theme.of(context).secondaryHeaderColor,
shape: BoxShape.circle,
), ),
child: const Icon(Icons.add_outlined),
), ),
onTap: () => InkWell(
controller.pickEmojiReactionAction(allReactionEvents)) borderRadius: BorderRadius.circular(8),
]); child: Container(
}), margin: const EdgeInsets.symmetric(horizontal: 8),
width: 36,
height: 56,
decoration: BoxDecoration(
color: Theme.of(context).secondaryHeaderColor,
shape: BoxShape.circle,
),
child: const Icon(Icons.add_outlined),
),
onTap: () =>
controller.pickEmojiReactionAction(allReactionEvents),
)
],
);
},
),
), ),
); );
} }

View File

@ -130,7 +130,8 @@ class RecordingDialogState extends State<RecordingDialog> {
.take(26) .take(26)
.toList() .toList()
.reversed .reversed
.map((amplitude) => Container( .map(
(amplitude) => Container(
margin: const EdgeInsets.only(left: 2), margin: const EdgeInsets.only(left: 2),
width: 4, width: 4,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -138,7 +139,9 @@ class RecordingDialogState extends State<RecordingDialog> {
borderRadius: borderRadius:
BorderRadius.circular(AppConfig.borderRadius), BorderRadius.circular(AppConfig.borderRadius),
), ),
height: maxDecibalWidth * (amplitude / 100))) height: maxDecibalWidth * (amplitude / 100),
),
)
.toList(), .toList(),
), ),
), ),

View File

@ -33,10 +33,14 @@ class ReplyDisplay extends StatelessWidget {
), ),
Expanded( Expanded(
child: controller.replyEvent != null child: controller.replyEvent != null
? ReplyContent(controller.replyEvent!, ? ReplyContent(
timeline: controller.timeline!) controller.replyEvent!,
: _EditContent(controller.editEvent timeline: controller.timeline!,
?.getDisplayEvent(controller.timeline!)), )
: _EditContent(
controller.editEvent
?.getDisplayEvent(controller.timeline!),
),
), ),
], ],
), ),
@ -64,26 +68,27 @@ class _EditContent extends StatelessWidget {
), ),
Container(width: 15.0), Container(width: 15.0),
FutureBuilder<String>( FutureBuilder<String>(
future: event.calcLocalizedBody( future: event.calcLocalizedBody(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false, withSenderNamePrefix: false,
hideReply: true, hideReply: true,
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
return Text( return Text(
snapshot.data ?? snapshot.data ??
event.calcLocalizedBodyFallback( event.calcLocalizedBodyFallback(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false, withSenderNamePrefix: false,
hideReply: true, hideReply: true,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
color: Theme.of(context).textTheme.bodyMedium!.color, color: Theme.of(context).textTheme.bodyMedium!.color,
), ),
); );
}), },
),
], ],
); );
} }

View File

@ -33,11 +33,12 @@ class SendFileDialogState extends State<SendFileDialog> {
MatrixImageFile? thumbnail; MatrixImageFile? thumbnail;
if (file is MatrixVideoFile && file.bytes.length > minSizeToCompress) { if (file is MatrixVideoFile && file.bytes.length > minSizeToCompress) {
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
file = await file.resizeVideo(); file = await file.resizeVideo();
thumbnail = await file.getVideoThumbnail(); thumbnail = await file.getVideoThumbnail();
}); },
);
} }
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
widget.room widget.room
@ -79,26 +80,29 @@ class SendFileDialogState extends State<SendFileDialog> {
} }
Widget contentWidget; Widget contentWidget;
if (allFilesAreImages) { if (allFilesAreImages) {
contentWidget = Column(mainAxisSize: MainAxisSize.min, children: <Widget>[ contentWidget = Column(
Flexible( mainAxisSize: MainAxisSize.min,
child: Image.memory( children: <Widget>[
widget.files.first.bytes, Flexible(
fit: BoxFit.contain, child: Image.memory(
widget.files.first.bytes,
fit: BoxFit.contain,
),
), ),
), Row(
Row( children: <Widget>[
children: <Widget>[ Checkbox(
Checkbox( value: origImage,
value: origImage, onChanged: (v) => setState(() => origImage = v ?? false),
onChanged: (v) => setState(() => origImage = v ?? false), ),
), InkWell(
InkWell( onTap: () => setState(() => origImage = !origImage),
onTap: () => setState(() => origImage = !origImage), child: Text('${L10n.of(context)!.sendOriginal} ($sizeString)'),
child: Text('${L10n.of(context)!.sendOriginal} ($sizeString)'), ),
), ],
], )
) ],
]); );
} else { } else {
contentWidget = Text('$fileName ($sizeString)'); contentWidget = Text('$fileName ($sizeString)');
} }

View File

@ -28,12 +28,13 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
final pack = stickerPacks[packSlugs[packIndex]]!; final pack = stickerPacks[packSlugs[packIndex]]!;
final filteredImagePackImageEntried = pack.images.entries.toList(); final filteredImagePackImageEntried = pack.images.entries.toList();
if (searchFilter?.isNotEmpty ?? false) { if (searchFilter?.isNotEmpty ?? false) {
filteredImagePackImageEntried.removeWhere((e) => filteredImagePackImageEntried.removeWhere(
!(e.key.toLowerCase().contains(searchFilter!.toLowerCase()) || (e) => !(e.key.toLowerCase().contains(searchFilter!.toLowerCase()) ||
(e.value.body (e.value.body
?.toLowerCase() ?.toLowerCase()
.contains(searchFilter!.toLowerCase()) ?? .contains(searchFilter!.toLowerCase()) ??
false))); false)),
);
} }
final imageKeys = final imageKeys =
filteredImagePackImageEntried.map((e) => e.key).toList(); filteredImagePackImageEntried.map((e) => e.key).toList();
@ -57,7 +58,8 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
GridView.builder( GridView.builder(
itemCount: imageKeys.length, itemCount: imageKeys.length,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 100), maxCrossAxisExtent: 100,
),
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int imageIndex) { itemBuilder: (BuildContext context, int imageIndex) {
@ -127,10 +129,11 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
), ),
), ),
SliverList( SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
packBuilder, packBuilder,
childCount: packSlugs.length, childCount: packSlugs.length,
)), ),
),
], ],
), ),
), ),

View File

@ -58,7 +58,8 @@ class ChatDetailsController extends State<ChatDetails> {
); );
if (success.error == null) { if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context)!.displaynameHasBeenChanged))); SnackBar(content: Text(L10n.of(context)!.displaynameHasBeenChanged)),
);
} }
} }
@ -212,8 +213,11 @@ class ChatDetailsController extends State<ChatDetails> {
future: () => room.setDescription(input.single), future: () => room.setDescription(input.single),
); );
if (success.error == null) { if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(
content: Text(L10n.of(context)!.groupDescriptionHasBeenChanged))); SnackBar(
content: Text(L10n.of(context)!.groupDescriptionHasBeenChanged),
),
);
} }
} }
@ -325,7 +329,9 @@ class ChatDetailsController extends State<ChatDetails> {
void requestMoreMembersAction() async { void requestMoreMembersAction() async {
final room = Matrix.of(context).client.getRoomById(roomId!); final room = Matrix.of(context).client.getRoomById(roomId!);
final participants = await showFutureLoadingDialog( final participants = await showFutureLoadingDialog(
context: context, future: () => room!.requestParticipants()); context: context,
future: () => room!.requestParticipants(),
);
if (participants.error == null) { if (participants.error == null) {
setState(() => members = participants.result); setState(() => members = participants.result);
} }

View File

@ -43,364 +43,400 @@ class ChatDetailsView extends StatelessWidget {
controller.members!.length < actualMembersCount; controller.members!.length < actualMembersCount;
final iconColor = Theme.of(context).textTheme.bodyLarge!.color; final iconColor = Theme.of(context).textTheme.bodyLarge!.color;
return StreamBuilder( return StreamBuilder(
stream: room.onUpdate.stream, stream: room.onUpdate.stream,
builder: (context, snapshot) { builder: (context, snapshot) {
return Scaffold( return Scaffold(
body: NestedScrollView( body: NestedScrollView(
headerSliverBuilder: headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) => <Widget>[ (BuildContext context, bool innerBoxIsScrolled) => <Widget>[
SliverAppBar( SliverAppBar(
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close_outlined), icon: const Icon(Icons.close_outlined),
onPressed: () => onPressed: () =>
VRouter.of(context).path.startsWith('/spaces/') VRouter.of(context).path.startsWith('/spaces/')
? VRouter.of(context).pop() ? VRouter.of(context).pop()
: VRouter.of(context) : VRouter.of(context)
.toSegments(['rooms', controller.roomId!]), .toSegments(['rooms', controller.roomId!]),
), ),
elevation: Theme.of(context).appBarTheme.elevation, elevation: Theme.of(context).appBarTheme.elevation,
expandedHeight: 300.0, expandedHeight: 300.0,
floating: true, floating: true,
pinned: true, pinned: true,
actions: <Widget>[ actions: <Widget>[
if (room.canonicalAlias.isNotEmpty) if (room.canonicalAlias.isNotEmpty)
IconButton( IconButton(
tooltip: L10n.of(context)!.share, tooltip: L10n.of(context)!.share,
icon: Icon(Icons.adaptive.share_outlined), icon: Icon(Icons.adaptive.share_outlined),
onPressed: () => FluffyShare.share( onPressed: () => FluffyShare.share(
AppConfig.inviteLinkPrefix + room.canonicalAlias, AppConfig.inviteLinkPrefix + room.canonicalAlias,
context), context,
), ),
ChatSettingsPopupMenu(room, false)
],
title: Text(
room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!)),
),
backgroundColor:
Theme.of(context).appBarTheme.backgroundColor,
flexibleSpace: FlexibleSpaceBar(
background: ContentBanner(
mxContent: room.avatar,
onEdit: room.canSendEvent('m.room.avatar')
? controller.setAvatarAction
: null,
defaultIcon: Icons.group_outlined,
), ),
ChatSettingsPopupMenu(room, false)
],
title: Text(
room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
), ),
), ),
], backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
body: MaxWidthBody( flexibleSpace: FlexibleSpaceBar(
child: ListView.builder( background: ContentBanner(
itemCount: controller.members!.length + mxContent: room.avatar,
1 + onEdit: room.canSendEvent('m.room.avatar')
(canRequestMoreMembers ? 1 : 0), ? controller.setAvatarAction
itemBuilder: (BuildContext context, int i) => i == 0 : null,
? Column( defaultIcon: Icons.group_outlined,
crossAxisAlignment: CrossAxisAlignment.stretch, ),
children: <Widget>[ ),
ListTile( ),
onTap: room.canSendEvent(EventTypes.RoomTopic) ],
? controller.setTopicAction body: MaxWidthBody(
: null, child: ListView.builder(
trailing: room.canSendEvent(EventTypes.RoomTopic) itemCount: controller.members!.length +
? Icon( 1 +
Icons.edit_outlined, (canRequestMoreMembers ? 1 : 0),
color: Theme.of(context) itemBuilder: (BuildContext context, int i) => i == 0
.colorScheme ? Column(
.onBackground, crossAxisAlignment: CrossAxisAlignment.stretch,
) children: <Widget>[
: null, ListTile(
title: Text( onTap: room.canSendEvent(EventTypes.RoomTopic)
L10n.of(context)!.groupDescription, ? controller.setTopicAction
style: TextStyle( : null,
color: trailing: room.canSendEvent(EventTypes.RoomTopic)
Theme.of(context).colorScheme.secondary, ? Icon(
fontWeight: FontWeight.bold, Icons.edit_outlined,
),
),
),
if (room.topic.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0),
child: LinkText(
text: room.topic.isEmpty
? L10n.of(context)!.addGroupDescription
: room.topic,
linkStyle:
const TextStyle(color: Colors.blueAccent),
textStyle: TextStyle(
fontSize: 14,
color: Theme.of(context) color: Theme.of(context)
.textTheme .colorScheme
.bodyMedium! .onBackground,
.color, )
decorationColor: Theme.of(context) : null,
.textTheme title: Text(
.bodyMedium! L10n.of(context)!.groupDescription,
.color, style: TextStyle(
), color: Theme.of(context).colorScheme.secondary,
onLinkTap: (url) => fontWeight: FontWeight.bold,
UrlLauncher(context, url).launchUrl(),
),
), ),
const SizedBox(height: 8),
const Divider(height: 1),
ListTile(
title: Text(
L10n.of(context)!.settings,
style: TextStyle(
color:
Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
trailing: Icon(controller.displaySettings
? Icons.keyboard_arrow_down_outlined
: Icons.keyboard_arrow_right_outlined),
onTap: controller.toggleDisplaySettings,
), ),
if (controller.displaySettings) ...[ ),
if (room.canSendEvent('m.room.name')) if (room.topic.isNotEmpty)
ListTile( Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: LinkText(
text: room.topic.isEmpty
? L10n.of(context)!.addGroupDescription
: room.topic,
linkStyle:
const TextStyle(color: Colors.blueAccent),
textStyle: TextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.bodyMedium!
.color,
decorationColor: Theme.of(context)
.textTheme
.bodyMedium!
.color,
),
onLinkTap: (url) =>
UrlLauncher(context, url).launchUrl(),
),
),
const SizedBox(height: 8),
const Divider(height: 1),
ListTile(
title: Text(
L10n.of(context)!.settings,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
trailing: Icon(
controller.displaySettings
? Icons.keyboard_arrow_down_outlined
: Icons.keyboard_arrow_right_outlined,
),
onTap: controller.toggleDisplaySettings,
),
if (controller.displaySettings) ...[
if (room.canSendEvent('m.room.name'))
ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(
Icons.people_outline_outlined,
),
),
title: Text(
L10n.of(context)!.changeTheNameOfTheGroup,
),
subtitle: Text(
room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
),
),
onTap: controller.setDisplaynameAction,
),
if (room.joinRules == JoinRules.public)
ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(Icons.link_outlined),
),
onTap: controller.editAliases,
title: Text(L10n.of(context)!.editRoomAliases),
subtitle: Text(
(room.canonicalAlias.isNotEmpty)
? room.canonicalAlias
: L10n.of(context)!.none,
),
),
ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(
Icons.insert_emoticon_outlined,
),
),
title: Text(L10n.of(context)!.emoteSettings),
subtitle: Text(L10n.of(context)!.setCustomEmotes),
onTap: controller.goToEmoteSettings,
),
PopupMenuButton(
onSelected: controller.setJoinRulesAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<JoinRules>>[
if (room.canChangeJoinRules)
PopupMenuItem<JoinRules>(
value: JoinRules.public,
child: Text(
JoinRules.public.getLocalizedString(
MatrixLocals(L10n.of(context)!),
),
),
),
if (room.canChangeJoinRules)
PopupMenuItem<JoinRules>(
value: JoinRules.invite,
child: Text(
JoinRules.invite.getLocalizedString(
MatrixLocals(L10n.of(context)!),
),
),
),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(Icons.shield_outlined),
),
title: Text(
L10n.of(context)!.whoIsAllowedToJoinThisGroup,
),
subtitle: Text(
room.joinRules?.getLocalizedString(
MatrixLocals(L10n.of(context)!),
) ??
L10n.of(context)!.none,
),
),
),
PopupMenuButton(
onSelected: controller.setHistoryVisibilityAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<HistoryVisibility>>[
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.invited,
child: Text(
HistoryVisibility.invited
.getLocalizedString(
MatrixLocals(L10n.of(context)!),
),
),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.joined,
child: Text(
HistoryVisibility.joined
.getLocalizedString(
MatrixLocals(L10n.of(context)!),
),
),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.shared,
child: Text(
HistoryVisibility.shared
.getLocalizedString(
MatrixLocals(L10n.of(context)!),
),
),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.worldReadable,
child: Text(
HistoryVisibility.worldReadable
.getLocalizedString(
MatrixLocals(L10n.of(context)!),
),
),
),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(Icons.visibility_outlined),
),
title: Text(
L10n.of(context)!.visibilityOfTheChatHistory,
),
subtitle: Text(
room.historyVisibility?.getLocalizedString(
MatrixLocals(L10n.of(context)!),
) ??
L10n.of(context)!.none,
),
),
),
if (room.joinRules == JoinRules.public)
PopupMenuButton(
onSelected: controller.setGuestAccessAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<GuestAccess>>[
if (room.canChangeGuestAccess)
PopupMenuItem<GuestAccess>(
value: GuestAccess.canJoin,
child: Text(
GuestAccess.canJoin.getLocalizedString(
MatrixLocals(
L10n.of(context)!,
),
),
),
),
if (room.canChangeGuestAccess)
PopupMenuItem<GuestAccess>(
value: GuestAccess.forbidden,
child: Text(
GuestAccess.forbidden
.getLocalizedString(
MatrixLocals(
L10n.of(context)!,
),
),
),
),
],
child: ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: Theme.of(context) backgroundColor: Theme.of(context)
.scaffoldBackgroundColor, .scaffoldBackgroundColor,
foregroundColor: iconColor, foregroundColor: iconColor,
child: const Icon( child: const Icon(
Icons.people_outline_outlined), Icons.person_add_alt_1_outlined,
),
), ),
title: Text(L10n.of(context)! title: Text(
.changeTheNameOfTheGroup), L10n.of(context)!.areGuestsAllowedToJoin,
subtitle: Text(room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!))),
onTap: controller.setDisplaynameAction,
),
if (room.joinRules == JoinRules.public)
ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(Icons.link_outlined),
), ),
onTap: controller.editAliases,
title:
Text(L10n.of(context)!.editRoomAliases),
subtitle: Text( subtitle: Text(
(room.canonicalAlias.isNotEmpty) room.guestAccess.getLocalizedString(
? room.canonicalAlias MatrixLocals(L10n.of(context)!),
: L10n.of(context)!.none),
),
ListTile(
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(
Icons.insert_emoticon_outlined),
),
title: Text(L10n.of(context)!.emoteSettings),
subtitle:
Text(L10n.of(context)!.setCustomEmotes),
onTap: controller.goToEmoteSettings,
),
PopupMenuButton(
onSelected: controller.setJoinRulesAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<JoinRules>>[
if (room.canChangeJoinRules)
PopupMenuItem<JoinRules>(
value: JoinRules.public,
child: Text(JoinRules.public
.getLocalizedString(
MatrixLocals(L10n.of(context)!))),
),
if (room.canChangeJoinRules)
PopupMenuItem<JoinRules>(
value: JoinRules.invite,
child: Text(JoinRules.invite
.getLocalizedString(
MatrixLocals(L10n.of(context)!))),
),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(Icons.shield_outlined)),
title: Text(L10n.of(context)!
.whoIsAllowedToJoinThisGroup),
subtitle: Text(
room.joinRules?.getLocalizedString(
MatrixLocals(L10n.of(context)!)) ??
L10n.of(context)!.none,
),
),
),
PopupMenuButton(
onSelected:
controller.setHistoryVisibilityAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<HistoryVisibility>>[
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.invited,
child: Text(HistoryVisibility.invited
.getLocalizedString(
MatrixLocals(L10n.of(context)!))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.joined,
child: Text(HistoryVisibility.joined
.getLocalizedString(
MatrixLocals(L10n.of(context)!))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.shared,
child: Text(HistoryVisibility.shared
.getLocalizedString(
MatrixLocals(L10n.of(context)!))),
),
if (room.canChangeHistoryVisibility)
PopupMenuItem<HistoryVisibility>(
value: HistoryVisibility.worldReadable,
child: Text(HistoryVisibility
.worldReadable
.getLocalizedString(
MatrixLocals(L10n.of(context)!))),
),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: iconColor,
child:
const Icon(Icons.visibility_outlined),
),
title: Text(L10n.of(context)!
.visibilityOfTheChatHistory),
subtitle: Text(
room.historyVisibility?.getLocalizedString(
MatrixLocals(L10n.of(context)!)) ??
L10n.of(context)!.none,
),
),
),
if (room.joinRules == JoinRules.public)
PopupMenuButton(
onSelected: controller.setGuestAccessAction,
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<GuestAccess>>[
if (room.canChangeGuestAccess)
PopupMenuItem<GuestAccess>(
value: GuestAccess.canJoin,
child: Text(
GuestAccess.canJoin
.getLocalizedString(MatrixLocals(
L10n.of(context)!)),
),
),
if (room.canChangeGuestAccess)
PopupMenuItem<GuestAccess>(
value: GuestAccess.forbidden,
child: Text(
GuestAccess.forbidden
.getLocalizedString(MatrixLocals(
L10n.of(context)!)),
),
),
],
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context)
.scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(
Icons.person_add_alt_1_outlined),
),
title: Text(L10n.of(context)!
.areGuestsAllowedToJoin),
subtitle: Text(
room.guestAccess.getLocalizedString(
MatrixLocals(L10n.of(context)!)),
), ),
), ),
), ),
ListTile(
title:
Text(L10n.of(context)!.editChatPermissions),
subtitle: Text(
L10n.of(context)!.whoCanPerformWhichAction),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon(
Icons.edit_attributes_outlined),
),
onTap: () =>
VRouter.of(context).to('permissions'),
), ),
],
const Divider(height: 1),
ListTile( ListTile(
title: Text( title:
actualMembersCount > 1 Text(L10n.of(context)!.editChatPermissions),
? L10n.of(context)!.countParticipants( subtitle: Text(
actualMembersCount.toString()) L10n.of(context)!.whoCanPerformWhichAction,
: L10n.of(context)!.emptyChat,
style: TextStyle(
color:
Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
), ),
),
room.canInvite
? ListTile(
title:
Text(L10n.of(context)!.inviteContact),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).primaryColor,
foregroundColor: Colors.white,
radius: Avatar.defaultSize / 2,
child: const Icon(Icons.add_outlined),
),
onTap: () =>
VRouter.of(context).to('invite'),
)
: Container(),
],
)
: i < controller.members!.length + 1
? ParticipantListItem(controller.members![i - 1])
: ListTile(
title: Text(L10n.of(context)!
.loadCountMoreParticipants(
(actualMembersCount -
controller.members!.length)
.toString())),
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: backgroundColor:
Theme.of(context).scaffoldBackgroundColor, Theme.of(context).scaffoldBackgroundColor,
foregroundColor: iconColor,
child: const Icon( child: const Icon(
Icons.refresh, Icons.edit_attributes_outlined,
color: Colors.grey,
), ),
), ),
onTap: controller.requestMoreMembersAction, onTap: () =>
VRouter.of(context).to('permissions'),
), ),
), ],
const Divider(height: 1),
ListTile(
title: Text(
actualMembersCount > 1
? L10n.of(context)!.countParticipants(
actualMembersCount.toString(),
)
: L10n.of(context)!.emptyChat,
style: TextStyle(
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
),
),
),
room.canInvite
? ListTile(
title: Text(L10n.of(context)!.inviteContact),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).primaryColor,
foregroundColor: Colors.white,
radius: Avatar.defaultSize / 2,
child: const Icon(Icons.add_outlined),
),
onTap: () => VRouter.of(context).to('invite'),
)
: Container(),
],
)
: i < controller.members!.length + 1
? ParticipantListItem(controller.members![i - 1])
: ListTile(
title: Text(
L10n.of(context)!.loadCountMoreParticipants(
(actualMembersCount -
controller.members!.length)
.toString(),
),
),
leading: CircleAvatar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
child: const Icon(
Icons.refresh,
color: Colors.grey,
),
),
onTap: controller.requestMoreMembersAction,
),
), ),
), ),
); ),
}); );
},
);
} }
} }

View File

@ -47,11 +47,12 @@ class ParticipantListItem extends StatelessWidget {
), ),
margin: const EdgeInsets.symmetric(horizontal: 8), margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer, color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
)), ),
),
child: Text( child: Text(
permissionBatch, permissionBatch,
style: TextStyle( style: TextStyle(

View File

@ -19,179 +19,180 @@ class ChatEncryptionSettingsView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final room = controller.room; final room = controller.room;
return StreamBuilder<Object>( return StreamBuilder<Object>(
stream: room.client.onSync.stream.where( stream: room.client.onSync.stream.where(
(s) => s.rooms?.join?[room.id] != null || s.deviceLists != null), (s) => s.rooms?.join?[room.id] != null || s.deviceLists != null,
builder: (context, _) => Scaffold( ),
appBar: AppBar( builder: (context, _) => Scaffold(
leading: IconButton( appBar: AppBar(
icon: const Icon(Icons.close_outlined), leading: IconButton(
onPressed: () => VRouter.of(context) icon: const Icon(Icons.close_outlined),
.toSegments(['rooms', controller.roomId!]), onPressed: () =>
), VRouter.of(context).toSegments(['rooms', controller.roomId!]),
title: Text(L10n.of(context)!.endToEndEncryption), ),
actions: [ title: Text(L10n.of(context)!.endToEndEncryption),
TextButton( actions: [
onPressed: () => TextButton(
launchUrlString(AppConfig.encryptionTutorial), onPressed: () => launchUrlString(AppConfig.encryptionTutorial),
child: Text(L10n.of(context)!.help), child: Text(L10n.of(context)!.help),
), ),
], ],
),
body: ListView(
children: [
SwitchListTile(
secondary: CircleAvatar(
foregroundColor:
Theme.of(context).colorScheme.onPrimaryContainer,
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
child: const Icon(Icons.lock_outlined),
), ),
body: ListView( title: Text(L10n.of(context)!.encryptThisChat),
children: [ value: room.encrypted,
SwitchListTile( onChanged: controller.enableEncryption,
secondary: CircleAvatar( ),
foregroundColor: Center(
Theme.of(context).colorScheme.onPrimaryContainer, child: Image.asset(
backgroundColor: 'assets/encryption.png',
Theme.of(context).colorScheme.primaryContainer, width: 212,
child: const Icon(Icons.lock_outlined)), ),
title: Text(L10n.of(context)!.encryptThisChat), ),
value: room.encrypted, const Divider(height: 1),
onChanged: controller.enableEncryption, if (room.isDirectChat)
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: controller.startVerification,
icon: const Icon(Icons.verified_outlined),
label: Text(L10n.of(context)!.verifyStart),
), ),
Center( ),
child: Image.asset( ),
'assets/encryption.png', if (room.encrypted) ...[
width: 212, const SizedBox(height: 16),
), ListTile(
title: Text(
L10n.of(context)!.deviceKeys,
style: const TextStyle(
fontWeight: FontWeight.bold,
), ),
const Divider(height: 1), ),
if (room.isDirectChat) ),
Padding( StreamBuilder(
padding: const EdgeInsets.all(16.0), stream: room.onUpdate.stream,
child: SizedBox( builder: (context, snapshot) => FutureBuilder<List<DeviceKeys>>(
width: double.infinity, future: room.getUserDeviceKeys(),
child: ElevatedButton.icon( builder: (BuildContext context, snapshot) {
onPressed: controller.startVerification, if (snapshot.hasError) {
icon: const Icon(Icons.verified_outlined), return Center(
label: Text(L10n.of(context)!.verifyStart), child: Text(
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}',
), ),
), );
), }
if (room.encrypted) ...[ if (!snapshot.hasData) {
const SizedBox(height: 16), return const Center(
ListTile( child: CircularProgressIndicator.adaptive(
title: Text( strokeWidth: 2,
L10n.of(context)!.deviceKeys,
style: const TextStyle(
fontWeight: FontWeight.bold,
), ),
), );
), }
StreamBuilder( final deviceKeys = snapshot.data!;
stream: room.onUpdate.stream, return ListView.builder(
builder: (context, snapshot) => FutureBuilder< shrinkWrap: true,
List<DeviceKeys>>( physics: const NeverScrollableScrollPhysics(),
future: room.getUserDeviceKeys(), itemCount: deviceKeys.length,
builder: (BuildContext context, snapshot) { itemBuilder: (BuildContext context, int i) =>
if (snapshot.hasError) { SwitchListTile(
return Center( value: !deviceKeys[i].blocked,
child: Text( activeColor: deviceKeys[i].verified
'${L10n.of(context)!.oopsSomethingWentWrong}: ${snapshot.error}'), ? Colors.green
); : Colors.orange,
} onChanged: (_) =>
if (!snapshot.hasData) { controller.toggleDeviceKey(deviceKeys[i]),
return const Center( title: Row(
child: CircularProgressIndicator.adaptive( children: [
strokeWidth: 2)); Icon(
} deviceKeys[i].verified
final deviceKeys = snapshot.data!; ? Icons.verified_outlined
return ListView.builder( : deviceKeys[i].blocked
shrinkWrap: true, ? Icons.block_outlined
physics: const NeverScrollableScrollPhysics(), : Icons.info_outlined,
itemCount: deviceKeys.length, color: deviceKeys[i].verified
itemBuilder: (BuildContext context, int i) => ? Colors.green
SwitchListTile( : deviceKeys[i].blocked
value: !deviceKeys[i].blocked, ? Colors.red
activeColor: deviceKeys[i].verified : Colors.orange,
? Colors.green size: 20,
: Colors.orange, ),
onChanged: (_) => const SizedBox(width: 4),
controller.toggleDeviceKey(deviceKeys[i]), Text(
title: Row( deviceKeys[i].deviceId ??
children: [ L10n.of(context)!.unknownDevice,
Icon( ),
deviceKeys[i].verified const SizedBox(width: 4),
? Icons.verified_outlined Flexible(
: deviceKeys[i].blocked fit: FlexFit.loose,
? Icons.block_outlined child: Material(
: Icons.info_outlined, shape: RoundedRectangleBorder(
color: deviceKeys[i].verified borderRadius: BorderRadius.circular(
? Colors.green AppConfig.borderRadius,
: deviceKeys[i].blocked ),
? Colors.red side: BorderSide(
: Colors.orange,
size: 20,
),
const SizedBox(width: 4),
Text(
deviceKeys[i].deviceId ??
L10n.of(context)!.unknownDevice,
),
const SizedBox(width: 4),
Flexible(
fit: FlexFit.loose,
child: Material(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius),
side: BorderSide(
color: Theme.of(context)
.colorScheme
.primary,
),
),
color: Theme.of(context)
.colorScheme
.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Text(
deviceKeys[i].userId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
fontSize: 12,
fontStyle: FontStyle.italic,
),
),
),
),
),
],
),
subtitle: Text(
deviceKeys[i].ed25519Key?.beautified ??
L10n.of(context)!
.unknownEncryptionAlgorithm,
style: TextStyle(
fontFamily: 'RobotoMono',
color: color:
Theme.of(context).colorScheme.secondary, Theme.of(context).colorScheme.primary,
),
),
color: Theme.of(context)
.colorScheme
.primaryContainer,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Text(
deviceKeys[i].userId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color:
Theme.of(context).colorScheme.primary,
fontSize: 12,
fontStyle: FontStyle.italic,
),
), ),
), ),
), ),
); ),
}), ],
), ),
] else subtitle: Text(
Padding( deviceKeys[i].ed25519Key?.beautified ??
padding: const EdgeInsets.all(16.0), L10n.of(context)!.unknownEncryptionAlgorithm,
child: Center( style: TextStyle(
child: Text( fontFamily: 'RobotoMono',
L10n.of(context)!.encryptionNotEnabled, color: Theme.of(context).colorScheme.secondary,
style: const TextStyle(
fontStyle: FontStyle.italic,
), ),
), ),
), ),
), );
], },
),
), ),
)); ] else
Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Text(
L10n.of(context)!.encryptionNotEnabled,
style: const TextStyle(
fontStyle: FontStyle.italic,
),
),
),
),
],
),
),
);
} }
} }

View File

@ -164,19 +164,21 @@ class ChatListController extends State<ChatList>
void setServer() async { void setServer() async {
final newServer = await showTextInputDialog( final newServer = await showTextInputDialog(
useRootNavigator: false, useRootNavigator: false,
title: L10n.of(context)!.changeTheHomeserver, title: L10n.of(context)!.changeTheHomeserver,
context: context, context: context,
okLabel: L10n.of(context)!.ok, okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel, cancelLabel: L10n.of(context)!.cancel,
textFields: [ textFields: [
DialogTextField( DialogTextField(
prefixText: 'https://', prefixText: 'https://',
hintText: Matrix.of(context).client.homeserver?.host, hintText: Matrix.of(context).client.homeserver?.host,
initialText: searchServer, initialText: searchServer,
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
autocorrect: false) autocorrect: false,
]); )
],
);
if (newServer == null) return; if (newServer == null) return;
Store().setItem(_serverStoreNamespace, newServer.single); Store().setItem(_serverStoreNamespace, newServer.single);
setState(() { setState(() {
@ -382,9 +384,11 @@ class ChatListController extends State<ChatList>
} }
void toggleSelection(String roomId) { void toggleSelection(String roomId) {
setState(() => selectedRoomIds.contains(roomId) setState(
? selectedRoomIds.remove(roomId) () => selectedRoomIds.contains(roomId)
: selectedRoomIds.add(roomId)); ? selectedRoomIds.remove(roomId)
: selectedRoomIds.add(roomId),
);
} }
Future<void> toggleUnread() async { Future<void> toggleUnread() async {
@ -456,16 +460,17 @@ class ChatListController extends State<ChatList>
void setStatus() async { void setStatus() async {
final input = await showTextInputDialog( final input = await showTextInputDialog(
useRootNavigator: false, useRootNavigator: false,
context: context, context: context,
title: L10n.of(context)!.setStatus, title: L10n.of(context)!.setStatus,
okLabel: L10n.of(context)!.ok, okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel, cancelLabel: L10n.of(context)!.cancel,
textFields: [ textFields: [
DialogTextField( DialogTextField(
hintText: L10n.of(context)!.statusExampleMessage, hintText: L10n.of(context)!.statusExampleMessage,
), ),
]); ],
);
if (input == null) return; if (input == null) return;
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
@ -491,22 +496,23 @@ class ChatListController extends State<ChatList>
Future<void> addToSpace() async { Future<void> addToSpace() async {
final selectedSpace = await showConfirmationDialog<String>( final selectedSpace = await showConfirmationDialog<String>(
context: context, context: context,
title: L10n.of(context)!.addToSpace, title: L10n.of(context)!.addToSpace,
message: L10n.of(context)!.addToSpaceDescription, message: L10n.of(context)!.addToSpaceDescription,
fullyCapitalizedForMaterial: false, fullyCapitalizedForMaterial: false,
actions: Matrix.of(context) actions: Matrix.of(context)
.client .client
.rooms .rooms
.where((r) => r.isSpace) .where((r) => r.isSpace)
.map( .map(
(space) => AlertDialogAction( (space) => AlertDialogAction(
key: space.id, key: space.id,
label: space label: space
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
), ),
) )
.toList()); .toList(),
);
if (selectedSpace == null) return; if (selectedSpace == null) return;
final result = await showFutureLoadingDialog( final result = await showFutureLoadingDialog(
context: context, context: context,
@ -532,14 +538,19 @@ class ChatListController extends State<ChatList>
} }
bool get anySelectedRoomNotMarkedUnread => selectedRoomIds.any( bool get anySelectedRoomNotMarkedUnread => selectedRoomIds.any(
(roomId) => !Matrix.of(context).client.getRoomById(roomId)!.markedUnread); (roomId) =>
!Matrix.of(context).client.getRoomById(roomId)!.markedUnread,
);
bool get anySelectedRoomNotFavorite => selectedRoomIds.any( bool get anySelectedRoomNotFavorite => selectedRoomIds.any(
(roomId) => !Matrix.of(context).client.getRoomById(roomId)!.isFavourite); (roomId) => !Matrix.of(context).client.getRoomById(roomId)!.isFavourite,
);
bool get anySelectedRoomNotMuted => selectedRoomIds.any((roomId) => bool get anySelectedRoomNotMuted => selectedRoomIds.any(
Matrix.of(context).client.getRoomById(roomId)!.pushRuleState == (roomId) =>
PushRuleState.notify); Matrix.of(context).client.getRoomById(roomId)!.pushRuleState ==
PushRuleState.notify,
);
bool waitForFirstSync = false; bool waitForFirstSync = false;
@ -624,9 +635,10 @@ class ChatListController extends State<ChatList>
switch (action) { switch (action) {
case EditBundleAction.addToBundle: case EditBundleAction.addToBundle:
final bundle = await showTextInputDialog( final bundle = await showTextInputDialog(
context: context, context: context,
title: l10n.bundleName, title: l10n.bundleName,
textFields: [DialogTextField(hintText: l10n.bundleName)]); textFields: [DialogTextField(hintText: l10n.bundleName)],
);
if (bundle == null || bundle.isEmpty || bundle.single.isEmpty) return; if (bundle == null || bundle.isEmpty || bundle.single.isEmpty) return;
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,

View File

@ -46,247 +46,248 @@ class ChatListViewBody extends StatelessWidget {
); );
}, },
child: StreamBuilder( child: StreamBuilder(
key: ValueKey(client.userID.toString() + key: ValueKey(
client.userID.toString() +
controller.activeFilter.toString() + controller.activeFilter.toString() +
controller.activeSpaceId.toString()), controller.activeSpaceId.toString(),
stream: client.onSync.stream ),
.where((s) => s.hasRoomUpdate) stream: client.onSync.stream
.rateLimit(const Duration(seconds: 1)), .where((s) => s.hasRoomUpdate)
builder: (context, _) { .rateLimit(const Duration(seconds: 1)),
if (controller.activeFilter == ActiveFilter.spaces && builder: (context, _) {
!controller.isSearchMode) { if (controller.activeFilter == ActiveFilter.spaces &&
return SpaceView( !controller.isSearchMode) {
controller, return SpaceView(
scrollController: controller.scrollController, controller,
key: Key(controller.activeSpaceId ?? 'Spaces'), scrollController: controller.scrollController,
); key: Key(controller.activeSpaceId ?? 'Spaces'),
} );
if (controller.waitForFirstSync && client.prevBatch != null) { }
final rooms = controller.filteredRooms; if (controller.waitForFirstSync && client.prevBatch != null) {
final displayStoriesHeader = { final rooms = controller.filteredRooms;
ActiveFilter.allChats, final displayStoriesHeader = {
ActiveFilter.messages, ActiveFilter.allChats,
}.contains(controller.activeFilter) && ActiveFilter.messages,
client.storiesRooms.isNotEmpty; }.contains(controller.activeFilter) &&
return ListView.builder( client.storiesRooms.isNotEmpty;
controller: controller.scrollController, return ListView.builder(
// add +1 space below in order to properly scroll below the spaces bar controller: controller.scrollController,
itemCount: rooms.length + 1, // add +1 space below in order to properly scroll below the spaces bar
itemBuilder: (BuildContext context, int i) { itemCount: rooms.length + 1,
if (i == 0) { itemBuilder: (BuildContext context, int i) {
return Column( if (i == 0) {
mainAxisSize: MainAxisSize.min, return Column(
children: [ mainAxisSize: MainAxisSize.min,
if (controller.isSearchMode) ...[ children: [
SearchTitle( if (controller.isSearchMode) ...[
title: L10n.of(context)!.publicRooms, SearchTitle(
icon: const Icon(Icons.explore_outlined), title: L10n.of(context)!.publicRooms,
), icon: const Icon(Icons.explore_outlined),
AnimatedContainer( ),
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
height: roomSearchResult == null ||
roomSearchResult.chunk.isEmpty
? 0
: 106,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: roomSearchResult == null
? null
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: roomSearchResult.chunk.length,
itemBuilder: (context, i) => _SearchItem(
title: roomSearchResult.chunk[i].name ??
roomSearchResult.chunk[i]
.canonicalAlias?.localpart ??
L10n.of(context)!.group,
avatar:
roomSearchResult.chunk[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => PublicRoomBottomSheet(
roomAlias: roomSearchResult
.chunk[i].canonicalAlias ??
roomSearchResult.chunk[i].roomId,
outerContext: context,
chunk: roomSearchResult.chunk[i],
),
),
),
),
),
SearchTitle(
title: L10n.of(context)!.users,
icon: const Icon(Icons.group_outlined),
),
AnimatedContainer(
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
height: userSearchResult == null ||
userSearchResult.results.isEmpty
? 0
: 106,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: userSearchResult == null
? null
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: userSearchResult.results.length,
itemBuilder: (context, i) => _SearchItem(
title: userSearchResult
.results[i].displayName ??
userSearchResult
.results[i].userId.localpart ??
L10n.of(context)!.unknownDevice,
avatar:
userSearchResult.results[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => ProfileBottomSheet(
userId: userSearchResult
.results[i].userId,
outerContext: context,
),
),
),
),
),
SearchTitle(
title: L10n.of(context)!.stories,
icon: const Icon(Icons.camera_alt_outlined),
),
],
if (displayStoriesHeader)
StoriesHeader(
key: const Key('stories_header'),
filter: controller.searchController.text,
),
const ConnectionStatusHeader(),
AnimatedContainer( AnimatedContainer(
height: controller.isTorBrowser ? 64 : 0,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(), decoration: const BoxDecoration(),
child: Material( height: roomSearchResult == null ||
color: Theme.of(context).colorScheme.surface, roomSearchResult.chunk.isEmpty
child: ListTile( ? 0
leading: const Icon(Icons.vpn_key), : 106,
title: Text(L10n.of(context)!.dehydrateTor), duration: FluffyThemes.animationDuration,
subtitle: curve: FluffyThemes.animationCurve,
Text(L10n.of(context)!.dehydrateTorLong), child: roomSearchResult == null
trailing: ? null
const Icon(Icons.chevron_right_outlined), : ListView.builder(
onTap: controller.dehydrate, scrollDirection: Axis.horizontal,
), itemCount: roomSearchResult.chunk.length,
), itemBuilder: (context, i) => _SearchItem(
), title: roomSearchResult.chunk[i].name ??
if (controller.isSearchMode) roomSearchResult.chunk[i].canonicalAlias
SearchTitle( ?.localpart ??
title: L10n.of(context)!.chats, L10n.of(context)!.group,
icon: const Icon(Icons.chat_outlined), avatar: roomSearchResult.chunk[i].avatarUrl,
), onPressed: () => showAdaptiveBottomSheet(
if (rooms.isEmpty && !controller.isSearchMode) context: context,
Padding( builder: (c) => PublicRoomBottomSheet(
padding: const EdgeInsets.all(32.0), roomAlias: roomSearchResult
child: Column( .chunk[i].canonicalAlias ??
mainAxisAlignment: MainAxisAlignment.center, roomSearchResult.chunk[i].roomId,
children: [ outerContext: context,
Image.asset( chunk: roomSearchResult.chunk[i],
'assets/start_chat.png', ),
height: 256, ),
),
), ),
const Divider(height: 1), ),
], SearchTitle(
), title: L10n.of(context)!.users,
), icon: const Icon(Icons.group_outlined),
),
AnimatedContainer(
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
height: userSearchResult == null ||
userSearchResult.results.isEmpty
? 0
: 106,
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
child: userSearchResult == null
? null
: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: userSearchResult.results.length,
itemBuilder: (context, i) => _SearchItem(
title: userSearchResult
.results[i].displayName ??
userSearchResult
.results[i].userId.localpart ??
L10n.of(context)!.unknownDevice,
avatar:
userSearchResult.results[i].avatarUrl,
onPressed: () => showAdaptiveBottomSheet(
context: context,
builder: (c) => ProfileBottomSheet(
userId:
userSearchResult.results[i].userId,
outerContext: context,
),
),
),
),
),
SearchTitle(
title: L10n.of(context)!.stories,
icon: const Icon(Icons.camera_alt_outlined),
),
], ],
); if (displayStoriesHeader)
} StoriesHeader(
i--; key: const Key('stories_header'),
if (!rooms[i] filter: controller.searchController.text,
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)) ),
.toLowerCase() const ConnectionStatusHeader(),
.contains( AnimatedContainer(
controller.searchController.text.toLowerCase())) { height: controller.isTorBrowser ? 64 : 0,
return Container(); duration: FluffyThemes.animationDuration,
} curve: FluffyThemes.animationCurve,
return ChatListItem( clipBehavior: Clip.hardEdge,
rooms[i], decoration: const BoxDecoration(),
key: Key('chat_list_item_${rooms[i].id}'), child: Material(
selected: controller.selectedRoomIds.contains(rooms[i].id), color: Theme.of(context).colorScheme.surface,
onTap: controller.selectMode == SelectMode.select child: ListTile(
? () => controller.toggleSelection(rooms[i].id) leading: const Icon(Icons.vpn_key),
: null, title: Text(L10n.of(context)!.dehydrateTor),
onLongPress: () => controller.toggleSelection(rooms[i].id), subtitle: Text(L10n.of(context)!.dehydrateTorLong),
activeChat: controller.activeChat == rooms[i].id, trailing: const Icon(Icons.chevron_right_outlined),
); onTap: controller.dehydrate,
},
);
}
const dummyChatCount = 5;
final titleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100);
final subtitleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50);
return ListView.builder(
key: const Key('dummychats'),
itemCount: dummyChatCount,
itemBuilder: (context, i) => Opacity(
opacity: (dummyChatCount - i) / dummyChatCount,
child: ListTile(
leading: CircleAvatar(
backgroundColor: titleColor,
child: CircularProgressIndicator(
strokeWidth: 1,
color: Theme.of(context).textTheme.bodyLarge!.color,
),
),
title: Row(
children: [
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.circular(3),
), ),
), ),
), ),
const SizedBox(width: 36), if (controller.isSearchMode)
Container( SearchTitle(
height: 14, title: L10n.of(context)!.chats,
width: 14, icon: const Icon(Icons.chat_outlined),
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
), ),
), if (rooms.isEmpty && !controller.isSearchMode)
const SizedBox(width: 12), Padding(
Container( padding: const EdgeInsets.all(32.0),
height: 14, child: Column(
width: 14, mainAxisAlignment: MainAxisAlignment.center,
decoration: BoxDecoration( children: [
color: subtitleColor, Image.asset(
borderRadius: BorderRadius.circular(14), 'assets/start_chat.png',
height: 256,
),
const Divider(height: 1),
],
),
), ),
),
], ],
), );
subtitle: Container( }
decoration: BoxDecoration( i--;
color: subtitleColor, if (!rooms[i]
borderRadius: BorderRadius.circular(3), .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!))
), .toLowerCase()
height: 12, .contains(
margin: const EdgeInsets.only(right: 22), controller.searchController.text.toLowerCase(),
)) {
return Container();
}
return ChatListItem(
rooms[i],
key: Key('chat_list_item_${rooms[i].id}'),
selected: controller.selectedRoomIds.contains(rooms[i].id),
onTap: controller.selectMode == SelectMode.select
? () => controller.toggleSelection(rooms[i].id)
: null,
onLongPress: () => controller.toggleSelection(rooms[i].id),
activeChat: controller.activeChat == rooms[i].id,
);
},
);
}
const dummyChatCount = 5;
final titleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100);
final subtitleColor =
Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50);
return ListView.builder(
key: const Key('dummychats'),
itemCount: dummyChatCount,
itemBuilder: (context, i) => Opacity(
opacity: (dummyChatCount - i) / dummyChatCount,
child: ListTile(
leading: CircleAvatar(
backgroundColor: titleColor,
child: CircularProgressIndicator(
strokeWidth: 1,
color: Theme.of(context).textTheme.bodyLarge!.color,
), ),
), ),
title: Row(
children: [
Expanded(
child: Container(
height: 14,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.circular(3),
),
),
),
const SizedBox(width: 36),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
const SizedBox(width: 12),
Container(
height: 14,
width: 14,
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(14),
),
),
],
),
subtitle: Container(
decoration: BoxDecoration(
color: subtitleColor,
borderRadius: BorderRadius.circular(3),
),
height: 12,
margin: const EdgeInsets.only(right: 22),
),
), ),
); ),
}), );
},
),
); );
} }
} }

View File

@ -120,22 +120,28 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
IconButton( IconButton(
tooltip: L10n.of(context)!.toggleUnread, tooltip: L10n.of(context)!.toggleUnread,
icon: Icon(controller.anySelectedRoomNotMarkedUnread icon: Icon(
? Icons.mark_chat_read_outlined controller.anySelectedRoomNotMarkedUnread
: Icons.mark_chat_unread_outlined), ? Icons.mark_chat_read_outlined
: Icons.mark_chat_unread_outlined,
),
onPressed: controller.toggleUnread, onPressed: controller.toggleUnread,
), ),
IconButton( IconButton(
tooltip: L10n.of(context)!.toggleFavorite, tooltip: L10n.of(context)!.toggleFavorite,
icon: Icon(controller.anySelectedRoomNotFavorite icon: Icon(
? Icons.push_pin_outlined controller.anySelectedRoomNotFavorite
: Icons.push_pin), ? Icons.push_pin_outlined
: Icons.push_pin,
),
onPressed: controller.toggleFavouriteRoom, onPressed: controller.toggleFavouriteRoom,
), ),
IconButton( IconButton(
icon: Icon(controller.anySelectedRoomNotMuted icon: Icon(
? Icons.notifications_off_outlined controller.anySelectedRoomNotMuted
: Icons.notifications_outlined), ? Icons.notifications_off_outlined
: Icons.notifications_outlined,
),
tooltip: L10n.of(context)!.toggleMuted, tooltip: L10n.of(context)!.toggleMuted,
onPressed: controller.toggleMuted, onPressed: controller.toggleMuted,
), ),

View File

@ -38,15 +38,16 @@ class ChatListItem extends StatelessWidget {
if (activeChat) return; if (activeChat) return;
if (room.membership == Membership.invite) { if (room.membership == Membership.invite) {
final joinResult = await showFutureLoadingDialog( final joinResult = await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
final waitForRoom = room.client.waitForRoomInSync( final waitForRoom = room.client.waitForRoomInSync(
room.id, room.id,
join: true, join: true,
); );
await room.join(); await room.join();
await waitForRoom; await waitForRoom;
}); },
);
if (joinResult.error != null) return; if (joinResult.error != null) return;
} }
@ -107,7 +108,9 @@ class ChatListItem extends StatelessWidget {
); );
if (confirmed == OkCancelResult.cancel) return; if (confirmed == OkCancelResult.cancel) return;
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, future: () => room.leave()); context: context,
future: () => room.leave(),
);
return; return;
} }
} }
@ -183,7 +186,8 @@ class ChatListItem extends StatelessWidget {
if (room.isFavourite) if (room.isFavourite)
Padding( Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: room.notificationCount > 0 ? 4.0 : 0.0), right: room.notificationCount > 0 ? 4.0 : 0.0,
),
child: Icon( child: Icon(
Icons.push_pin, Icons.push_pin,
size: 16, size: 16,
@ -282,7 +286,8 @@ class ChatListItem extends StatelessWidget {
: null, : null,
), ),
); );
}), },
),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
AnimatedContainer( AnimatedContainer(

View File

@ -109,56 +109,60 @@ class ChatListView extends StatelessWidget {
children: [ children: [
if (FluffyThemes.isColumnMode(context) && if (FluffyThemes.isColumnMode(context) &&
FluffyThemes.getDisplayNavigationRail(context)) ...[ FluffyThemes.getDisplayNavigationRail(context)) ...[
Builder(builder: (context) { Builder(
final allSpaces = client.rooms.where((room) => room.isSpace); builder: (context) {
final rootSpaces = allSpaces final allSpaces =
.where( client.rooms.where((room) => room.isSpace);
(space) => !allSpaces.any( final rootSpaces = allSpaces
(parentSpace) => parentSpace.spaceChildren .where(
.any((child) => child.roomId == space.id), (space) => !allSpaces.any(
), (parentSpace) => parentSpace.spaceChildren
) .any((child) => child.roomId == space.id),
.toList(); ),
final destinations = getNavigationDestinations(context); )
.toList();
final destinations = getNavigationDestinations(context);
return SizedBox( return SizedBox(
width: 64, width: 64,
child: ListView.builder( child: ListView.builder(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
itemCount: rootSpaces.length + destinations.length, itemCount: rootSpaces.length + destinations.length,
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i < destinations.length) { if (i < destinations.length) {
return NaviRailItem(
isSelected: i == controller.selectedIndex,
onTap: () => controller.onDestinationSelected(i),
icon: destinations[i].icon,
selectedIcon: destinations[i].selectedIcon,
toolTip: destinations[i].label,
);
}
i -= destinations.length;
final isSelected =
controller.activeFilter == ActiveFilter.spaces &&
rootSpaces[i].id == controller.activeSpaceId;
return NaviRailItem( return NaviRailItem(
isSelected: i == controller.selectedIndex, toolTip: rootSpaces[i].getLocalizedDisplayname(
onTap: () => controller.onDestinationSelected(i),
icon: destinations[i].icon,
selectedIcon: destinations[i].selectedIcon,
toolTip: destinations[i].label,
);
}
i -= destinations.length;
final isSelected =
controller.activeFilter == ActiveFilter.spaces &&
rootSpaces[i].id == controller.activeSpaceId;
return NaviRailItem(
toolTip: rootSpaces[i].getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!)),
isSelected: isSelected,
onTap: () =>
controller.setActiveSpace(rootSpaces[i].id),
icon: Avatar(
mxContent: rootSpaces[i].avatar,
name: rootSpaces[i].getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
), ),
size: 32, isSelected: isSelected,
fontSize: 12, onTap: () =>
), controller.setActiveSpace(rootSpaces[i].id),
); icon: Avatar(
}, mxContent: rootSpaces[i].avatar,
), name: rootSpaces[i].getLocalizedDisplayname(
); MatrixLocals(L10n.of(context)!),
}), ),
size: 32,
fontSize: 12,
),
);
},
),
);
},
),
Container( Container(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
width: 1, width: 1,

View File

@ -20,11 +20,13 @@ class ClientChooserButton extends StatelessWidget {
List<PopupMenuEntry<Object>> _bundleMenuItems(BuildContext context) { List<PopupMenuEntry<Object>> _bundleMenuItems(BuildContext context) {
final matrix = Matrix.of(context); final matrix = Matrix.of(context);
final bundles = matrix.accountBundles.keys.toList() final bundles = matrix.accountBundles.keys.toList()
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId ..sort(
? 0 (a, b) => a!.isValidMatrixId == b!.isValidMatrixId
: a.isValidMatrixId && !b.isValidMatrixId ? 0
? -1 : a.isValidMatrixId && !b.isValidMatrixId
: 1); ? -1
: 1,
);
return <PopupMenuEntry<Object>>[ return <PopupMenuEntry<Object>>[
PopupMenuItem( PopupMenuItem(
value: SettingsAction.newStory, value: SettingsAction.newStory,
@ -142,7 +144,9 @@ class ClientChooserButton extends StatelessWidget {
IconButton( IconButton(
icon: const Icon(Icons.edit_outlined), icon: const Icon(Icons.edit_outlined),
onPressed: () => controller.editBundlesForAccount( onPressed: () => controller.editBundlesForAccount(
client.userID, bundle), client.userID,
bundle,
),
), ),
], ],
), ),
@ -270,9 +274,12 @@ class ClientChooserButton extends StatelessWidget {
break; break;
case SettingsAction.invite: case SettingsAction.invite:
FluffyShare.share( FluffyShare.share(
L10n.of(context)!.inviteText(Matrix.of(context).client.userID!, L10n.of(context)!.inviteText(
'https://matrix.to/#/${Matrix.of(context).client.userID}?client=im.fluffychat'), Matrix.of(context).client.userID!,
context); 'https://matrix.to/#/${Matrix.of(context).client.userID}?client=im.fluffychat',
),
context,
);
break; break;
case SettingsAction.settings: case SettingsAction.settings:
VRouter.of(context).to('/settings'); VRouter.of(context).to('/settings');
@ -290,11 +297,13 @@ class ClientChooserButton extends StatelessWidget {
BuildContext context, BuildContext context,
) { ) {
final bundles = matrix.accountBundles.keys.toList() final bundles = matrix.accountBundles.keys.toList()
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId ..sort(
? 0 (a, b) => a!.isValidMatrixId == b!.isValidMatrixId
: a.isValidMatrixId && !b.isValidMatrixId ? 0
? -1 : a.isValidMatrixId && !b.isValidMatrixId
: 1); ? -1
: 1,
);
// beginning from end if negative // beginning from end if negative
if (index < 0) { if (index < 0) {
int clientCount = 0; int clientCount = 0;
@ -320,11 +329,13 @@ class ClientChooserButton extends StatelessWidget {
int index = 0; int index = 0;
final bundles = matrix.accountBundles.keys.toList() final bundles = matrix.accountBundles.keys.toList()
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId ..sort(
? 0 (a, b) => a!.isValidMatrixId == b!.isValidMatrixId
: a.isValidMatrixId && !b.isValidMatrixId ? 0
? -1 : a.isValidMatrixId && !b.isValidMatrixId
: 1); ? -1
: 1,
);
for (final bundleName in bundles) { for (final bundleName in bundles) {
final bundle = matrix.accountBundles[bundleName]; final bundle = matrix.accountBundles[bundleName];
if (bundle == null) return null; if (bundle == null) return null;

View File

@ -48,17 +48,18 @@ class NaviRailItem extends StatelessWidget {
onPressed: onTap, onPressed: onTap,
tooltip: toolTip, tooltip: toolTip,
icon: Material( icon: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius), borderRadius: BorderRadius.circular(AppConfig.borderRadius),
color: isSelected color: isSelected
? Theme.of(context).colorScheme.primaryContainer ? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.background, : Theme.of(context).colorScheme.background,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8.0, horizontal: 8.0,
vertical: 8.0, vertical: 8.0,
), ),
child: isSelected ? selectedIcon ?? icon : icon, child: isSelected ? selectedIcon ?? icon : icon,
)), ),
),
), ),
), ),
], ],

View File

@ -45,13 +45,15 @@ class SearchTitle extends StatelessWidget {
children: [ children: [
icon, icon,
const SizedBox(width: 16), const SizedBox(width: 16),
Text(title, Text(
textAlign: TextAlign.left, title,
style: TextStyle( textAlign: TextAlign.left,
color: Theme.of(context).colorScheme.onSurface, style: TextStyle(
fontSize: 12, color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.bold, fontSize: 12,
)), fontWeight: FontWeight.bold,
),
),
if (trailing != null) if (trailing != null)
Expanded( Expanded(
child: Align( child: Align(

View File

@ -53,11 +53,14 @@ class _SpaceViewState extends State<SpaceView> {
final result = await showFutureLoadingDialog( final result = await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
await client.joinRoom(spaceChild.roomId, await client.joinRoom(
serverName: space?.spaceChildren spaceChild.roomId,
.firstWhereOrNull( serverName: space?.spaceChildren
(child) => child.roomId == spaceChild.roomId) .firstWhereOrNull(
?.via); (child) => child.roomId == spaceChild.roomId,
)
?.via,
);
if (client.getRoomById(spaceChild.roomId) == null) { if (client.getRoomById(spaceChild.roomId) == null) {
// Wait for room actually appears in sync // Wait for room actually appears in sync
await client.waitForRoomInSync(spaceChild.roomId, join: true); await client.waitForRoomInSync(spaceChild.roomId, join: true);
@ -78,8 +81,10 @@ class _SpaceViewState extends State<SpaceView> {
VRouter.of(context).toSegments(['rooms', spaceChild.roomId]); VRouter.of(context).toSegments(['rooms', spaceChild.roomId]);
} }
void _onSpaceChildContextMenu( void _onSpaceChildContextMenu([
[SpaceRoomsChunk? spaceChild, Room? room]) async { SpaceRoomsChunk? spaceChild,
Room? room,
]) async {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
final activeSpaceId = widget.controller.activeSpaceId; final activeSpaceId = widget.controller.activeSpaceId;
final activeSpace = final activeSpace =
@ -169,8 +174,10 @@ class _SpaceViewState extends State<SpaceView> {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
subtitle: Text(L10n.of(context)! subtitle: Text(
.numChats(rootSpace.spaceChildren.length.toString())), L10n.of(context)!
.numChats(rootSpace.spaceChildren.length.toString()),
),
onTap: () => widget.controller.setActiveSpace(rootSpace.id), onTap: () => widget.controller.setActiveSpace(rootSpace.id),
onLongPress: () => _onSpaceChildContextMenu(null, rootSpace), onLongPress: () => _onSpaceChildContextMenu(null, rootSpace),
trailing: const Icon(Icons.chevron_right_outlined), trailing: const Icon(Icons.chevron_right_outlined),
@ -180,166 +187,166 @@ class _SpaceViewState extends State<SpaceView> {
); );
} }
return FutureBuilder<GetSpaceHierarchyResponse>( return FutureBuilder<GetSpaceHierarchyResponse>(
future: getFuture(activeSpaceId), future: getFuture(activeSpaceId),
builder: (context, snapshot) { builder: (context, snapshot) {
final response = snapshot.data; final response = snapshot.data;
final error = snapshot.error; final error = snapshot.error;
if (error != null) { if (error != null) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Text(error.toLocalizedString(context)), child: Text(error.toLocalizedString(context)),
), ),
IconButton( IconButton(
onPressed: _refresh, onPressed: _refresh,
icon: const Icon(Icons.refresh_outlined), icon: const Icon(Icons.refresh_outlined),
) )
], ],
); );
} }
if (response == null) { if (response == null) {
return const Center(child: CircularProgressIndicator.adaptive()); return const Center(child: CircularProgressIndicator.adaptive());
} }
final parentSpace = allSpaces.firstWhereOrNull((space) => space final parentSpace = allSpaces.firstWhereOrNull(
.spaceChildren (space) =>
.any((child) => child.roomId == activeSpaceId)); space.spaceChildren.any((child) => child.roomId == activeSpaceId),
final spaceChildren = response.rooms; );
final canLoadMore = response.nextBatch != null; final spaceChildren = response.rooms;
return VWidgetGuard( final canLoadMore = response.nextBatch != null;
onSystemPop: (redirector) async { return VWidgetGuard(
if (parentSpace != null) { onSystemPop: (redirector) async {
widget.controller.setActiveSpace(parentSpace.id); if (parentSpace != null) {
redirector.stopRedirection(); widget.controller.setActiveSpace(parentSpace.id);
return; redirector.stopRedirection();
return;
}
},
child: ListView.builder(
itemCount: spaceChildren.length + 1 + (canLoadMore ? 1 : 0),
controller: widget.scrollController,
itemBuilder: (context, i) {
if (i == 0) {
return ListTile(
leading: BackButton(
onPressed: () =>
widget.controller.setActiveSpace(parentSpace?.id),
),
title: Text(
parentSpace == null
? L10n.of(context)!.allSpaces
: parentSpace.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
),
),
trailing: IconButton(
icon: snapshot.connectionState != ConnectionState.done
? const CircularProgressIndicator.adaptive()
: const Icon(Icons.refresh_outlined),
onPressed: snapshot.connectionState != ConnectionState.done
? null
: _refresh,
),
);
} }
}, i--;
child: ListView.builder( if (canLoadMore && i == spaceChildren.length) {
itemCount: spaceChildren.length + 1 + (canLoadMore ? 1 : 0), return ListTile(
controller: widget.scrollController, title: Text(L10n.of(context)!.loadMore),
itemBuilder: (context, i) { trailing: const Icon(Icons.chevron_right_outlined),
if (i == 0) { onTap: () {
return ListTile( prevBatch = response.nextBatch;
leading: BackButton( _refresh();
onPressed: () => },
widget.controller.setActiveSpace(parentSpace?.id), );
), }
title: Text(parentSpace == null final spaceChild = spaceChildren[i];
? L10n.of(context)!.allSpaces final room = client.getRoomById(spaceChild.roomId);
: parentSpace.getLocalizedDisplayname( if (room != null && !room.isSpace) {
MatrixLocals(L10n.of(context)!), return ChatListItem(
)), room,
trailing: IconButton( onLongPress: () => _onSpaceChildContextMenu(spaceChild, room),
icon: snapshot.connectionState != ConnectionState.done activeChat: widget.controller.activeChat == room.id,
? const CircularProgressIndicator.adaptive() );
: const Icon(Icons.refresh_outlined), }
onPressed: final isSpace = spaceChild.roomType == 'm.space';
snapshot.connectionState != ConnectionState.done final topic =
? null spaceChild.topic?.isEmpty ?? true ? null : spaceChild.topic;
: _refresh, if (spaceChild.roomId == activeSpaceId) {
), return SearchTitle(
); title:
} spaceChild.name ?? spaceChild.canonicalAlias ?? 'Space',
i--; icon: Padding(
if (canLoadMore && i == spaceChildren.length) { padding: const EdgeInsets.symmetric(horizontal: 10.0),
return ListTile( child: Avatar(
title: Text(L10n.of(context)!.loadMore), size: 24,
trailing: const Icon(Icons.chevron_right_outlined),
onTap: () {
prevBatch = response.nextBatch;
_refresh();
},
);
}
final spaceChild = spaceChildren[i];
final room = client.getRoomById(spaceChild.roomId);
if (room != null && !room.isSpace) {
return ChatListItem(
room,
onLongPress: () =>
_onSpaceChildContextMenu(spaceChild, room),
activeChat: widget.controller.activeChat == room.id,
);
}
final isSpace = spaceChild.roomType == 'm.space';
final topic = spaceChild.topic?.isEmpty ?? true
? null
: spaceChild.topic;
if (spaceChild.roomId == activeSpaceId) {
return SearchTitle(
title: spaceChild.name ??
spaceChild.canonicalAlias ??
'Space',
icon: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Avatar(
size: 24,
mxContent: spaceChild.avatarUrl,
name: spaceChild.name,
fontSize: 9,
),
),
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withAlpha(128),
trailing: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Icon(Icons.edit_outlined),
),
onTap: () => _onJoinSpaceChild(spaceChild),
);
}
return ListTile(
leading: Avatar(
mxContent: spaceChild.avatarUrl, mxContent: spaceChild.avatarUrl,
name: spaceChild.name, name: spaceChild.name,
fontSize: 9,
), ),
title: Row( ),
children: [ color: Theme.of(context)
Expanded( .colorScheme
child: Text( .secondaryContainer
spaceChild.name ?? .withAlpha(128),
spaceChild.canonicalAlias ?? trailing: const Padding(
L10n.of(context)!.chat, padding: EdgeInsets.symmetric(horizontal: 16.0),
maxLines: 1, child: Icon(Icons.edit_outlined),
style: const TextStyle(fontWeight: FontWeight.bold), ),
), onTap: () => _onJoinSpaceChild(spaceChild),
), );
if (!isSpace) ...[ }
const Icon( return ListTile(
Icons.people_outline, leading: Avatar(
size: 16, mxContent: spaceChild.avatarUrl,
), name: spaceChild.name,
const SizedBox(width: 4), ),
Text( title: Row(
spaceChild.numJoinedMembers.toString(), children: [
style: const TextStyle(fontSize: 14), Expanded(
), child: Text(
], spaceChild.name ??
], spaceChild.canonicalAlias ??
L10n.of(context)!.chat,
maxLines: 1,
style: const TextStyle(fontWeight: FontWeight.bold),
),
), ),
onTap: () => _onJoinSpaceChild(spaceChild), if (!isSpace) ...[
onLongPress: () => const Icon(
_onSpaceChildContextMenu(spaceChild, room), Icons.people_outline,
subtitle: Text( size: 16,
topic ?? ),
(isSpace const SizedBox(width: 4),
? L10n.of(context)!.enterSpace Text(
: L10n.of(context)!.enterRoom), spaceChild.numJoinedMembers.toString(),
maxLines: 1, style: const TextStyle(fontSize: 14),
style: TextStyle( ),
color: Theme.of(context).colorScheme.onBackground), ],
), ],
trailing: isSpace ),
? const Icon(Icons.chevron_right_outlined) onTap: () => _onJoinSpaceChild(spaceChild),
: null, onLongPress: () => _onSpaceChildContextMenu(spaceChild, room),
); subtitle: Text(
}), topic ??
); (isSpace
}); ? L10n.of(context)!.enterSpace
: L10n.of(context)!.enterRoom),
maxLines: 1,
style: TextStyle(
color: Theme.of(context).colorScheme.onBackground,
),
),
trailing:
isSpace ? const Icon(Icons.chevron_right_outlined) : null,
);
},
),
);
},
);
} }
} }

View File

@ -22,12 +22,17 @@ class ChatPermissionsSettings extends StatefulWidget {
class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> { class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
String? get roomId => VRouter.of(context).pathParameters['roomid']; String? get roomId => VRouter.of(context).pathParameters['roomid'];
void editPowerLevel(BuildContext context, String key, int currentLevel, void editPowerLevel(
{String? category}) async { BuildContext context,
String key,
int currentLevel, {
String? category,
}) 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(context).showSnackBar(
SnackBar(content: Text(L10n.of(context)!.noPermission))); SnackBar(content: Text(L10n.of(context)!.noPermission)),
);
return; return;
} }
final newLevel = await showPermissionChooser( final newLevel = await showPermissionChooser(
@ -36,7 +41,8 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
); );
if (newLevel == null) return; if (newLevel == null) return;
final content = Map<String, dynamic>.from( final content = Map<String, dynamic>.from(
room.getState(EventTypes.RoomPowerLevels)!.content); room.getState(EventTypes.RoomPowerLevels)!.content,
);
if (category != null) { if (category != null) {
if (!content.containsKey(category)) { if (!content.containsKey(category)) {
content[category] = <String, dynamic>{}; content[category] = <String, dynamic>{};
@ -74,10 +80,13 @@ class ChatPermissionsSettingsController extends State<ChatPermissionsSettings> {
title: L10n.of(context)!.replaceRoomWithNewerVersion, title: L10n.of(context)!.replaceRoomWithNewerVersion,
actions: capabilities.mRoomVersions!.available.entries actions: capabilities.mRoomVersions!.available.entries
.where((r) => r.key != roomVersion) .where((r) => r.key != roomVersion)
.map((version) => AlertDialogAction( .map(
(version) => AlertDialogAction(
key: version.key, key: version.key,
label: label:
'${version.key} (${version.value.toString().split('.').last})')) '${version.key} (${version.value.toString().split('.').last})',
),
)
.toList(), .toList(),
); );
if (newVersion == null || if (newVersion == null ||

View File

@ -41,7 +41,8 @@ class ChatPermissionsSettingsView extends StatelessWidget {
return Center(child: Text(L10n.of(context)!.noRoomsFound)); return Center(child: Text(L10n.of(context)!.noRoomsFound));
} }
final powerLevelsContent = Map<String, dynamic>.from( final powerLevelsContent = Map<String, dynamic>.from(
room.getState(EventTypes.RoomPowerLevels)!.content); room.getState(EventTypes.RoomPowerLevels)!.content,
);
final powerLevels = Map<String, dynamic>.from(powerLevelsContent) final powerLevels = Map<String, dynamic>.from(powerLevelsContent)
..removeWhere((k, v) => v is! int); ..removeWhere((k, v) => v is! int);
final eventsPowerLevels = final eventsPowerLevels =
@ -57,7 +58,10 @@ class ChatPermissionsSettingsView extends StatelessWidget {
permissionKey: entry.key, permissionKey: entry.key,
permission: entry.value, permission: entry.value,
onTap: () => controller.editPowerLevel( onTap: () => controller.editPowerLevel(
context, entry.key, entry.value), context,
entry.key,
entry.value,
),
), ),
const Divider(thickness: 1), const Divider(thickness: 1),
ListTile( ListTile(
@ -69,21 +73,26 @@ class ChatPermissionsSettingsView extends StatelessWidget {
), ),
), ),
), ),
Builder(builder: (context) { Builder(
const key = 'rooms'; builder: (context) {
final int value = powerLevelsContent const key = 'rooms';
.containsKey('notifications') final int value = powerLevelsContent
? powerLevelsContent['notifications']['rooms'] ?? 0 .containsKey('notifications')
: 0; ? powerLevelsContent['notifications']['rooms'] ?? 0
return PermissionsListTile( : 0;
permissionKey: key, return PermissionsListTile(
permission: value, permissionKey: key,
category: 'notifications', permission: value,
onTap: () => controller.editPowerLevel( category: 'notifications',
context, key, value, onTap: () => controller.editPowerLevel(
category: 'notifications'), context,
); key,
}), value,
category: 'notifications',
),
);
},
),
const Divider(thickness: 1), const Divider(thickness: 1),
ListTile( ListTile(
title: Text( title: Text(
@ -100,8 +109,11 @@ class ChatPermissionsSettingsView extends StatelessWidget {
category: 'events', category: 'events',
permission: entry.value, permission: entry.value,
onTap: () => controller.editPowerLevel( onTap: () => controller.editPowerLevel(
context, entry.key, entry.value, context,
category: 'events'), entry.key,
entry.value,
category: 'events',
),
), ),
if (room.canSendEvent(EventTypes.RoomTombstone)) ...{ if (room.canSendEvent(EventTypes.RoomTombstone)) ...{
const Divider(thickness: 1), const Divider(thickness: 1),
@ -110,8 +122,10 @@ class ChatPermissionsSettingsView extends StatelessWidget {
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Center( return const Center(
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(
strokeWidth: 2)); strokeWidth: 2,
),
);
} }
final String roomVersion = room final String roomVersion = room
.getState(EventTypes.RoomCreate)! .getState(EventTypes.RoomCreate)!
@ -120,7 +134,8 @@ class ChatPermissionsSettingsView extends StatelessWidget {
return ListTile( return ListTile(
title: Text( title: Text(
'${L10n.of(context)!.roomVersion}: $roomVersion'), '${L10n.of(context)!.roomVersion}: $roomVersion',
),
onTap: () => onTap: () =>
controller.updateRoomAction(snapshot.data!), controller.updateRoomAction(snapshot.data!),
); );

View File

@ -118,8 +118,9 @@ class ConnectPageController extends State<ConnectPage> {
List<IdentityProvider>? get identityProviders { List<IdentityProvider>? get identityProviders {
final loginTypes = _rawLoginTypes; final loginTypes = _rawLoginTypes;
if (loginTypes == null) return null; if (loginTypes == null) return null;
final rawProviders = loginTypes.tryGetList('flows')!.singleWhere((flow) => final rawProviders = loginTypes.tryGetList('flows')!.singleWhere(
flow['type'] == AuthenticationTypes.sso)['identity_providers']; (flow) => flow['type'] == AuthenticationTypes.sso,
)['identity_providers'];
final list = (rawProviders as List) final list = (rawProviders as List)
.map((json) => IdentityProvider.fromJson(json)) .map((json) => IdentityProvider.fromJson(json))
.toList(); .toList();
@ -163,9 +164,11 @@ class ConnectPageController extends State<ConnectPage> {
RequestType.GET, RequestType.GET,
'/client/r0/login', '/client/r0/login',
) )
.then((loginTypes) => setState(() { .then(
_rawLoginTypes = loginTypes; (loginTypes) => setState(() {
})); _rawLoginTypes = loginTypes;
}),
);
} }
} }

View File

@ -174,17 +174,20 @@ class ConnectPageView extends StatelessWidget {
) )
: Image.network( : Image.network(
Uri.parse(identityProviders.single.icon!) Uri.parse(identityProviders.single.icon!)
.getDownloadLink(Matrix.of(context) .getDownloadLink(
.getLoginClient()) Matrix.of(context).getLoginClient(),
)
.toString(), .toString(),
width: 32, width: 32,
height: 32, height: 32,
), ),
onPressed: () => controller onPressed: () => controller
.ssoLoginAction(identityProviders.single.id!), .ssoLoginAction(identityProviders.single.id!),
label: Text(identityProviders.single.name ?? label: Text(
identityProviders.single.brand ?? identityProviders.single.name ??
L10n.of(context)!.loginWithOneClick), identityProviders.single.brand ??
L10n.of(context)!.loginWithOneClick,
),
), ),
) )
: Wrap( : Wrap(

View File

@ -37,7 +37,8 @@ class SsoButton extends StatelessWidget {
: Image.network( : Image.network(
Uri.parse(identityProvider.icon!) Uri.parse(identityProvider.icon!)
.getDownloadLink( .getDownloadLink(
Matrix.of(context).getLoginClient()) Matrix.of(context).getLoginClient(),
)
.toString(), .toString(),
width: 32, width: 32,
height: 32, height: 32,

View File

@ -35,7 +35,8 @@ class DevicesSettingsView extends StatelessWidget {
} }
if (!snapshot.hasData || controller.devices == null) { if (!snapshot.hasData || controller.devices == null) {
return const Center( return const Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2)); child: CircularProgressIndicator.adaptive(strokeWidth: 2),
);
} }
return ListView.builder( return ListView.builder(
itemCount: controller.notThisDevice.length + 1, itemCount: controller.notThisDevice.length + 1,
@ -63,12 +64,14 @@ class DevicesSettingsView extends StatelessWidget {
), ),
trailing: controller.loadingDeletingDevices trailing: controller.loadingDeletingDevices
? const CircularProgressIndicator.adaptive( ? const CircularProgressIndicator.adaptive(
strokeWidth: 2) strokeWidth: 2,
)
: const Icon(Icons.delete_outline), : const Icon(Icons.delete_outline),
onTap: controller.loadingDeletingDevices onTap: controller.loadingDeletingDevices
? null ? null
: () => controller.removeDevicesAction( : () => controller.removeDevicesAction(
controller.notThisDevice), controller.notThisDevice,
),
) )
else else
Center( Center(

View File

@ -135,8 +135,9 @@ class UserDeviceListItem extends StatelessWidget {
), ),
subtitle: Text( subtitle: Text(
L10n.of(context)!.lastActiveAgo( L10n.of(context)!.lastActiveAgo(
DateTime.fromMillisecondsSinceEpoch(userDevice.lastSeenTs ?? 0) DateTime.fromMillisecondsSinceEpoch(userDevice.lastSeenTs ?? 0)
.localizedTimeShort(context)), .localizedTimeShort(context),
),
style: const TextStyle(fontWeight: FontWeight.w300), style: const TextStyle(fontWeight: FontWeight.w300),
), ),
); );

View File

@ -36,9 +36,12 @@ import 'package:fluffychat/widgets/avatar.dart';
import 'pip/pip_view.dart'; import 'pip/pip_view.dart';
class _StreamView extends StatelessWidget { class _StreamView extends StatelessWidget {
const _StreamView(this.wrappedStream, const _StreamView(
{Key? key, this.mainView = false, required this.matrixClient}) this.wrappedStream, {
: super(key: key); Key? key,
this.mainView = false,
required this.matrixClient,
}) : super(key: key);
final WrappedMediaStream wrappedStream; final WrappedMediaStream wrappedStream;
final Client matrixClient; final Client matrixClient;
@ -67,43 +70,48 @@ 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>[
if (videoMuted) if (videoMuted)
Container( Container(
color: Colors.transparent, color: Colors.transparent,
), ),
if (!videoMuted) if (!videoMuted)
RTCVideoView( RTCVideoView(
// yes, it must explicitly be casted even though I do not feel // yes, it must explicitly be casted even though I do not feel
// comfortable with it... // comfortable with it...
wrappedStream.renderer as RTCVideoRenderer, wrappedStream.renderer as RTCVideoRenderer,
mirror: mirrored, mirror: mirrored,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain, objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
), ),
if (videoMuted) if (videoMuted)
Positioned( Positioned(
child: Avatar( child: Avatar(
mxContent: avatarUrl, mxContent: avatarUrl,
name: displayName, name: displayName,
size: mainView ? 96 : 48, size: mainView ? 96 : 48,
client: matrixClient, client: matrixClient,
// textSize: mainView ? 36 : 24, // textSize: mainView ? 36 : 24,
// matrixClient: matrixClient, // matrixClient: matrixClient,
)), ),
if (!isScreenSharing) ),
Positioned( if (!isScreenSharing)
left: 4.0, Positioned(
bottom: 4.0, left: 4.0,
child: Icon(audioMuted ? Icons.mic_off : Icons.mic, bottom: 4.0,
color: Colors.white, size: 18.0), child: Icon(
) audioMuted ? Icons.mic_off : Icons.mic,
], color: Colors.white,
)); size: 18.0,
),
)
],
),
);
} }
} }
@ -114,14 +122,14 @@ class Calling extends StatefulWidget {
final CallSession call; final CallSession call;
final Client client; final Client client;
const Calling( const Calling({
{required this.context, required this.context,
required this.call, required this.call,
required this.client, required this.client,
required this.callId, required this.callId,
this.onClear, this.onClear,
Key? key}) Key? key,
: super(key: key); }) : super(key: key);
@override @override
MyCallingPage createState() => MyCallingPage(); MyCallingPage createState() => MyCallingPage();
@ -206,7 +214,8 @@ class MyCallingPage extends State<Calling> {
event == CallEvent.kRemoteHoldUnhold) { event == CallEvent.kRemoteHoldUnhold) {
setState(() {}); setState(() {});
Logs().i( Logs().i(
'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}'); 'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}',
);
} }
}); });
_state = call.state; _state = call.state;
@ -239,7 +248,9 @@ class MyCallingPage extends State<Calling> {
void _resizeLocalVideo(Orientation orientation) { void _resizeLocalVideo(Orientation orientation) {
final shortSide = min( final shortSide = min(
MediaQuery.of(context).size.width, MediaQuery.of(context).size.height); MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height,
);
_localVideoMargin = remoteStream != null _localVideoMargin = remoteStream != null
? const EdgeInsets.only(top: 20.0, right: 20.0) ? const EdgeInsets.only(top: 20.0, right: 20.0)
: EdgeInsets.zero; : EdgeInsets.zero;
@ -304,8 +315,9 @@ class MyCallingPage extends State<Calling> {
foregroundTaskOptions: const ForegroundTaskOptions(), foregroundTaskOptions: const ForegroundTaskOptions(),
); );
FlutterForegroundTask.startService( FlutterForegroundTask.startService(
notificationTitle: L10n.of(context)!.screenSharingTitle, notificationTitle: L10n.of(context)!.screenSharingTitle,
notificationText: L10n.of(context)!.screenSharingDetail); notificationText: L10n.of(context)!.screenSharingDetail,
);
} else { } else {
FlutterForegroundTask.stopService(); FlutterForegroundTask.stopService();
} }
@ -331,7 +343,8 @@ class MyCallingPage extends State<Calling> {
void _switchCamera() async { void _switchCamera() async {
if (call.localUserMediaStream != null) { if (call.localUserMediaStream != null) {
await Helper.switchCamera( await Helper.switchCamera(
call.localUserMediaStream!.stream!.getVideoTracks()[0]); call.localUserMediaStream!.stream!.getVideoTracks()[0],
);
if (PlatformInfos.isMobile) { if (PlatformInfos.isMobile) {
call.facingMode == 'user' call.facingMode == 'user'
? call.facingMode = 'environment' ? call.facingMode = 'environment'
@ -473,22 +486,27 @@ class MyCallingPage extends State<Calling> {
} else if (call.remoteOnHold) { } else if (call.remoteOnHold) {
title = 'You held the call.'; title = 'You held the call.';
} }
stackWidgets.add(Center( stackWidgets.add(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Center(
const Icon( child: Column(
Icons.pause, mainAxisAlignment: MainAxisAlignment.center,
size: 48.0, children: [
color: Colors.white, const Icon(
Icons.pause,
size: 48.0,
color: Colors.white,
),
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 24.0,
),
)
],
), ),
Text( ),
title, );
style: const TextStyle(
color: Colors.white,
fontSize: 24.0,
),
)
]),
));
return stackWidgets; return stackWidgets;
} }
@ -502,10 +520,15 @@ class MyCallingPage extends State<Calling> {
} }
if (primaryStream != null) { if (primaryStream != null) {
stackWidgets.add(Center( stackWidgets.add(
child: _StreamView(primaryStream, Center(
mainView: true, matrixClient: widget.client), child: _StreamView(
)); primaryStream,
mainView: true,
matrixClient: widget.client,
),
),
);
} }
if (isFloating || !connected) { if (isFloating || !connected) {
@ -522,47 +545,58 @@ class MyCallingPage extends State<Calling> {
if (call.remoteScreenSharingStream != null) { if (call.remoteScreenSharingStream != null) {
final remoteUserMediaStream = call.remoteUserMediaStream; final remoteUserMediaStream = call.remoteUserMediaStream;
secondaryStreamViews.add(SizedBox( secondaryStreamViews.add(
width: _localVideoWidth, SizedBox(
height: _localVideoHeight, width: _localVideoWidth,
child: _StreamView(remoteUserMediaStream!, matrixClient: widget.client), height: _localVideoHeight,
)); child:
_StreamView(remoteUserMediaStream!, matrixClient: widget.client),
),
);
secondaryStreamViews.add(const SizedBox(height: 10)); secondaryStreamViews.add(const SizedBox(height: 10));
} }
final localStream = final localStream =
call.localUserMediaStream ?? call.localScreenSharingStream; call.localUserMediaStream ?? call.localScreenSharingStream;
if (localStream != null && !isFloating) { if (localStream != null && !isFloating) {
secondaryStreamViews.add(SizedBox( secondaryStreamViews.add(
width: _localVideoWidth, SizedBox(
height: _localVideoHeight, width: _localVideoWidth,
child: _StreamView(localStream, matrixClient: widget.client), height: _localVideoHeight,
)); child: _StreamView(localStream, matrixClient: widget.client),
),
);
secondaryStreamViews.add(const SizedBox(height: 10)); secondaryStreamViews.add(const SizedBox(height: 10));
} }
if (call.localScreenSharingStream != null && !isFloating) { if (call.localScreenSharingStream != null && !isFloating) {
secondaryStreamViews.add(SizedBox( secondaryStreamViews.add(
width: _localVideoWidth, SizedBox(
height: _localVideoHeight, width: _localVideoWidth,
child: _StreamView(call.remoteUserMediaStream!, height: _localVideoHeight,
matrixClient: widget.client), child: _StreamView(
)); call.remoteUserMediaStream!,
matrixClient: widget.client,
),
),
);
secondaryStreamViews.add(const SizedBox(height: 10)); secondaryStreamViews.add(const SizedBox(height: 10));
} }
if (secondaryStreamViews.isNotEmpty) { if (secondaryStreamViews.isNotEmpty) {
stackWidgets.add(Container( stackWidgets.add(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 120), Container(
alignment: Alignment.bottomRight, padding: const EdgeInsets.fromLTRB(0, 20, 0, 120),
child: Container( alignment: Alignment.bottomRight,
width: _localVideoWidth, child: Container(
margin: _localVideoMargin, width: _localVideoWidth,
child: Column( margin: _localVideoMargin,
children: secondaryStreamViews, child: Column(
children: secondaryStreamViews,
),
), ),
), ),
)); );
} }
return stackWidgets; return stackWidgets;
@ -570,27 +604,31 @@ class MyCallingPage extends State<Calling> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PIPView(builder: (context, isFloating) { return PIPView(
return Scaffold( builder: (context, isFloating) {
return Scaffold(
resizeToAvoidBottomInset: !isFloating, resizeToAvoidBottomInset: !isFloating,
floatingActionButtonLocation: floatingActionButtonLocation:
FloatingActionButtonLocation.centerFloat, FloatingActionButtonLocation.centerFloat,
floatingActionButton: SizedBox( floatingActionButton: SizedBox(
width: 320.0, width: 320.0,
height: 150.0, height: 150.0,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: 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(children: [ child: Stack(
..._buildContent(orientation, isFloating), children: [
if (!isFloating) ..._buildContent(orientation, isFloating),
Positioned( if (!isFloating)
Positioned(
top: 24.0, top: 24.0,
left: 24.0, left: 24.0,
child: IconButton( child: IconButton(
@ -599,9 +637,15 @@ class MyCallingPage extends State<Calling> {
onPressed: () { onPressed: () {
PIPView.of(context)?.setFloating(true); PIPView.of(context)?.setFloating(true);
}, },
)) ),
])); )
})); ],
}); ),
);
},
),
);
},
);
} }
} }

View File

@ -198,9 +198,11 @@ class PIPViewState extends State<PIPView> with TickerProviderStateMixin {
: Tween<Offset>( : Tween<Offset>(
begin: _dragOffset, begin: _dragOffset,
end: calculatedOffset, end: calculatedOffset,
).transform(_dragAnimationController.isAnimating ).transform(
? dragAnimationValue _dragAnimationController.isAnimating
: toggleFloatingAnimationValue); ? dragAnimationValue
: toggleFloatingAnimationValue,
);
final borderRadius = Tween<double>( final borderRadius = Tween<double>(
begin: 0, begin: 0,
end: 10, end: 10,

View File

@ -21,52 +21,54 @@ class HomeserverBottomSheet extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: Text(homeserver.homeserver.baseUrl.host), title: Text(homeserver.homeserver.baseUrl.host),
), ),
body: ListView(children: [ body: ListView(
if (description != null && description.isNotEmpty) children: [
if (description != null && description.isNotEmpty)
ListTile(
leading: const Icon(Icons.info_outlined),
title: Text(description),
),
if (jurisdiction != null && jurisdiction.isNotEmpty)
ListTile(
leading: const Icon(Icons.location_city_outlined),
title: Text(jurisdiction),
),
if (homeserverSoftware != null && homeserverSoftware.isNotEmpty)
ListTile(
leading: const Icon(Icons.domain_outlined),
title: Text(homeserverSoftware),
),
ListTile( ListTile(
leading: const Icon(Icons.info_outlined), onTap: () =>
title: Text(description), launchUrlString(homeserver.homeserver.baseUrl.toString()),
leading: const Icon(Icons.link_outlined),
title: Text(homeserver.homeserver.baseUrl.toString()),
), ),
if (jurisdiction != null && jurisdiction.isNotEmpty) if (registration != null)
ListTile( ListTile(
leading: const Icon(Icons.location_city_outlined), onTap: () => launchUrlString(registration.toString()),
title: Text(jurisdiction), leading: const Icon(Icons.person_add_outlined),
), title: Text(registration.toString()),
if (homeserverSoftware != null && homeserverSoftware.isNotEmpty) ),
ListTile( if (rules != null)
leading: const Icon(Icons.domain_outlined), ListTile(
title: Text(homeserverSoftware), onTap: () => launchUrlString(rules.toString()),
), leading: const Icon(Icons.visibility_outlined),
ListTile( title: Text(rules.toString()),
onTap: () => ),
launchUrlString(homeserver.homeserver.baseUrl.toString()), if (privacy != null)
leading: const Icon(Icons.link_outlined), ListTile(
title: Text(homeserver.homeserver.baseUrl.toString()), onTap: () => launchUrlString(privacy.toString()),
), leading: const Icon(Icons.shield_outlined),
if (registration != null) title: Text(privacy.toString()),
ListTile( ),
onTap: () => launchUrlString(registration.toString()), if (responseTime != null)
leading: const Icon(Icons.person_add_outlined), ListTile(
title: Text(registration.toString()), leading: const Icon(Icons.timer_outlined),
), title: Text('${responseTime.inMilliseconds}ms'),
if (rules != null) ),
ListTile( ],
onTap: () => launchUrlString(rules.toString()), ),
leading: const Icon(Icons.visibility_outlined),
title: Text(rules.toString()),
),
if (privacy != null)
ListTile(
onTap: () => launchUrlString(privacy.toString()),
leading: const Icon(Icons.shield_outlined),
title: Text(privacy.toString()),
),
if (responseTime != null)
ListTile(
leading: const Icon(Icons.timer_outlined),
title: Text('${responseTime.inMilliseconds}ms'),
),
]),
); );
} }
} }

View File

@ -51,10 +51,11 @@ class HomeserverPickerController extends State<HomeserverPicker> {
Hive.openBox('test').then((value) => null).catchError( Hive.openBox('test').then((value) => null).catchError(
(e, s) async { (e, s) async {
await showOkAlertDialog( await showOkAlertDialog(
context: context, context: context,
title: L10n.of(context)!.indexedDbErrorTitle, title: L10n.of(context)!.indexedDbErrorTitle,
message: L10n.of(context)!.indexedDbErrorLong, message: L10n.of(context)!.indexedDbErrorLong,
onWillPop: () async => false); onWillPop: () async => false,
);
_checkTorBrowser(); _checkTorBrowser();
}, },
); );
@ -85,9 +86,11 @@ class HomeserverPickerController extends State<HomeserverPicker> {
}); });
List<HomeserverBenchmarkResult> get filteredHomeservers => benchmarkResults! List<HomeserverBenchmarkResult> get filteredHomeservers => benchmarkResults!
.where((element) => .where(
element.homeserver.baseUrl.host.contains(searchTerm) || (element) =>
(element.homeserver.description?.contains(searchTerm) ?? false)) element.homeserver.baseUrl.host.contains(searchTerm) ||
(element.homeserver.description?.contains(searchTerm) ?? false),
)
.toList(); .toList();
void _loadHomeserverList() async { void _loadHomeserverList() async {
@ -186,15 +189,16 @@ class HomeserverPickerController extends State<HomeserverPicker> {
final file = await FilePickerCross.importFromStorage(); final file = await FilePickerCross.importFromStorage();
if (file.fileName == null) return; if (file.fileName == null) return;
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
try { try {
final client = Matrix.of(context).getLoginClient(); final client = Matrix.of(context).getLoginClient();
await client.importDump(file.toString()); await client.importDump(file.toString());
Matrix.of(context).initMatrix(); Matrix.of(context).initMatrix();
} catch (e, s) { } catch (e, s) {
Logs().e('Future error:', e, s); Logs().e('Future error:', e, s);
} }
}); },
);
} }
} }

View File

@ -65,10 +65,11 @@ class HomeserverPickerView extends StatelessWidget {
child: benchmarkResults == null child: benchmarkResults == null
? const Center( ? const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(12.0), padding: EdgeInsets.all(12.0),
child: child: CircularProgressIndicator
CircularProgressIndicator.adaptive(), .adaptive(),
)) ),
)
: Column( : Column(
children: controller.filteredHomeservers children: controller.filteredHomeservers
.map( .map(
@ -82,19 +83,20 @@ class HomeserverPickerView extends StatelessWidget {
.showServerInfo(server), .showServerInfo(server),
), ),
onTap: () => controller.setServer( onTap: () => controller.setServer(
server server.homeserver.baseUrl.host,
.homeserver.baseUrl.host), ),
title: Text( title: Text(
server.homeserver.baseUrl.host, server.homeserver.baseUrl.host,
style: const TextStyle( style: const TextStyle(
color: Colors.black), color: Colors.black,
),
), ),
subtitle: Text( subtitle: Text(
server.homeserver.description ?? server.homeserver.description ??
'', '',
style: TextStyle( style: TextStyle(
color: color: Colors.grey.shade700,
Colors.grey.shade700), ),
), ),
), ),
) )

View File

@ -42,12 +42,13 @@ class ImageViewerView extends StatelessWidget {
if (PlatformInfos.isMobile) if (PlatformInfos.isMobile)
// Use builder context to correctly position the share dialog on iPad // Use builder context to correctly position the share dialog on iPad
Builder( Builder(
builder: (context) => IconButton( builder: (context) => IconButton(
onPressed: () => controller.shareFileAction(context), onPressed: () => controller.shareFileAction(context),
tooltip: L10n.of(context)!.share, tooltip: L10n.of(context)!.share,
color: Colors.white, color: Colors.white,
icon: Icon(Icons.adaptive.share_outlined), icon: Icon(Icons.adaptive.share_outlined),
)) ),
)
], ],
), ),
body: InteractiveViewer( body: InteractiveViewer(

View File

@ -71,8 +71,11 @@ class InvitationSelectionController extends State<InvitationSelection> {
future: () => room.invite(id), future: () => room.invite(id),
); );
if (success.error == null) { if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(
content: Text(L10n.of(context)!.contactHasBeenInvitedToTheGroup))); SnackBar(
content: Text(L10n.of(context)!.contactHasBeenInvitedToTheGroup),
),
);
} }
} }
@ -99,7 +102,8 @@ class InvitationSelectionController extends State<InvitationSelection> {
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(context).showSnackBar(
SnackBar(content: Text((e).toLocalizedString(context)))); SnackBar(content: Text((e).toLocalizedString(context))),
);
return; return;
} finally { } finally {
setState(() => loading = false); setState(() => loading = false);
@ -108,19 +112,25 @@ class InvitationSelectionController extends State<InvitationSelection> {
foundProfiles = List<Profile>.from(response.results); foundProfiles = List<Profile>.from(response.results);
if (text.isValidMatrixId && if (text.isValidMatrixId &&
foundProfiles.indexWhere((profile) => text == profile.userId) == -1) { foundProfiles.indexWhere((profile) => text == profile.userId) == -1) {
setState(() => foundProfiles = [ setState(
Profile.fromJson({'user_id': text}), () => foundProfiles = [
]); Profile.fromJson({'user_id': text}),
],
);
} }
final participants = Matrix.of(context) final participants = Matrix.of(context)
.client .client
.getRoomById(roomId!)! .getRoomById(roomId!)!
.getParticipants() .getParticipants()
.where((user) => .where(
[Membership.join, Membership.invite].contains(user.membership)) (user) =>
[Membership.join, Membership.invite].contains(user.membership),
)
.toList(); .toList();
foundProfiles.removeWhere((profile) => foundProfiles.removeWhere(
participants.indexWhere((u) => u.id == profile.userId) != -1); (profile) =>
participants.indexWhere((u) => u.id == profile.userId) != -1,
);
}); });
} }

View File

@ -75,7 +75,9 @@ class InvitationSelectionView extends StatelessWidget {
), ),
subtitle: Text(controller.foundProfiles[i].userId), subtitle: Text(controller.foundProfiles[i].userId),
onTap: () => controller.inviteAction( onTap: () => controller.inviteAction(
context, controller.foundProfiles[i].userId), context,
controller.foundProfiles[i].userId,
),
), ),
) )
: FutureBuilder<List<User>>( : FutureBuilder<List<User>>(
@ -106,7 +108,8 @@ class InvitationSelectionView extends StatelessWidget {
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.secondary), color: Theme.of(context).colorScheme.secondary,
),
), ),
onTap: () => onTap: () =>
controller.inviteAction(context, contacts[i].id), controller.inviteAction(context, contacts[i].id),

View File

@ -70,19 +70,20 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
if (input.isEmpty) return; if (input.isEmpty) return;
final valid = await showFutureLoadingDialog( final valid = await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
// make sure the loading spinner shows before we test the keys // make sure the loading spinner shows before we test the keys
await Future.delayed(const Duration(milliseconds: 100)); await Future.delayed(const Duration(milliseconds: 100));
var valid = false; var valid = false;
try { try {
await widget.request.openSSSS(keyOrPassphrase: input); await widget.request.openSSSS(keyOrPassphrase: input);
valid = true; valid = true;
} catch (_) { } catch (_) {
valid = false; valid = false;
} }
return valid; return valid;
}); },
);
if (valid.error != null) { if (valid.error != null) {
await showOkAlertDialog( await showOkAlertDialog(
useRootNavigator: false, useRootNavigator: false,
@ -117,8 +118,10 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Text(L10n.of(context)!.askSSSSSign, Text(
style: const TextStyle(fontSize: 20)), L10n.of(context)!.askSSSSSign,
style: const TextStyle(fontSize: 20),
),
Container(height: 10), Container(height: 10),
TextField( TextField(
controller: textEditingController, controller: textEditingController,
@ -141,18 +144,22 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
], ],
), ),
); );
buttons.add(TextButton( buttons.add(
child: Text( TextButton(
L10n.of(context)!.submit, child: Text(
L10n.of(context)!.submit,
),
onPressed: () => checkInput(textEditingController.text),
), ),
onPressed: () => checkInput(textEditingController.text), );
)); buttons.add(
buttons.add(TextButton( TextButton(
child: Text( child: Text(
L10n.of(context)!.skip, L10n.of(context)!.skip,
),
onPressed: () => widget.request.openSSSS(skip: true),
), ),
onPressed: () => widget.request.openSSSS(skip: true), );
));
break; break;
case KeyVerificationState.askAccept: case KeyVerificationState.askAccept:
title = Text(L10n.of(context)!.newVerificationRequest); title = Text(L10n.of(context)!.newVerificationRequest);
@ -171,19 +178,23 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
) )
], ],
); );
buttons.add(TextButton.icon( buttons.add(
icon: const Icon(Icons.close), TextButton.icon(
style: TextButton.styleFrom(foregroundColor: Colors.red), icon: const Icon(Icons.close),
label: Text(L10n.of(context)!.reject), style: TextButton.styleFrom(foregroundColor: Colors.red),
onPressed: () => widget.request label: Text(L10n.of(context)!.reject),
.rejectVerification() onPressed: () => widget.request
.then((_) => Navigator.of(context, rootNavigator: false).pop()), .rejectVerification()
)); .then((_) => Navigator.of(context, rootNavigator: false).pop()),
buttons.add(TextButton.icon( ),
icon: const Icon(Icons.check), );
label: Text(L10n.of(context)!.accept), buttons.add(
onPressed: () => widget.request.acceptVerification(), TextButton.icon(
)); icon: const Icon(Icons.check),
label: Text(L10n.of(context)!.accept),
onPressed: () => widget.request.acceptVerification(),
),
);
break; break;
case KeyVerificationState.waitingAccept: case KeyVerificationState.waitingAccept:
body = Center( body = Center(
@ -245,19 +256,23 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
), ),
], ],
); );
buttons.add(TextButton.icon( buttons.add(
icon: const Icon(Icons.close), TextButton.icon(
style: TextButton.styleFrom( icon: const Icon(Icons.close),
foregroundColor: Colors.red, style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
label: Text(L10n.of(context)!.theyDontMatch),
onPressed: () => widget.request.rejectSas(),
), ),
label: Text(L10n.of(context)!.theyDontMatch), );
onPressed: () => widget.request.rejectSas(), buttons.add(
)); TextButton.icon(
buttons.add(TextButton.icon( icon: const Icon(Icons.check_outlined),
icon: const Icon(Icons.check_outlined), label: Text(L10n.of(context)!.theyMatch),
label: Text(L10n.of(context)!.theyMatch), onPressed: () => widget.request.acceptSas(),
onPressed: () => widget.request.acceptSas(), ),
)); );
break; break;
case KeyVerificationState.waitingSas: case KeyVerificationState.waitingSas:
final acceptText = widget.request.sasTypes.contains('emoji') final acceptText = widget.request.sasTypes.contains('emoji')
@ -279,8 +294,11 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
body = Column( body = Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
const Icon(Icons.check_circle_outlined, const Icon(
color: Colors.green, size: 128.0), Icons.check_circle_outlined,
color: Colors.green,
size: 128.0,
),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
L10n.of(context)!.verifySuccess, L10n.of(context)!.verifySuccess,
@ -288,12 +306,14 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
), ),
], ],
); );
buttons.add(TextButton( buttons.add(
child: Text( TextButton(
L10n.of(context)!.close, child: Text(
L10n.of(context)!.close,
),
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
), ),
onPressed: () => Navigator.of(context, rootNavigator: false).pop(), );
));
break; break;
case KeyVerificationState.error: case KeyVerificationState.error:
body = Column( body = Column(
@ -307,12 +327,14 @@ class KeyVerificationPageState extends State<KeyVerificationDialog> {
), ),
], ],
); );
buttons.add(TextButton( buttons.add(
child: Text( TextButton(
L10n.of(context)!.close, child: Text(
L10n.of(context)!.close,
),
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
), ),
onPressed: () => Navigator.of(context, rootNavigator: false).pop(), );
));
break; break;
} }
return Scaffold( return Scaffold(
@ -350,7 +372,8 @@ class _Emoji extends StatelessWidget {
return emoji.name; return emoji.name;
} }
final translations = Map<String, String?>.from( final translations = Map<String, String?>.from(
sasEmoji[emoji.number]['translated_descriptions']); sasEmoji[emoji.number]['translated_descriptions'],
);
translations['en'] = emoji.name; translations['en'] = emoji.name;
for (final locale in window.locales) { for (final locale in window.locales) {
final wantLocaleParts = locale.toString().split('_'); final wantLocaleParts = locale.toString().split('_');

View File

@ -67,15 +67,17 @@ class LoginController extends State<Login> {
} else { } else {
identifier = AuthenticationUserIdentifier(user: username); identifier = AuthenticationUserIdentifier(user: username);
} }
await matrix.getLoginClient().login(LoginType.mLoginPassword, await matrix.getLoginClient().login(
identifier: identifier, LoginType.mLoginPassword,
// To stay compatible with older server versions identifier: identifier,
// ignore: deprecated_member_use // To stay compatible with older server versions
user: identifier.type == AuthenticationIdentifierTypes.userId // ignore: deprecated_member_use
? username user: identifier.type == AuthenticationIdentifierTypes.userId
: null, ? username
password: passwordController.text, : null,
initialDeviceDisplayName: PlatformInfos.clientName); password: passwordController.text,
initialDeviceDisplayName: PlatformInfos.clientName,
);
} on MatrixException catch (exception) { } on MatrixException catch (exception) {
setState(() => passwordError = exception.errorMessage); setState(() => passwordError = exception.errorMessage);
return setState(() => loading = false); return setState(() => loading = false);
@ -121,7 +123,8 @@ class LoginController extends State<Login> {
Matrix.of(context).getLoginClient().homeserver = oldHomeserver; Matrix.of(context).getLoginClient().homeserver = oldHomeserver;
// okay, the server we checked does not appear to be a matrix server // okay, the server we checked does not appear to be a matrix server
Logs().v( Logs().v(
'$newDomain is not running a homeserver, asking to use $oldHomeserver'); '$newDomain is not running a homeserver, asking to use $oldHomeserver',
);
final dialogResult = await showOkCancelAlertDialog( final dialogResult = await showOkCancelAlertDialog(
context: context, context: context,
useRootNavigator: false, useRootNavigator: false,
@ -230,7 +233,8 @@ class LoginController extends State<Login> {
); );
if (success.error == null) { if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged))); SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)),
);
usernameController.text = input.single; usernameController.text = input.single;
passwordController.text = password.single; passwordController.text = password.single;
login(); login();

View File

@ -19,125 +19,131 @@ class LoginView extends StatelessWidget {
automaticallyImplyLeading: !controller.loading, automaticallyImplyLeading: !controller.loading,
centerTitle: true, centerTitle: true,
title: Text( title: Text(
L10n.of(context)!.logInTo(Matrix.of(context) L10n.of(context)!.logInTo(
.getLoginClient() Matrix.of(context)
.homeserver .getLoginClient()
.toString() .homeserver
.replaceFirst('https://', '')), .toString()
.replaceFirst('https://', ''),
),
), ),
), ),
body: Builder(builder: (context) { body: Builder(
return AutofillGroup( builder: (context) {
child: ListView( return AutofillGroup(
children: <Widget>[ child: ListView(
Padding( children: <Widget>[
padding: const EdgeInsets.all(12.0), Padding(
child: TextField( padding: const EdgeInsets.all(12.0),
readOnly: controller.loading, child: TextField(
autocorrect: false, readOnly: controller.loading,
autofocus: true, autocorrect: false,
onChanged: controller.checkWellKnownWithCoolDown, autofocus: true,
controller: controller.usernameController, onChanged: controller.checkWellKnownWithCoolDown,
textInputAction: TextInputAction.next, controller: controller.usernameController,
keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next,
autofillHints: keyboardType: TextInputType.emailAddress,
controller.loading ? null : [AutofillHints.username], autofillHints:
decoration: InputDecoration( controller.loading ? null : [AutofillHints.username],
prefixIcon: const Icon(Icons.account_box_outlined), decoration: InputDecoration(
errorText: controller.usernameError, prefixIcon: const Icon(Icons.account_box_outlined),
errorStyle: const TextStyle(color: Colors.orange), errorText: controller.usernameError,
hintText: L10n.of(context)!.emailOrUsername, errorStyle: const TextStyle(color: Colors.orange),
), hintText: L10n.of(context)!.emailOrUsername,
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
readOnly: controller.loading,
autocorrect: false,
autofillHints:
controller.loading ? null : [AutofillHints.password],
controller: controller.passwordController,
textInputAction: TextInputAction.go,
obscureText: !controller.showPassword,
onSubmitted: (_) => controller.login(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outlined),
errorText: controller.passwordError,
errorStyle: const TextStyle(color: Colors.orange),
suffixIcon: IconButton(
onPressed: controller.toggleShowPassword,
icon: Icon(
controller.showPassword
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: Colors.black,
),
), ),
hintText: L10n.of(context)!.password,
), ),
), ),
), Padding(
Hero( padding: const EdgeInsets.all(12.0),
tag: 'signinButton', child: TextField(
child: Padding( readOnly: controller.loading,
autocorrect: false,
autofillHints:
controller.loading ? null : [AutofillHints.password],
controller: controller.passwordController,
textInputAction: TextInputAction.go,
obscureText: !controller.showPassword,
onSubmitted: (_) => controller.login(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outlined),
errorText: controller.passwordError,
errorStyle: const TextStyle(color: Colors.orange),
suffixIcon: IconButton(
onPressed: controller.toggleShowPassword,
icon: Icon(
controller.showPassword
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
color: Colors.black,
),
),
hintText: L10n.of(context)!.password,
),
),
),
Hero(
tag: 'signinButton',
child: Padding(
padding: const EdgeInsets.all(12.0),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
),
onPressed: controller.loading ? null : controller.login,
icon: const Icon(Icons.login_outlined),
label: controller.loading
? const LinearProgressIndicator()
: Text(L10n.of(context)!.login),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Expanded(
child: Divider(
thickness: 1,
color: Theme.of(context).dividerColor,
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.or,
style: const TextStyle(fontSize: 18),
),
),
Expanded(
child: Divider(
thickness: 1,
color: Theme.of(context).dividerColor,
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: controller.loading
? () {}
: controller.passwordForgotten,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onPrimary, backgroundColor: Theme.of(context).colorScheme.onError,
), ),
onPressed: controller.loading ? null : controller.login, icon: const Icon(Icons.safety_check_outlined),
icon: const Icon(Icons.login_outlined), label: Text(L10n.of(context)!.passwordForgotten),
label: controller.loading
? const LinearProgressIndicator()
: Text(L10n.of(context)!.login),
), ),
), ),
), ],
Padding( ),
padding: const EdgeInsets.symmetric(horizontal: 16.0), );
child: Row( },
children: [ ),
Expanded(
child: Divider(
thickness: 1,
color: Theme.of(context).dividerColor,
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
L10n.of(context)!.or,
style: const TextStyle(fontSize: 18),
),
),
Expanded(
child: Divider(
thickness: 1,
color: Theme.of(context).dividerColor,
),
),
],
),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: ElevatedButton.icon(
onPressed:
controller.loading ? () {} : controller.passwordForgotten,
style: ElevatedButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
backgroundColor: Theme.of(context).colorScheme.onError,
),
icon: const Icon(Icons.safety_check_outlined),
label: Text(L10n.of(context)!.passwordForgotten),
),
),
],
),
);
}),
); );
} }
} }

View File

@ -29,9 +29,10 @@ class NewGroupView extends StatelessWidget {
textInputAction: TextInputAction.go, textInputAction: TextInputAction.go,
onSubmitted: controller.submitAction, onSubmitted: controller.submitAction,
decoration: InputDecoration( decoration: InputDecoration(
labelText: L10n.of(context)!.optionalGroupName, labelText: L10n.of(context)!.optionalGroupName,
prefixIcon: const Icon(Icons.people_outlined), prefixIcon: const Icon(Icons.people_outlined),
hintText: L10n.of(context)!.enterAGroupName), hintText: L10n.of(context)!.enterAGroupName,
),
), ),
), ),
SwitchListTile.adaptive( SwitchListTile.adaptive(

View File

@ -29,9 +29,10 @@ class NewSpaceView extends StatelessWidget {
textInputAction: TextInputAction.go, textInputAction: TextInputAction.go,
onSubmitted: controller.submitAction, onSubmitted: controller.submitAction,
decoration: InputDecoration( decoration: InputDecoration(
labelText: L10n.of(context)!.spaceName, labelText: L10n.of(context)!.spaceName,
prefixIcon: const Icon(Icons.people_outlined), prefixIcon: const Icon(Icons.people_outlined),
hintText: L10n.of(context)!.enterASpacepName), hintText: L10n.of(context)!.enterASpacepName,
),
), ),
), ),
SwitchListTile.adaptive( SwitchListTile.adaptive(

View File

@ -40,98 +40,100 @@ class SettingsView extends StatelessWidget {
key: const Key('SettingsListViewContent'), key: const Key('SettingsListViewContent'),
children: <Widget>[ children: <Widget>[
FutureBuilder<Profile>( FutureBuilder<Profile>(
future: controller.profileFuture, future: controller.profileFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
final profile = snapshot.data; final profile = snapshot.data;
final mxid = Matrix.of(context).client.userID ?? final mxid =
L10n.of(context)!.user; Matrix.of(context).client.userID ?? L10n.of(context)!.user;
final displayname = final displayname =
profile?.displayName ?? mxid.localpart ?? mxid; profile?.displayName ?? mxid.localpart ?? mxid;
return Row( return Row(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(32.0), padding: const EdgeInsets.all(32.0),
child: Stack( child: Stack(
children: [ children: [
Material( Material(
elevation: Theme.of(context) elevation: Theme.of(context)
.appBarTheme .appBarTheme
.scrolledUnderElevation ?? .scrolledUnderElevation ??
4, 4,
shadowColor: shadowColor:
Theme.of(context).appBarTheme.shadowColor, Theme.of(context).appBarTheme.shadowColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: BorderSide( side: BorderSide(
color: Theme.of(context).dividerColor, color: Theme.of(context).dividerColor,
),
borderRadius: BorderRadius.circular(
Avatar.defaultSize * 2.5),
), ),
child: Avatar( borderRadius: BorderRadius.circular(
mxContent: profile?.avatarUrl, Avatar.defaultSize * 2.5,
name: displayname,
size: Avatar.defaultSize * 2.5,
fontSize: 18 * 2.5,
), ),
), ),
if (profile != null) child: Avatar(
Positioned( mxContent: profile?.avatarUrl,
bottom: 0, name: displayname,
right: 0, size: Avatar.defaultSize * 2.5,
child: FloatingActionButton.small( fontSize: 18 * 2.5,
onPressed: controller.setAvatarAction, ),
heroTag: null, ),
child: const Icon(Icons.camera_alt_outlined), if (profile != null)
), Positioned(
bottom: 0,
right: 0,
child: FloatingActionButton.small(
onPressed: controller.setAvatarAction,
heroTag: null,
child: const Icon(Icons.camera_alt_outlined),
), ),
], ),
), ],
), ),
Expanded( ),
child: Column( Expanded(
mainAxisAlignment: MainAxisAlignment.center, child: Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center,
children: [ crossAxisAlignment: CrossAxisAlignment.start,
TextButton.icon( children: [
onPressed: controller.setDisplaynameAction, TextButton.icon(
icon: const Icon( onPressed: controller.setDisplaynameAction,
Icons.edit_outlined, icon: const Icon(
size: 16, Icons.edit_outlined,
), size: 16,
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.onBackground,
),
label: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 18),
),
), ),
TextButton.icon( style: TextButton.styleFrom(
onPressed: () => FluffyShare.share(mxid, context), foregroundColor:
icon: const Icon( Theme.of(context).colorScheme.onBackground,
Icons.copy_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.secondary,
),
label: Text(
mxid,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 12),
),
), ),
], label: Text(
), displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 18),
),
),
TextButton.icon(
onPressed: () => FluffyShare.share(mxid, context),
icon: const Icon(
Icons.copy_outlined,
size: 14,
),
style: TextButton.styleFrom(
foregroundColor:
Theme.of(context).colorScheme.secondary,
),
label: Text(
mxid,
maxLines: 1,
overflow: TextOverflow.ellipsis,
// style: const TextStyle(fontSize: 12),
),
),
],
), ),
], ),
); ],
}), );
},
),
const Divider(thickness: 1), const Divider(thickness: 1),
if (showChatBackupBanner == null) if (showChatBackupBanner == null)
ListTile( ListTile(

View File

@ -79,11 +79,12 @@ class Settings3PidController extends State<Settings3Pid> {
return; return;
} }
final success = await showFutureLoadingDialog( final success = await showFutureLoadingDialog(
context: context, context: context,
future: () => Matrix.of(context).client.delete3pidFromAccount( future: () => Matrix.of(context).client.delete3pidFromAccount(
identifier.address, identifier.address,
identifier.medium, identifier.medium,
)); ),
);
if (success.error != null) return; if (success.error != null) return;
setState(() => request = null); setState(() => request = null);
} }

View File

@ -30,8 +30,10 @@ class Settings3PidView extends StatelessWidget {
body: MaxWidthBody( body: MaxWidthBody(
child: FutureBuilder<List<ThirdPartyIdentifier>?>( child: FutureBuilder<List<ThirdPartyIdentifier>?>(
future: controller.request, future: controller.request,
builder: (BuildContext context, builder: (
AsyncSnapshot<List<ThirdPartyIdentifier>?> snapshot) { BuildContext context,
AsyncSnapshot<List<ThirdPartyIdentifier>?> snapshot,
) {
if (snapshot.hasError) { if (snapshot.hasError) {
return Center( return Center(
child: Text( child: Text(
@ -42,7 +44,8 @@ class Settings3PidView extends StatelessWidget {
} }
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Center( return const Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2)); child: CircularProgressIndicator.adaptive(strokeWidth: 2),
);
} }
final identifier = snapshot.data!; final identifier = snapshot.data!;
return Column( return Column(
@ -71,10 +74,11 @@ class Settings3PidView extends StatelessWidget {
itemCount: identifier.length, itemCount: identifier.length,
itemBuilder: (BuildContext context, int i) => ListTile( itemBuilder: (BuildContext context, int i) => ListTile(
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: backgroundColor:
Theme.of(context).scaffoldBackgroundColor, Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Colors.grey, foregroundColor: Colors.grey,
child: Icon(identifier[i].iconData)), child: Icon(identifier[i].iconData),
),
title: Text(identifier[i].address), title: Text(identifier[i].address),
trailing: IconButton( trailing: IconButton(
tooltip: L10n.of(context)!.delete, tooltip: L10n.of(context)!.delete,

View File

@ -60,13 +60,20 @@ class EmotesSettingsController extends State<EmotesSettings> {
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () => client.setRoomStateWithKey( future: () => client.setRoomStateWithKey(
room!.id, 'im.ponies.room_emotes', stateKey ?? '', pack!.toJson()), room!.id,
'im.ponies.room_emotes',
stateKey ?? '',
pack!.toJson(),
),
); );
} else { } else {
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () => client.setAccountData( future: () => client.setAccountData(
client.userID!, 'im.ponies.user_emotes', pack!.toJson()), client.userID!,
'im.ponies.user_emotes',
pack!.toJson(),
),
); );
} }
} }
@ -95,7 +102,10 @@ class EmotesSettingsController extends State<EmotesSettings> {
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () => client.setAccountData( future: () => client.setAccountData(
client.userID!, 'im.ponies.emote_rooms', content), client.userID!,
'im.ponies.emote_rooms',
content,
),
); );
setState(() {}); setState(() {});
} }
@ -197,7 +207,8 @@ class EmotesSettingsController extends State<EmotesSettings> {
} }
void imagePickerAction( void imagePickerAction(
ValueNotifier<ImagePackImageContent?> controller) async { ValueNotifier<ImagePackImageContent?> controller,
) async {
final result = final result =
await FilePickerCross.importFromStorage(type: FileTypeCross.image); await FilePickerCross.importFromStorage(type: FileTypeCross.image);
if (result.fileName == null) return; if (result.fileName == null) return;

View File

@ -140,16 +140,17 @@ class EmotesSettingsView extends StatelessWidget {
actions: !useShortCuts actions: !useShortCuts
? {} ? {}
: { : {
SubmitLineIntent: SubmitLineIntent: CallbackAction(
CallbackAction(onInvoke: (i) { onInvoke: (i) {
controller.submitImageAction( controller.submitImageAction(
imageCode, imageCode,
textEditingController.text, textEditingController.text,
image, image,
textEditingController, textEditingController,
); );
return null; return null;
}), },
),
}, },
child: TextField( child: TextField(
readOnly: controller.readonly, readOnly: controller.readonly,

View File

@ -58,34 +58,36 @@ class SettingsIgnoreListView extends StatelessWidget {
const Divider(height: 1), const Divider(height: 1),
Expanded( Expanded(
child: StreamBuilder<Object>( child: StreamBuilder<Object>(
stream: client.onAccountData.stream stream: client.onAccountData.stream
.where((a) => a.type == 'm.ignored_user_list'), .where((a) => a.type == 'm.ignored_user_list'),
builder: (context, snapshot) { builder: (context, snapshot) {
return ListView.builder( return ListView.builder(
itemCount: client.ignoredUsers.length, itemCount: client.ignoredUsers.length,
itemBuilder: (c, i) => FutureBuilder<Profile>( itemBuilder: (c, i) => FutureBuilder<Profile>(
future: future:
client.getProfileFromUserId(client.ignoredUsers[i]), client.getProfileFromUserId(client.ignoredUsers[i]),
builder: (c, s) => ListTile( builder: (c, s) => ListTile(
leading: Avatar( leading: Avatar(
mxContent: s.data?.avatarUrl ?? Uri.parse(''), mxContent: s.data?.avatarUrl ?? Uri.parse(''),
name: s.data?.displayName ?? client.ignoredUsers[i], name: s.data?.displayName ?? client.ignoredUsers[i],
), ),
title: Text( title: Text(
s.data?.displayName ?? client.ignoredUsers[i]), s.data?.displayName ?? client.ignoredUsers[i],
trailing: IconButton( ),
tooltip: L10n.of(context)!.delete, trailing: IconButton(
icon: const Icon(Icons.delete_forever_outlined), tooltip: L10n.of(context)!.delete,
onPressed: () => showFutureLoadingDialog( icon: const Icon(Icons.delete_forever_outlined),
context: context, onPressed: () => showFutureLoadingDialog(
future: () => context: context,
client.unignoreUser(client.ignoredUsers[i]), future: () =>
), client.unignoreUser(client.ignoredUsers[i]),
), ),
), ),
), ),
); ),
}), );
},
),
), ),
], ],
), ),

View File

@ -32,27 +32,28 @@ class MultipleEmotesSettingsView extends StatelessWidget {
final keys = packs.keys.toList(); final keys = packs.keys.toList();
keys.sort(); keys.sort();
return ListView.separated( return ListView.separated(
separatorBuilder: (BuildContext context, int i) => Container(), separatorBuilder: (BuildContext context, int i) => Container(),
itemCount: keys.length, itemCount: keys.length,
itemBuilder: (BuildContext context, int i) { itemBuilder: (BuildContext context, int i) {
final event = packs[keys[i]]; final event = packs[keys[i]];
String? packName = String? packName = keys[i].isNotEmpty ? keys[i] : 'Default Pack';
keys[i].isNotEmpty ? keys[i] : 'Default Pack'; if (event != null && event.content['pack'] is Map) {
if (event != null && event.content['pack'] is Map) { if (event.content['pack']['displayname'] is String) {
if (event.content['pack']['displayname'] is String) { packName = event.content['pack']['displayname'];
packName = event.content['pack']['displayname']; } else if (event.content['pack']['name'] is String) {
} else if (event.content['pack']['name'] is String) { packName = event.content['pack']['name'];
packName = event.content['pack']['name'];
}
} }
return ListTile( }
title: Text(packName!), return ListTile(
onTap: () async { title: Text(packName!),
VRouter.of(context).toSegments( onTap: () async {
['rooms', room.id, 'details', 'emotes', keys[i]]); VRouter.of(context).toSegments(
}, ['rooms', room.id, 'details', 'emotes', keys[i]],
); );
}); },
);
},
);
}, },
), ),
); );

View File

@ -25,90 +25,94 @@ class SettingsNotificationsView extends StatelessWidget {
body: MaxWidthBody( body: MaxWidthBody(
withScrolling: true, withScrolling: true,
child: StreamBuilder( child: StreamBuilder(
stream: Matrix.of(context) stream: Matrix.of(context)
.client .client
.onAccountData .onAccountData
.stream .stream
.where((event) => event.type == 'm.push_rules'), .where((event) => event.type == 'm.push_rules'),
builder: (BuildContext context, _) { builder: (BuildContext context, _) {
return Column( return Column(
children: [ children: [
SwitchListTile.adaptive( SwitchListTile.adaptive(
value: !Matrix.of(context).client.allPushNotificationsMuted, value: !Matrix.of(context).client.allPushNotificationsMuted,
title: Text( title: Text(
L10n.of(context)!.notificationsEnabledForThisAccount), L10n.of(context)!.notificationsEnabledForThisAccount,
onChanged: (_) => showFutureLoadingDialog(
context: context,
future: () =>
Matrix.of(context).client.setMuteAllPushNotifications(
!Matrix.of(context)
.client
.allPushNotificationsMuted,
),
),
), ),
if (!Matrix.of(context).client.allPushNotificationsMuted) ...{ onChanged: (_) => showFutureLoadingDialog(
const Divider(thickness: 1), context: context,
ListTile( future: () => Matrix.of(context)
title: Text( .client
L10n.of(context)!.pushRules, .setMuteAllPushNotifications(
style: TextStyle( !Matrix.of(context).client.allPushNotificationsMuted,
color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold,
), ),
), ),
), ),
for (var item in NotificationSettingsItem.items) if (!Matrix.of(context).client.allPushNotificationsMuted) ...{
SwitchListTile.adaptive(
value: controller.getNotificationSetting(item) ?? true,
title: Text(item.title(context)),
onChanged: (bool enabled) =>
controller.setNotificationSetting(item, enabled),
),
},
const Divider(thickness: 1), const Divider(thickness: 1),
ListTile( ListTile(
title: Text( title: Text(
L10n.of(context)!.devices, L10n.of(context)!.pushRules,
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
FutureBuilder<List<Pusher>?>( for (var item in NotificationSettingsItem.items)
future: controller.pusherFuture ??= SwitchListTile.adaptive(
Matrix.of(context).client.getPushers(), value: controller.getNotificationSetting(item) ?? true,
builder: (context, snapshot) { title: Text(item.title(context)),
if (snapshot.hasError) { onChanged: (bool enabled) =>
Center( controller.setNotificationSetting(item, enabled),
child: Text( ),
snapshot.error!.toLocalizedString(context), },
), const Divider(thickness: 1),
); ListTile(
} title: Text(
if (snapshot.connectionState != ConnectionState.done) { L10n.of(context)!.devices,
const Center( style: TextStyle(
child: CircularProgressIndicator.adaptive( color: Theme.of(context).colorScheme.secondary,
strokeWidth: 2)); fontWeight: FontWeight.bold,
} ),
final pushers = snapshot.data ?? []; ),
return ListView.builder( ),
physics: const NeverScrollableScrollPhysics(), FutureBuilder<List<Pusher>?>(
shrinkWrap: true, future: controller.pusherFuture ??=
itemCount: pushers.length, Matrix.of(context).client.getPushers(),
itemBuilder: (_, i) => ListTile( builder: (context, snapshot) {
title: Text( if (snapshot.hasError) {
'${pushers[i].appDisplayName} - ${pushers[i].appId}'), Center(
subtitle: Text(pushers[i].data.url.toString()), child: Text(
onTap: () => controller.onPusherTap(pushers[i]), snapshot.error!.toLocalizedString(context),
), ),
); );
}, }
), if (snapshot.connectionState != ConnectionState.done) {
], const Center(
); child: CircularProgressIndicator.adaptive(
}), strokeWidth: 2,
),
);
}
final pushers = snapshot.data ?? [];
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: pushers.length,
itemBuilder: (_, i) => ListTile(
title: Text(
'${pushers[i].appDisplayName} - ${pushers[i].appId}',
),
subtitle: Text(pushers[i].data.url.toString()),
onTap: () => controller.onPusherTap(pushers[i]),
),
);
},
),
],
);
},
),
), ),
); );
} }

View File

@ -56,7 +56,8 @@ class SettingsSecurityController extends State<SettingsSecurity> {
); );
if (success.error == null) { if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged))); SnackBar(content: Text(L10n.of(context)!.passwordHasBeenChanged)),
);
} }
} }
@ -151,7 +152,8 @@ class SettingsSecurityController extends State<SettingsSecurity> {
auth: AuthenticationPassword( auth: AuthenticationPassword(
password: input.single, password: input.single,
identifier: AuthenticationUserIdentifier( identifier: AuthenticationUserIdentifier(
user: Matrix.of(context).client.userID!), user: Matrix.of(context).client.userID!,
),
), ),
), ),
); );
@ -181,10 +183,11 @@ class SettingsSecurityController extends State<SettingsSecurity> {
try { try {
final export = await Matrix.of(context).client.exportDump(); final export = await Matrix.of(context).client.exportDump();
final filePickerCross = FilePickerCross( final filePickerCross = FilePickerCross(
Uint8List.fromList(const Utf8Codec().encode(export!)), Uint8List.fromList(const Utf8Codec().encode(export!)),
path: path:
'/fluffychat-export-${DateFormat(DateFormat.YEAR_MONTH_DAY).format(DateTime.now())}.fluffybackup', '/fluffychat-export-${DateFormat(DateFormat.YEAR_MONTH_DAY).format(DateTime.now())}.fluffybackup',
fileExtension: 'fluffybackup'); fileExtension: 'fluffybackup',
);
await filePickerCross.exportToStorage( await filePickerCross.exportToStorage(
subject: L10n.of(context)!.dehydrateShare, subject: L10n.of(context)!.dehydrateShare,
); );

View File

@ -32,14 +32,15 @@ class SettingsStoriesController extends State<SettingsStories> {
final blockList = room.client.storiesBlockList; final blockList = room.client.storiesBlockList;
blockList.add(user.id); blockList.add(user.id);
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
await user.kick(); await user.kick();
await room.client.setStoriesBlockList(blockList.toSet().toList()); await room.client.setStoriesBlockList(blockList.toSet().toList());
setState(() { setState(() {
users[user] = false; users[user] = false;
});
}); });
},
);
return; return;
} }
@ -47,14 +48,15 @@ class SettingsStoriesController extends State<SettingsStories> {
final blockList = room.client.storiesBlockList; final blockList = room.client.storiesBlockList;
blockList.remove(user.id); blockList.remove(user.id);
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () async { future: () async {
await room.client.setStoriesBlockList(blockList); await room.client.setStoriesBlockList(blockList);
await room.invite(user.id); await room.invite(user.id);
setState(() { setState(() {
users[user] = true; users[user] = true;
});
}); });
},
);
return; return;
} }

View File

@ -38,9 +38,10 @@ class SettingsStoriesView extends StatelessWidget {
} }
if (snapshot.connectionState != ConnectionState.done) { if (snapshot.connectionState != ConnectionState.done) {
return const Center( return const Center(
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(
strokeWidth: 2, strokeWidth: 2,
)); ),
);
} }
return ListView.builder( return ListView.builder(
itemCount: controller.users.length, itemCount: controller.users.length,

View File

@ -59,16 +59,18 @@ class SettingsStyleView extends StatelessWidget {
borderRadius: borderRadius:
BorderRadius.circular(colorPickerSize), BorderRadius.circular(colorPickerSize),
child: SizedBox( child: SizedBox(
width: colorPickerSize, width: colorPickerSize,
height: colorPickerSize, height: colorPickerSize,
child: controller.currentColor == color child: controller.currentColor == color
? const Center( ? const Center(
child: Icon( child: Icon(
Icons.check, Icons.check,
size: 16, size: 16,
color: Colors.white, color: Colors.white,
)) ),
: null), )
: null,
),
), ),
), ),
), ),
@ -118,16 +120,18 @@ class SettingsStyleView extends StatelessWidget {
), ),
onTap: controller.deleteWallpaperAction, onTap: controller.deleteWallpaperAction,
), ),
Builder(builder: (context) { Builder(
return ListTile( builder: (context) {
title: Text(L10n.of(context)!.changeWallpaper), return ListTile(
trailing: Icon( title: Text(L10n.of(context)!.changeWallpaper),
Icons.photo_outlined, trailing: Icon(
color: Theme.of(context).textTheme.bodyLarge?.color, Icons.photo_outlined,
), color: Theme.of(context).textTheme.bodyLarge?.color,
onTap: controller.setWallpaperAction, ),
); onTap: controller.setWallpaperAction,
}), );
},
),
const Divider(height: 1), const Divider(height: 1),
ListTile( ListTile(
title: Text( title: Text(

View File

@ -46,10 +46,11 @@ class StoryPageController extends State<StoryPage> {
Timeline? timeline; Timeline? timeline;
Event? get currentEvent => index < events.length ? events[index] : null; Event? get currentEvent => index < events.length ? events[index] : null;
StoryThemeData get storyThemeData => StoryThemeData get storyThemeData => StoryThemeData.fromJson(
StoryThemeData.fromJson(currentEvent?.content currentEvent?.content
.tryGetMap<String, dynamic>(StoryThemeData.contentKey) ?? .tryGetMap<String, dynamic>(StoryThemeData.contentKey) ??
{}); {},
);
bool replyLoading = false; bool replyLoading = false;
bool _modalOpened = false; bool _modalOpened = false;
@ -83,8 +84,9 @@ class StoryPageController extends State<StoryPage> {
final client = Matrix.of(context).client; final client = Matrix.of(context).client;
final roomId = await client.startDirectChat(currentEvent.senderId); final roomId = await client.startDirectChat(currentEvent.senderId);
var replyText = L10n.of(context)!.storyFrom( var replyText = L10n.of(context)!.storyFrom(
currentEvent.originServerTs.localizedTime(context), currentEvent.originServerTs.localizedTime(context),
currentEvent.content.tryGet<String>('body') ?? ''); currentEvent.content.tryGet<String>('body') ?? '',
);
replyText = replyText.split('\n').map((line) => '> $line').join('\n'); replyText = replyText.split('\n').map((line) => '> $line').join('\n');
message = '$replyText\n\n$message'; message = '$replyText\n\n$message';
await client.getRoomById(roomId)!.sendTextEvent(message); await client.getRoomById(roomId)!.sendTextEvent(message);
@ -307,33 +309,35 @@ class StoryPageController extends State<StoryPage> {
final event = currentEvent; final event = currentEvent;
if (event == null) return; if (event == null) return;
final score = await showConfirmationDialog<int>( final score = await showConfirmationDialog<int>(
context: context, context: context,
title: L10n.of(context)!.reportMessage, title: L10n.of(context)!.reportMessage,
message: L10n.of(context)!.howOffensiveIsThisContent, message: L10n.of(context)!.howOffensiveIsThisContent,
cancelLabel: L10n.of(context)!.cancel, cancelLabel: L10n.of(context)!.cancel,
okLabel: L10n.of(context)!.ok, okLabel: L10n.of(context)!.ok,
actions: [ actions: [
AlertDialogAction( AlertDialogAction(
key: -100, key: -100,
label: L10n.of(context)!.extremeOffensive, label: L10n.of(context)!.extremeOffensive,
), ),
AlertDialogAction( AlertDialogAction(
key: -50, key: -50,
label: L10n.of(context)!.offensive, label: L10n.of(context)!.offensive,
), ),
AlertDialogAction( AlertDialogAction(
key: 0, key: 0,
label: L10n.of(context)!.inoffensive, label: L10n.of(context)!.inoffensive,
), ),
]); ],
);
if (score == null) return; if (score == null) return;
final reason = await showTextInputDialog( final reason = await showTextInputDialog(
useRootNavigator: false, useRootNavigator: false,
context: context, context: context,
title: L10n.of(context)!.whyDoYouWantToReportThis, title: L10n.of(context)!.whyDoYouWantToReportThis,
okLabel: L10n.of(context)!.ok, okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel, cancelLabel: L10n.of(context)!.cancel,
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)]); textFields: [DialogTextField(hintText: L10n.of(context)!.reason)],
);
if (reason == null || reason.single.isEmpty) return; if (reason == null || reason.single.isEmpty) return;
final result = await showFutureLoadingDialog( final result = await showFutureLoadingDialog(
context: context, context: context,
@ -352,7 +356,9 @@ class StoryPageController extends State<StoryPage> {
} }
Future<MatrixFile> downloadAndDecryptAttachment( Future<MatrixFile> downloadAndDecryptAttachment(
Event event, bool getThumbnail) async { Event event,
bool getThumbnail,
) async {
return _fileCache[event.eventId] ??= return _fileCache[event.eventId] ??=
event.downloadAndDecryptAttachment(getThumbnail: getThumbnail); event.downloadAndDecryptAttachment(getThumbnail: getThumbnail);
} }
@ -400,10 +406,12 @@ class StoryPageController extends State<StoryPage> {
final timeline = this.timeline = await room.getTimeline(); final timeline = this.timeline = await room.getTimeline();
timeline.requestKeys(); timeline.requestKeys();
var events = timeline.events var events = timeline.events
.where((e) => .where(
e.type == EventTypes.Message && (e) =>
!e.redacted && e.type == EventTypes.Message &&
e.status == EventStatus.synced) !e.redacted &&
e.status == EventStatus.synced,
)
.toList(); .toList();
final hasOutdatedEvents = events.removeOutdatedEvents(); final hasOutdatedEvents = events.removeOutdatedEvents();
@ -432,12 +440,16 @@ class StoryPageController extends State<StoryPage> {
// Preload images and videos // Preload images and videos
events events
.where((event) => {MessageTypes.Image, MessageTypes.Video} .where(
.contains(event.messageType)) (event) => {MessageTypes.Image, MessageTypes.Video}
.forEach((event) => downloadAndDecryptAttachment( .contains(event.messageType),
)
.forEach(
(event) => downloadAndDecryptAttachment(
event, event,
event.messageType == MessageTypes.Video && event.messageType == MessageTypes.Video && PlatformInfos.isMobile,
PlatformInfos.isMobile)); ),
);
// Reverse list // Reverse list
this.events.clear(); this.events.clear();
@ -502,9 +514,11 @@ class StoryPageController extends State<StoryPage> {
extension on List<Event> { extension on List<Event> {
bool removeOutdatedEvents() { bool removeOutdatedEvents() {
final outdatedIndex = indexWhere((event) => final outdatedIndex = indexWhere(
DateTime.now().difference(event.originServerTs).inHours > (event) =>
ClientStoriesExtension.lifeTimeInHours); DateTime.now().difference(event.originServerTs).inHours >
ClientStoriesExtension.lifeTimeInHours,
);
if (outdatedIndex != -1) { if (outdatedIndex != -1) {
removeRange(outdatedIndex, length); removeRange(outdatedIndex, length);
return true; return true;

View File

@ -144,9 +144,10 @@ class StoryView extends StatelessWidget {
final events = controller.events; final events = controller.events;
if (snapshot.connectionState != ConnectionState.done) { if (snapshot.connectionState != ConnectionState.done) {
return const Center( return const Center(
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(
strokeWidth: 2, strokeWidth: 2,
)); ),
);
} }
if (events.isEmpty) { if (events.isEmpty) {
return Center( return Center(
@ -218,7 +219,9 @@ class StoryView extends StatelessWidget {
!PlatformInfos.isMobile)) !PlatformInfos.isMobile))
FutureBuilder<MatrixFile>( FutureBuilder<MatrixFile>(
future: controller.downloadAndDecryptAttachment( future: controller.downloadAndDecryptAttachment(
event, event.messageType == MessageTypes.Video), event,
event.messageType == MessageTypes.Video,
),
builder: (context, snapshot) { builder: (context, snapshot) {
final matrixFile = snapshot.data; final matrixFile = snapshot.data;
if (matrixFile == null) { if (matrixFile == null) {
@ -364,7 +367,8 @@ class StoryView extends StatelessWidget {
height: 16, height: 16,
child: Center( child: Center(
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(
strokeWidth: 2), strokeWidth: 2,
),
), ),
) )
: IconButton( : IconButton(

View File

@ -52,33 +52,35 @@ class UserBottomSheetController extends State<UserBottomSheet> {
case UserBottomSheetAction.report: case UserBottomSheetAction.report:
final event = widget.user; final event = widget.user;
final score = await showConfirmationDialog<int>( final score = await showConfirmationDialog<int>(
context: context, context: context,
title: L10n.of(context)!.reportUser, title: L10n.of(context)!.reportUser,
message: L10n.of(context)!.howOffensiveIsThisContent, message: L10n.of(context)!.howOffensiveIsThisContent,
cancelLabel: L10n.of(context)!.cancel, cancelLabel: L10n.of(context)!.cancel,
okLabel: L10n.of(context)!.ok, okLabel: L10n.of(context)!.ok,
actions: [ actions: [
AlertDialogAction( AlertDialogAction(
key: -100, key: -100,
label: L10n.of(context)!.extremeOffensive, label: L10n.of(context)!.extremeOffensive,
), ),
AlertDialogAction( AlertDialogAction(
key: -50, key: -50,
label: L10n.of(context)!.offensive, label: L10n.of(context)!.offensive,
), ),
AlertDialogAction( AlertDialogAction(
key: 0, key: 0,
label: L10n.of(context)!.inoffensive, label: L10n.of(context)!.inoffensive,
), ),
]); ],
);
if (score == null) return; if (score == null) return;
final reason = await showTextInputDialog( final reason = await showTextInputDialog(
useRootNavigator: false, useRootNavigator: false,
context: context, context: context,
title: L10n.of(context)!.whyDoYouWantToReportThis, title: L10n.of(context)!.whyDoYouWantToReportThis,
okLabel: L10n.of(context)!.ok, okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel, cancelLabel: L10n.of(context)!.cancel,
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)]); textFields: [DialogTextField(hintText: L10n.of(context)!.reason)],
);
if (reason == null || reason.single.isEmpty) return; if (reason == null || reason.single.isEmpty) return;
final result = await showFutureLoadingDialog( final result = await showFutureLoadingDialog(
context: context, context: context,
@ -91,7 +93,8 @@ class UserBottomSheetController extends State<UserBottomSheet> {
); );
if (result.error != null) return; if (result.error != null) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported))); SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)),
);
break; break;
case UserBottomSheetAction.mention: case UserBottomSheetAction.mention:
Navigator.of(context, rootNavigator: false).pop(); Navigator.of(context, rootNavigator: false).pop();
@ -151,9 +154,9 @@ class UserBottomSheetController extends State<UserBottomSheet> {
case UserBottomSheetAction.ignore: case UserBottomSheetAction.ignore:
if (await askConfirmation()) { if (await askConfirmation()) {
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () => future: () => Matrix.of(context).client.ignoreUser(widget.user.id),
Matrix.of(context).client.ignoreUser(widget.user.id)); );
} }
} }
} }

View File

@ -55,10 +55,12 @@ extension AccountBundlesExtension on Client {
} }
ret ??= []; ret ??= [];
if (ret.isEmpty) { if (ret.isEmpty) {
ret.add(AccountBundle( ret.add(
name: userID, AccountBundle(
priority: 0, name: userID,
)); priority: 0,
),
);
} }
return ret; return ret;
} }

View File

@ -78,7 +78,8 @@ class BackgroundPush {
firebase?.setListeners( firebase?.setListeners(
onMessage: (message) => pushHelper( onMessage: (message) => pushHelper(
PushNotification.fromJson( PushNotification.fromJson(
Map<String, dynamic>.from(message['data'] ?? message)), Map<String, dynamic>.from(message['data'] ?? message),
),
client: client, client: client,
l10n: l10n, l10n: l10n,
activeRoomId: router?.currentState?.pathParameters['roomid'], activeRoomId: router?.currentState?.pathParameters['roomid'],
@ -331,7 +332,8 @@ class BackgroundPush {
} }
} catch (e) { } catch (e) {
Logs().i( Logs().i(
'[Push] No self-hosted unified push gateway present: $newEndpoint'); '[Push] No self-hosted unified push gateway present: $newEndpoint',
);
} }
Logs().i('[Push] UnifiedPush using endpoint $endpoint'); Logs().i('[Push] UnifiedPush using endpoint $endpoint');
final oldTokens = <String?>{}; final oldTokens = <String?>{};
@ -366,7 +368,8 @@ class BackgroundPush {
Future<void> _onUpMessage(Uint8List message, String i) async { Future<void> _onUpMessage(Uint8List message, String i) async {
upAction = true; upAction = true;
final data = Map<String, dynamic>.from( final data = Map<String, dynamic>.from(
json.decode(utf8.decode(message))['notification']); json.decode(utf8.decode(message))['notification'],
);
// UP may strip the devices list // UP may strip the devices list
data['devices'] ??= []; data['devices'] ??= [];
await pushHelper( await pushHelper(
@ -382,8 +385,11 @@ class BackgroundPush {
/// IDs we map the [roomId] to a number and store this number. /// IDs we map the [roomId] to a number and store this number.
late Map<String, int> idMap; late Map<String, int> idMap;
Future<void> _loadIdMap() async { Future<void> _loadIdMap() async {
idMap = Map<String, int>.from(json.decode( idMap = Map<String, int>.from(
(await store.getItem(SettingKeys.notificationCurrentIds)) ?? '{}')); json.decode(
(await store.getItem(SettingKeys.notificationCurrentIds)) ?? '{}',
),
);
} }
Future<int> mapRoomIdToInt(String roomId) async { Future<int> mapRoomIdToInt(String roomId) async {
@ -441,7 +447,8 @@ class BackgroundPush {
if (syncErrored) { if (syncErrored) {
try { try {
Logs().v( Logs().v(
'[Push] failed to sync for fallback push, fetching notifications endpoint...'); '[Push] failed to sync for fallback push, fetching notifications endpoint...',
);
final notifications = await client.getNotifications(limit: 20); final notifications = await client.getNotifications(limit: 20);
final notificationRooms = final notificationRooms =
notifications.notifications.map((n) => n.roomId).toSet(); notifications.notifications.map((n) => n.roomId).toSet();
@ -450,8 +457,9 @@ class BackgroundPush {
.map((r) => r.id); .map((r) => r.id);
} catch (e) { } catch (e) {
Logs().v( Logs().v(
'[Push] failed to fetch pending notifications for clearing push, falling back...', '[Push] failed to fetch pending notifications for clearing push, falling back...',
e); e,
);
emptyRooms = client.rooms emptyRooms = client.rooms
.where((r) => r.notificationCount == 0) .where((r) => r.notificationCount == 0)
.map((r) => r.id); .map((r) => r.id);
@ -474,7 +482,9 @@ class BackgroundPush {
} }
if (changed) { if (changed) {
await store.setItem( await store.setItem(
SettingKeys.notificationCurrentIds, json.encode(idMap)); SettingKeys.notificationCurrentIds,
json.encode(idMap),
);
} }
} finally { } finally {
_clearingPushLock = false; _clearingPushLock = false;

View File

@ -39,19 +39,25 @@ abstract class ClientManager {
} }
final clients = clientNames.map(createClient).toList(); final clients = clientNames.map(createClient).toList();
if (initialize) { if (initialize) {
await Future.wait(clients.map((client) => client await Future.wait(
.init( clients.map(
waitForFirstSync: false, (client) => client
waitUntilLoadCompletedLoaded: false, .init(
) waitForFirstSync: false,
.catchError( waitUntilLoadCompletedLoaded: false,
(e, s) => Logs().e('Unable to initialize client', e, s)))); )
.catchError(
(e, s) => Logs().e('Unable to initialize client', e, s),
),
),
);
} }
if (clients.length > 1 && clients.any((c) => !c.isLogged())) { if (clients.length > 1 && clients.any((c) => !c.isLogged())) {
final loggedOutClients = clients.where((c) => !c.isLogged()).toList(); final loggedOutClients = clients.where((c) => !c.isLogged()).toList();
for (final client in loggedOutClients) { for (final client in loggedOutClients) {
Logs().w( Logs().w(
'Multi account is enabled but client ${client.userID} is not logged in. Removing...'); 'Multi account is enabled but client ${client.userID} is not logged in. Removing...',
);
clientNames.remove(client.clientName); clientNames.remove(client.clientName);
clients.remove(client); clients.remove(client);
} }

View File

@ -5,7 +5,8 @@ import 'package:matrix/matrix.dart';
import 'package:native_imaging/native_imaging.dart' as native; import 'package:native_imaging/native_imaging.dart' as native;
Future<MatrixImageFileResizedResponse?> customImageResizer( Future<MatrixImageFileResizedResponse?> customImageResizer(
MatrixImageFileResizeArguments arguments) async { MatrixImageFileResizeArguments arguments,
) async {
await native.init(); await native.init();
late native.Image nativeImg; late native.Image nativeImg;
@ -21,7 +22,10 @@ Future<MatrixImageFileResizedResponse?> customImageResizer(
return null; return null;
} }
final rgba = Uint8List.view( final rgba = Uint8List.view(
rgbaData.buffer, rgbaData.offsetInBytes, rgbaData.lengthInBytes); rgbaData.buffer,
rgbaData.offsetInBytes,
rgbaData.lengthInBytes,
);
final width = dartFrame.image.width; final width = dartFrame.image.width;
final height = dartFrame.image.height; final height = dartFrame.image.height;

View File

@ -77,10 +77,15 @@ extension DateTimeExtension on DateTime {
} }
} else if (sameYear) { } else if (sameYear) {
return L10n.of(context)!.dateWithoutYear( return L10n.of(context)!.dateWithoutYear(
month.toString().padLeft(2, '0'), day.toString().padLeft(2, '0')); month.toString().padLeft(2, '0'),
day.toString().padLeft(2, '0'),
);
} }
return L10n.of(context)!.dateWithYear(year.toString(), return L10n.of(context)!.dateWithYear(
month.toString().padLeft(2, '0'), day.toString().padLeft(2, '0')); year.toString(),
month.toString().padLeft(2, '0'),
day.toString().padLeft(2, '0'),
);
} }
/// If the DateTime is today, this returns [localizedTimeOfDay()], if not it also /// If the DateTime is today, this returns [localizedTimeOfDay()], if not it also
@ -95,7 +100,9 @@ extension DateTimeExtension on DateTime {
if (sameDay) return localizedTimeOfDay(context); if (sameDay) return localizedTimeOfDay(context);
return L10n.of(context)!.dateAndTimeOfDay( return L10n.of(context)!.dateAndTimeOfDay(
localizedTimeShort(context), localizedTimeOfDay(context)); localizedTimeShort(context),
localizedTimeOfDay(context),
);
} }
static String _z(int i) => i < 10 ? '0${i.toString()}' : i.toString(); static String _z(int i) => i < 10 ? '0${i.toString()}' : i.toString();

View File

@ -19,7 +19,8 @@ abstract class FluffyShare {
ClipboardData(text: text), ClipboardData(text: text),
); );
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context)!.copiedToClipboard))); SnackBar(content: Text(L10n.of(context)!.copiedToClipboard)),
);
return; return;
} }
} }

View File

@ -15,8 +15,10 @@ extension ClientStoriesExtension on Client {
List<User> get contacts => rooms List<User> get contacts => rooms
.where((room) => room.isDirectChat) .where((room) => room.isDirectChat)
.map((room) => .map(
room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!)) (room) =>
room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!),
)
.toList(); .toList();
List<Room> get storiesRooms => List<Room> get storiesRooms =>
@ -78,23 +80,30 @@ extension ClientStoriesExtension on Client {
} }
Future<Room?> getStoriesRoom(BuildContext context) async { Future<Room?> getStoriesRoom(BuildContext context) async {
final candidates = rooms.where((room) => final candidates = rooms.where(
room.getState(EventTypes.RoomCreate)?.content.tryGet<String>('type') == (room) =>
storiesRoomType && room
room.ownPowerLevel >= 100); .getState(EventTypes.RoomCreate)
?.content
.tryGet<String>('type') ==
storiesRoomType &&
room.ownPowerLevel >= 100,
);
if (candidates.isEmpty) return null; if (candidates.isEmpty) return null;
if (candidates.length == 1) return candidates.single; if (candidates.length == 1) return candidates.single;
return await showModalActionSheet<Room>( return await showModalActionSheet<Room>(
context: context, context: context,
actions: candidates actions: candidates
.map( .map(
(room) => SheetAction( (room) => SheetAction(
label: room.getLocalizedDisplayname( label: room.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!), MatrixLocals(L10n.of(context)!),
), ),
key: room), key: room,
) ),
.toList()); )
.toList(),
);
} }
} }

View File

@ -23,7 +23,8 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase {
static const String _cipherStorageKey = 'hive_encryption_key'; static const String _cipherStorageKey = 'hive_encryption_key';
static Future<FlutterHiveCollectionsDatabase> databaseBuilder( static Future<FlutterHiveCollectionsDatabase> databaseBuilder(
Client client) async { Client client,
) async {
Logs().d('Open Hive...'); Logs().d('Open Hive...');
HiveAesCipher? hiverCipher; HiveAesCipher? hiverCipher;
try { try {
@ -96,9 +97,9 @@ class FlutterHiveCollectionsDatabase extends HiveCollectionsDatabase {
} }
} }
// do not destroy your stable FluffyChat in debug mode // do not destroy your stable FluffyChat in debug mode
directory = Directory(directory.uri directory = Directory(
.resolve(kDebugMode ? 'hive_debug' : 'hive') directory.uri.resolve(kDebugMode ? 'hive_debug' : 'hive').toFilePath(),
.toFilePath()); );
directory.create(recursive: true); directory.create(recursive: true);
path = directory.path; path = directory.path;
} }

View File

@ -8,8 +8,9 @@ extension IosBadgeClientExtension on Client {
void updateIosBadge() { void updateIosBadge() {
if (PlatformInfos.isIOS) { if (PlatformInfos.isIOS) {
// Workaround for iOS not clearing notifications with fcm_shared_isolate // Workaround for iOS not clearing notifications with fcm_shared_isolate
if (!rooms.any((r) => if (!rooms.any(
r.membership == Membership.invite || (r.notificationCount > 0))) { (r) => r.membership == Membership.invite || (r.notificationCount > 0),
)) {
// ignore: unawaited_futures // ignore: unawaited_futures
FlutterLocalNotificationsPlugin().cancelAll(); FlutterLocalNotificationsPlugin().cancelAll();
FlutterAppBadger.removeBadge(); FlutterAppBadger.removeBadge();

View File

@ -62,7 +62,9 @@ class MatrixLocals extends MatrixLocalizations {
@override @override
String changedTheGuestAccessRulesTo( String changedTheGuestAccessRulesTo(
String senderName, String localizedString) { String senderName,
String localizedString,
) {
return l10n.changedTheGuestAccessRulesTo(senderName, localizedString); return l10n.changedTheGuestAccessRulesTo(senderName, localizedString);
} }
@ -73,7 +75,9 @@ class MatrixLocals extends MatrixLocalizations {
@override @override
String changedTheHistoryVisibilityTo( String changedTheHistoryVisibilityTo(
String senderName, String localizedString) { String senderName,
String localizedString,
) {
return l10n.changedTheHistoryVisibilityTo(senderName, localizedString); return l10n.changedTheHistoryVisibilityTo(senderName, localizedString);
} }

View File

@ -111,7 +111,9 @@ Future<void> _tryPushHelper(
await flutterLocalNotificationsPlugin.cancelAll(); await flutterLocalNotificationsPlugin.cancelAll();
final store = await SharedPreferences.getInstance(); final store = await SharedPreferences.getInstance();
await store.setString( await store.setString(
SettingKeys.notificationCurrentIds, json.encode({})); SettingKeys.notificationCurrentIds,
json.encode({}),
);
} }
} }
return; return;
@ -237,7 +239,8 @@ Future<void> _tryPushHelper(
Future<int> mapRoomIdToInt(String roomId) async { Future<int> mapRoomIdToInt(String roomId) async {
final store = await SharedPreferences.getInstance(); final store = await SharedPreferences.getInstance();
final idMap = Map<String, int>.from( final idMap = Map<String, int>.from(
jsonDecode(store.getString(SettingKeys.notificationCurrentIds) ?? '{}')); jsonDecode(store.getString(SettingKeys.notificationCurrentIds) ?? '{}'),
);
int? currentInt; int? currentInt;
try { try {
currentInt = idMap[roomId]; currentInt = idMap[roomId];

View File

@ -54,12 +54,14 @@ extension RoomStatusExtension on Room {
} }
} else if (typingUsers.length == 2) { } else if (typingUsers.length == 2) {
typingText = L10n.of(context)!.userAndUserAreTyping( typingText = L10n.of(context)!.userAndUserAreTyping(
typingUsers.first.calcDisplayname(), typingUsers.first.calcDisplayname(),
typingUsers[1].calcDisplayname()); typingUsers[1].calcDisplayname(),
);
} else if (typingUsers.length > 2) { } else if (typingUsers.length > 2) {
typingText = L10n.of(context)!.userAndOthersAreTyping( typingText = L10n.of(context)!.userAndOthersAreTyping(
typingUsers.first.calcDisplayname(), typingUsers.first.calcDisplayname(),
(typingUsers.length - 1).toString()); (typingUsers.length - 1).toString(),
);
} }
return typingText; return typingText;
} }
@ -76,8 +78,10 @@ extension RoomStatusExtension on Room {
break; break;
} }
} }
lastReceipts.removeWhere((user) => lastReceipts.removeWhere(
user.id == client.userID || user.id == timeline.events.first.senderId); (user) =>
user.id == client.userID || user.id == timeline.events.first.senderId,
);
return lastReceipts.toList(); return lastReceipts.toList();
} }
} }

View File

@ -33,9 +33,11 @@ extension StreamExtension on Stream {
gotMessage = true; gotMessage = true;
} }
}; };
final subscription = listen((_) => onMessage?.call(), final subscription = listen(
onDone: () => controller.close(), (_) => onMessage?.call(),
onError: (e, s) => controller.addError(e, s)); onDone: () => controller.close(),
onError: (e, s) => controller.addError(e, s),
);
// add proper cleanup to the subscription and the controller, to not memory leak // add proper cleanup to the subscription and the controller, to not memory leak
controller.onCancel = () { controller.onCancel = () {
subscription.cancel(); subscription.cancel();

View File

@ -82,7 +82,8 @@ extension UiaRequestManager on MatrixState {
); );
default: default:
final url = Uri.parse( final url = Uri.parse(
'${client.homeserver}/_matrix/client/r0/auth/$stage/fallback/web?session=${uiaRequest.session}'); '${client.homeserver}/_matrix/client/r0/auth/$stage/fallback/web?session=${uiaRequest.session}',
);
launchUrlString(url.toString()); launchUrlString(url.toString());
if (OkCancelResult.ok == if (OkCancelResult.ok ==
await showOkCancelAlertDialog( await showOkCancelAlertDialog(

View File

@ -20,7 +20,8 @@ class UpdateCheckerNoStore {
static const gitLabHost = 'gitlab.com'; static const gitLabHost = 'gitlab.com';
static Uri get tagsUri => Uri.parse( static Uri get tagsUri => Uri.parse(
'https://$gitLabHost/projects/$gitLabProjectId/repository/tags'); 'https://$gitLabHost/projects/$gitLabProjectId/repository/tags',
);
final BuildContext context; final BuildContext context;

View File

@ -33,7 +33,8 @@ class UrlLauncher {
if (uri == null) { if (uri == null) {
// we can't open this thing // we can't open this thing
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!)))); SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))),
);
return; return;
} }
if (!{'https', 'http'}.contains(uri.scheme)) { if (!{'https', 'http'}.contains(uri.scheme)) {
@ -61,7 +62,8 @@ class UrlLauncher {
// transmute geo URIs on desktop to openstreetmap links, as those usually can't handle // transmute geo URIs on desktop to openstreetmap links, as those usually can't handle
// geo URIs // geo URIs
launchUrlString( launchUrlString(
'https://www.openstreetmap.org/?mlat=${latlong.first}&mlon=${latlong.last}#map=16/${latlong.first}/${latlong.last}'); 'https://www.openstreetmap.org/?mlat=${latlong.first}&mlon=${latlong.last}#map=16/${latlong.first}/${latlong.last}',
);
} }
return; return;
} }
@ -71,7 +73,8 @@ class UrlLauncher {
} }
if (uri.host.isEmpty) { if (uri.host.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!)))); SnackBar(content: Text(L10n.of(context)!.cantOpenUri(url!))),
);
return; return;
} }
// okay, we have either an http or an https URI. // okay, we have either an http or an https URI.
@ -86,8 +89,10 @@ class UrlLauncher {
}).join('.'); }).join('.');
// Force LaunchMode.externalApplication, otherwise url_launcher will default // Force LaunchMode.externalApplication, otherwise url_launcher will default
// to opening links in a webview on mobile platforms. // to opening links in a webview on mobile platforms.
launchUrlString(uri.replace(host: newHost).toString(), launchUrlString(
mode: LaunchMode.externalApplication); uri.replace(host: newHost).toString(),
mode: LaunchMode.externalApplication,
);
} }
void openMatrixToUrl() async { void openMatrixToUrl() async {
@ -142,8 +147,10 @@ class UrlLauncher {
} }
// we have the room, so....just open it // we have the room, so....just open it
if (event != null) { if (event != null) {
VRouter.of(context).toSegments(['rooms', room.id], VRouter.of(context).toSegments(
queryParameters: {'event': event}); ['rooms', room.id],
queryParameters: {'event': event},
);
} else { } else {
VRouter.of(context).toSegments(['rooms', room.id]); VRouter.of(context).toSegments(['rooms', room.id]);
} }
@ -175,11 +182,14 @@ class UrlLauncher {
if (response.error != null) return; if (response.error != null) return;
// wait for two seconds so that it probably came down /sync // wait for two seconds so that it probably came down /sync
await showFutureLoadingDialog( await showFutureLoadingDialog(
context: context, context: context,
future: () => Future.delayed(const Duration(seconds: 2))); future: () => Future.delayed(const Duration(seconds: 2)),
);
if (event != null) { if (event != null) {
VRouter.of(context).toSegments(['rooms', response.result!], VRouter.of(context).toSegments(
queryParameters: {'event': event}); ['rooms', response.result!],
queryParameters: {'event': event},
);
} else { } else {
VRouter.of(context).toSegments(['rooms', response.result!]); VRouter.of(context).toSegments(['rooms', response.result!]);
} }

View File

@ -111,14 +111,15 @@ class CallKeepManager {
Future<void> showCallkitIncoming(CallSession call) async { Future<void> showCallkitIncoming(CallSession call) async {
if (!setupDone) { if (!setupDone) {
await _callKeep.setup( await _callKeep.setup(
null, null,
<String, dynamic>{ <String, dynamic>{
'ios': <String, dynamic>{ 'ios': <String, dynamic>{
'appName': appName, 'appName': appName,
},
'android': alertOptions,
}, },
backgroundMode: true); 'android': alertOptions,
},
backgroundMode: true,
);
} }
setupDone = true; setupDone = true;
await displayIncomingCall(call); await displayIncomingCall(call);
@ -131,7 +132,8 @@ class CallKeepManager {
(event) { (event) {
if (event == CallEvent.kLocalHoldUnhold) { if (event == CallEvent.kLocalHoldUnhold) {
Logs().i( Logs().i(
'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}'); 'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}',
);
} }
}, },
); );
@ -169,10 +171,14 @@ class CallKeepManager {
_callKeep.on(CallKeepPerformAnswerCallAction(), answerCall); _callKeep.on(CallKeepPerformAnswerCallAction(), answerCall);
_callKeep.on(CallKeepDidPerformDTMFAction(), didPerformDTMFAction); _callKeep.on(CallKeepDidPerformDTMFAction(), didPerformDTMFAction);
_callKeep.on( _callKeep.on(
CallKeepDidReceiveStartCallAction(), didReceiveStartCallAction); CallKeepDidReceiveStartCallAction(),
didReceiveStartCallAction,
);
_callKeep.on(CallKeepDidToggleHoldAction(), didToggleHoldCallAction); _callKeep.on(CallKeepDidToggleHoldAction(), didToggleHoldCallAction);
_callKeep.on( _callKeep.on(
CallKeepDidPerformSetMutedCallAction(), didPerformSetMutedCallAction); CallKeepDidPerformSetMutedCallAction(),
didPerformSetMutedCallAction,
);
_callKeep.on(CallKeepPerformEndCallAction(), endCall); _callKeep.on(CallKeepPerformEndCallAction(), endCall);
_callKeep.on(CallKeepPushKitToken(), onPushKitToken); _callKeep.on(CallKeepPushKitToken(), onPushKitToken);
_callKeep.on(CallKeepDidDisplayIncomingCall(), didDisplayIncomingCall); _callKeep.on(CallKeepDidDisplayIncomingCall(), didDisplayIncomingCall);
@ -209,11 +215,17 @@ class CallKeepManager {
Future<void> updateDisplay(String callUUID) async { Future<void> updateDisplay(String callUUID) async {
// Workaround because Android doesn't display well displayName, se we have to switch ... // Workaround because Android doesn't display well displayName, se we have to switch ...
if (isIOS) { if (isIOS) {
await _callKeep.updateDisplay(callUUID, await _callKeep.updateDisplay(
displayName: 'New Name', handle: callUUID); callUUID,
displayName: 'New Name',
handle: callUUID,
);
} else { } else {
await _callKeep.updateDisplay(callUUID, await _callKeep.updateDisplay(
displayName: callUUID, handle: 'New Name'); callUUID,
displayName: callUUID,
handle: 'New Name',
);
} }
} }
@ -250,7 +262,8 @@ class CallKeepManager {
const Divider(), const Divider(),
ListTile( ListTile(
onTap: () => FlutterForegroundTask.openSystemAlertWindowSettings( onTap: () => FlutterForegroundTask.openSystemAlertWindowSettings(
forceOpen: true), forceOpen: true,
),
title: Text(L10n.of(context)!.appearOnTop), title: Text(L10n.of(context)!.appearOnTop),
subtitle: Text(L10n.of(context)!.appearOnTopDetails), subtitle: Text(L10n.of(context)!.appearOnTopDetails),
trailing: const Icon(Icons.file_upload_rounded), trailing: const Icon(Icons.file_upload_rounded),
@ -310,7 +323,8 @@ class CallKeepManager {
} }
Future<void> didReceiveStartCallAction( Future<void> didReceiveStartCallAction(
CallKeepDidReceiveStartCallAction event) async { CallKeepDidReceiveStartCallAction event,
) async {
if (event.handle == null) { if (event.handle == null) {
// @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined` // @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined`
return; return;
@ -328,7 +342,8 @@ class CallKeepManager {
} }
Future<void> didPerformSetMutedCallAction( Future<void> didPerformSetMutedCallAction(
CallKeepDidPerformSetMutedCallAction event) async { CallKeepDidPerformSetMutedCallAction event,
) async {
final keeper = calls[event.callUUID]; final keeper = calls[event.callUUID];
if (event.muted!) { if (event.muted!) {
keeper!.call.setMicrophoneMuted(true); keeper!.call.setMicrophoneMuted(true);
@ -339,7 +354,8 @@ class CallKeepManager {
} }
Future<void> didToggleHoldCallAction( Future<void> didToggleHoldCallAction(
CallKeepDidToggleHoldAction event) async { CallKeepDidToggleHoldAction event,
) async {
final keeper = calls[event.callUUID]; final keeper = calls[event.callUUID];
if (event.hold!) { if (event.hold!) {
keeper!.call.setRemoteOnHold(true); keeper!.call.setRemoteOnHold(true);

View File

@ -82,14 +82,15 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
} else { } else {
overlayEntry = OverlayEntry( overlayEntry = OverlayEntry(
builder: (_) => Calling( builder: (_) => Calling(
context: context, context: context,
client: client, client: client,
callId: callId, callId: callId,
call: call, call: call,
onClear: () { onClear: () {
overlayEntry?.remove(); overlayEntry?.remove();
overlayEntry = null; overlayEntry = null;
}), },
),
); );
Overlay.of(context).insert(overlayEntry!); Overlay.of(context).insert(overlayEntry!);
} }
@ -103,8 +104,9 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
@override @override
Future<RTCPeerConnection> createPeerConnection( Future<RTCPeerConnection> createPeerConnection(
Map<String, dynamic> configuration, Map<String, dynamic> configuration, [
[Map<String, dynamic> constraints = const {}]) => Map<String, dynamic> constraints = const {},
]) =>
webrtc_impl.createPeerConnection(configuration, constraints); webrtc_impl.createPeerConnection(configuration, constraints);
@override @override
@ -150,7 +152,9 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
try { try {
final wasForeground = await FlutterForegroundTask.isAppOnForeground; final wasForeground = await FlutterForegroundTask.isAppOnForeground;
await Store().setItem( await Store().setItem(
'wasForeground', wasForeground == true ? 'true' : 'false'); 'wasForeground',
wasForeground == true ? 'true' : 'false',
);
FlutterForegroundTask.setOnLockScreenVisibility(true); FlutterForegroundTask.setOnLockScreenVisibility(true);
FlutterForegroundTask.wakeUpScreen(); FlutterForegroundTask.wakeUpScreen();
FlutterForegroundTask.launchApp(); FlutterForegroundTask.launchApp();
@ -162,10 +166,13 @@ class VoipPlugin with WidgetsBindingObserver implements WebRTCDelegate {
try { try {
if (!hasCallingAccount) { if (!hasCallingAccount) {
ScaffoldMessenger.of(FluffyChatApp.routerKey.currentContext!) ScaffoldMessenger.of(FluffyChatApp.routerKey.currentContext!)
.showSnackBar(const SnackBar( .showSnackBar(
content: Text( const SnackBar(
'No calling accounts found (used for native calls UI)', content: Text(
))); 'No calling accounts found (used for native calls UI)',
),
),
);
} }
} catch (e) { } catch (e) {
Logs().e('failed to show snackbar'); Logs().e('failed to show snackbar');

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