refactor: Migrate to null safety

This commit is contained in:
Krille Fear 2022-01-29 12:35:03 +01:00 committed by Christian Pauly
commit 55f0300f9f
163 changed files with 1759 additions and 1903 deletions

View file

@ -34,31 +34,31 @@ import 'send_location_dialog.dart';
import 'sticker_picker_dialog.dart';
class Chat extends StatefulWidget {
final Widget sideView;
final Widget? sideView;
const Chat({Key key, this.sideView}) : super(key: key);
const Chat({Key? key, this.sideView}) : super(key: key);
@override
ChatController createState() => ChatController();
}
class ChatController extends State<Chat> {
Room room;
Room? room;
Client sendingClient;
Client? sendingClient;
Timeline timeline;
Timeline? timeline;
MatrixState matrix;
MatrixState? matrix;
String get roomId => context.vRouter.pathParameters['roomid'];
String? get roomId => context.vRouter.pathParameters['roomid'];
final AutoScrollController scrollController = AutoScrollController();
FocusNode inputFocus = FocusNode();
Timer typingCoolDown;
Timer typingTimeout;
Timer? typingCoolDown;
Timer? typingTimeout;
bool currentlyTyping = false;
bool dragging = false;
@ -76,7 +76,7 @@ class ChatController extends State<Chat> {
bytes: bytes,
name: xfile.name,
).detectFileType,
room: room,
room: room!,
),
);
}
@ -96,13 +96,13 @@ class ChatController extends State<Chat> {
List<Event> selectedEvents = [];
List<Event> filteredEvents;
late List<Event> filteredEvents;
final Set<String> unfolded = {};
Event replyEvent;
Event? replyEvent;
Event editEvent;
Event? editEvent;
bool showScrollDownButton = false;
@ -115,8 +115,8 @@ class ChatController extends State<Chat> {
String pendingText = '';
bool get canLoadMore =>
timeline.events.isEmpty ||
timeline.events.last.type != EventTypes.RoomCreate;
timeline!.events.isEmpty ||
timeline!.events.last.type != EventTypes.RoomCreate;
bool showEmojiPicker = false;
@ -126,7 +126,7 @@ class ChatController extends State<Chat> {
final success = await showFutureLoadingDialog(
context: context,
future: () => room.sendEvent({
future: () => room!.sendEvent({
'msgtype': Matrix.callNamespace,
'body': url,
}));
@ -137,12 +137,12 @@ class ChatController extends State<Chat> {
void requestHistory() async {
if (canLoadMore) {
try {
await timeline.requestHistory(historyCount: _loadHistoryCount);
await timeline!.requestHistory(historyCount: _loadHistoryCount);
} catch (err) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
(err as Object).toLocalizedString(context),
(err).toLocalizedString(context),
),
),
);
@ -157,8 +157,8 @@ class ChatController extends State<Chat> {
}
if (scrollController.position.pixels ==
scrollController.position.maxScrollExtent &&
timeline.events.isNotEmpty &&
timeline.events[timeline.events.length - 1].type !=
timeline!.events.isNotEmpty &&
timeline!.events[timeline!.events.length - 1].type !=
EventTypes.RoomCreate) {
requestHistory();
}
@ -180,7 +180,7 @@ class ChatController extends State<Chat> {
if (!mounted) return;
setState(
() {
filteredEvents = timeline.getFilteredEvents(unfolded: unfolded);
filteredEvents = timeline!.getFilteredEvents(unfolded: unfolded);
},
);
}
@ -192,22 +192,22 @@ class ChatController extends State<Chat> {
unfolded.add(filteredEvents[i].eventId);
i++;
}
filteredEvents = timeline.getFilteredEvents(unfolded: unfolded);
filteredEvents = timeline!.getFilteredEvents(unfolded: unfolded);
});
}
Future<bool> getTimeline() async {
if (timeline == null) {
timeline = await room.getTimeline(onUpdate: updateView);
if (timeline.events.isNotEmpty) {
timeline = await room!.getTimeline(onUpdate: updateView);
if (timeline!.events.isNotEmpty) {
// ignore: unawaited_futures
if (room.markedUnread) room.markUnread(false);
if (room!.markedUnread) room!.markUnread(false);
}
// when the scroll controller is attached we want to scroll to an event id, if specified
// and update the scroll controller...which will trigger a request history, if the
// "load more" button is visible on the screen
SchedulerBinding.instance.addPostFrameCallback((_) async {
SchedulerBinding.instance!.addPostFrameCallback((_) async {
if (mounted) {
final event = VRouter.of(context).queryParameters['event'];
if (event != null) {
@ -217,16 +217,15 @@ class ChatController extends State<Chat> {
}
});
}
filteredEvents = timeline.getFilteredEvents(unfolded: unfolded);
timeline.requestKeys();
if (room.notificationCount != null &&
room.notificationCount > 0 &&
filteredEvents = timeline!.getFilteredEvents(unfolded: unfolded);
timeline!.requestKeys();
if (room!.notificationCount > 0 &&
timeline != null &&
timeline.events.isNotEmpty &&
timeline!.events.isNotEmpty &&
Matrix.of(context).webHasFocus) {
// ignore: unawaited_futures
timeline.setReadMarker();
room.client.updateIosBadge();
timeline!.setReadMarker();
room!.client.updateIosBadge();
}
return true;
}
@ -240,13 +239,13 @@ class ChatController extends State<Chat> {
TextEditingController sendController = TextEditingController();
void setSendingClient(Client c) {
void setSendingClient(Client? c) {
// first cancle typing with the old sending client
if (currentlyTyping) {
// no need to have the setting typing to false be blocking
typingCoolDown?.cancel();
typingCoolDown = null;
room.setTyping(false);
room!.setTyping(false);
currentlyTyping = false;
}
// then set the new sending client
@ -263,22 +262,22 @@ class ChatController extends State<Chat> {
final commandMatch = RegExp(r'^\/(\w+)').firstMatch(sendController.text);
if (commandMatch != null &&
!room.client.commands.keys.contains(commandMatch[1].toLowerCase())) {
final l10n = L10n.of(context);
!room!.client.commands.keys.contains(commandMatch[1]!.toLowerCase())) {
final l10n = L10n.of(context)!;
final dialogResult = await showOkCancelAlertDialog(
context: context,
useRootNavigator: false,
title: l10n.commandInvalid,
message: l10n.commandMissing(commandMatch[0]),
message: l10n.commandMissing(commandMatch[0]!),
okLabel: l10n.sendAsText,
cancelLabel: l10n.cancel,
);
if (dialogResult == null || dialogResult == OkCancelResult.cancel) return;
if (dialogResult == OkCancelResult.cancel) return;
parseCommands = false;
}
// ignore: unawaited_futures
room.sendTextEvent(sendController.text,
room!.sendTextEvent(sendController.text,
inReplyTo: replyEvent,
editEventId: editEvent?.eventId,
parseCommands: parseCommands);
@ -298,16 +297,16 @@ class ChatController extends State<Chat> {
void sendFileAction() async {
final result =
await FilePickerCross.importFromStorage(type: FileTypeCross.any);
if (result == null) return;
if (result.fileName == null) return;
await showDialog(
context: context,
useRootNavigator: false,
builder: (c) => SendFileDialog(
file: MatrixFile(
bytes: result.toUint8List(),
name: result.fileName,
name: result.fileName!,
).detectFileType,
room: room,
room: room!,
),
);
}
@ -315,16 +314,16 @@ class ChatController extends State<Chat> {
void sendImageAction() async {
final result =
await FilePickerCross.importFromStorage(type: FileTypeCross.image);
if (result == null) return;
if (result.fileName == null) return;
await showDialog(
context: context,
useRootNavigator: false,
builder: (c) => SendFileDialog(
file: MatrixImageFile(
bytes: result.toUint8List(),
name: result.fileName,
name: result.fileName!,
),
room: room,
room: room!,
),
);
}
@ -343,7 +342,7 @@ class ChatController extends State<Chat> {
bytes: bytes,
name: file.path,
),
room: room,
room: room!,
),
);
}
@ -362,7 +361,7 @@ class ChatController extends State<Chat> {
bytes: bytes,
name: file.path,
),
room: room,
room: room!,
),
);
}
@ -371,7 +370,7 @@ class ChatController extends State<Chat> {
final sticker = await showModalBottomSheet<ImagePackImageContent>(
context: context,
useRootNavigator: false,
builder: (c) => StickerPickerDialog(room: room),
builder: (c) => StickerPickerDialog(room: room!),
);
if (sticker == null) return;
final eventContent = <String, dynamic>{
@ -382,7 +381,7 @@ class ChatController extends State<Chat> {
// send the sticker
await showFutureLoadingDialog(
context: context,
future: () => room.sendEvent(
future: () => room!.sendEvent(
eventContent,
type: EventTypes.Sticker,
),
@ -405,7 +404,7 @@ class ChatController extends State<Chat> {
await showFutureLoadingDialog(
context: context,
future: () =>
room.sendFileEvent(file, inReplyTo: replyEvent, extraContent: {
room!.sendFileEvent(file, inReplyTo: replyEvent, extraContent: {
'info': {
...file.info,
'duration': result.duration,
@ -426,7 +425,7 @@ class ChatController extends State<Chat> {
await showDialog(
context: context,
useRootNavigator: false,
builder: (c) => SendLocationDialog(room: room),
builder: (c) => SendLocationDialog(room: room!),
);
}
@ -434,13 +433,13 @@ class ChatController extends State<Chat> {
var copyString = '';
if (selectedEvents.length == 1) {
return selectedEvents.first
.getDisplayEvent(timeline)
.getLocalizedBody(MatrixLocals(L10n.of(context)));
.getDisplayEvent(timeline!)
.getLocalizedBody(MatrixLocals(L10n.of(context)!));
}
for (final event in selectedEvents) {
if (copyString.isNotEmpty) copyString += '\n\n';
copyString += event.getDisplayEvent(timeline).getLocalizedBody(
MatrixLocals(L10n.of(context)),
copyString += event.getDisplayEvent(timeline!).getLocalizedBody(
MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: true);
}
return copyString;
@ -458,37 +457,37 @@ class ChatController extends State<Chat> {
final event = selectedEvents.single;
final score = await showConfirmationDialog<int>(
context: context,
title: L10n.of(context).reportMessage,
message: L10n.of(context).howOffensiveIsThisContent,
cancelLabel: L10n.of(context).cancel,
okLabel: L10n.of(context).ok,
title: L10n.of(context)!.reportMessage,
message: L10n.of(context)!.howOffensiveIsThisContent,
cancelLabel: L10n.of(context)!.cancel,
okLabel: L10n.of(context)!.ok,
actions: [
AlertDialogAction(
key: -100,
label: L10n.of(context).extremeOffensive,
label: L10n.of(context)!.extremeOffensive,
),
AlertDialogAction(
key: -50,
label: L10n.of(context).offensive,
label: L10n.of(context)!.offensive,
),
AlertDialogAction(
key: 0,
label: L10n.of(context).inoffensive,
label: L10n.of(context)!.inoffensive,
),
]);
if (score == null) return;
final reason = await showTextInputDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).whyDoYouWantToReportThis,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
textFields: [DialogTextField(hintText: L10n.of(context).reason)]);
title: L10n.of(context)!.whyDoYouWantToReportThis,
okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel,
textFields: [DialogTextField(hintText: L10n.of(context)!.reason)]);
if (reason == null || reason.single.isEmpty) return;
final result = await showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).client.reportContent(
event.roomId,
event.roomId!,
event.eventId,
reason: reason.single,
score: score,
@ -500,16 +499,16 @@ class ChatController extends State<Chat> {
selectedEvents.clear();
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(L10n.of(context).contentHasBeenReported)));
SnackBar(content: Text(L10n.of(context)!.contentHasBeenReported)));
}
void redactEventsAction() async {
final confirmed = await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).messageWillBeRemovedWarning,
okLabel: L10n.of(context).remove,
cancelLabel: L10n.of(context).cancel,
title: L10n.of(context)!.messageWillBeRemovedWarning,
okLabel: L10n.of(context)!.remove,
cancelLabel: L10n.of(context)!.cancel,
) ==
OkCancelResult.ok;
if (!confirmed) return;
@ -522,12 +521,12 @@ class ChatController extends State<Chat> {
await event.redactEvent();
} else {
final client = currentRoomBundle.firstWhere(
(cl) => selectedEvents.first.senderId == cl.userID,
(cl) => selectedEvents.first.senderId == cl!.userID,
orElse: () => null);
if (client == null) {
return;
}
final room = client.getRoomById(roomId);
final room = client.getRoomById(roomId!)!;
await Event.fromJson(event.toJson(), room).redactEvent();
}
} else {
@ -541,17 +540,17 @@ class ChatController extends State<Chat> {
});
}
List<Client> get currentRoomBundle {
final clients = matrix.currentBundle;
clients.removeWhere((c) => c.getRoomById(roomId) == null);
List<Client?> get currentRoomBundle {
final clients = matrix!.currentBundle!;
clients.removeWhere((c) => c!.getRoomById(roomId!) == null);
return clients;
}
bool get canRedactSelectedEvents {
final clients = matrix.currentBundle;
final clients = matrix!.currentBundle;
for (final event in selectedEvents) {
if (event.canRedact == false &&
!(clients.any((cl) => event.senderId == cl.userID))) return false;
!(clients!.any((cl) => event.senderId == cl!.userID))) return false;
}
return true;
}
@ -561,7 +560,7 @@ class ChatController extends State<Chat> {
return false;
}
return currentRoomBundle
.any((cl) => selectedEvents.first.senderId == cl.userID);
.any((cl) => selectedEvents.first.senderId == cl!.userID);
}
void forwardEventsAction() async {
@ -583,7 +582,7 @@ class ChatController extends State<Chat> {
event.sendAgain();
}
final allEditEvents = event
.aggregatedEvents(timeline, RelationshipTypes.edit)
.aggregatedEvents(timeline!, RelationshipTypes.edit)
.where((e) => e.status.isError);
for (final e in allEditEvents) {
e.sendAgain();
@ -591,7 +590,7 @@ class ChatController extends State<Chat> {
setState(() => selectedEvents.clear());
}
void replyAction({Event replyTo}) {
void replyAction({Event? replyTo}) {
setState(() {
replyEvent = replyTo ?? selectedEvents.first;
selectedEvents.clear();
@ -609,7 +608,7 @@ class ChatController extends State<Chat> {
future: () async {
// okay, we first have to fetch if the event is in the room
try {
final event = await timeline.getEventById(eventId);
final event = await timeline!.getEventById(eventId);
if (event == null) {
// event is null...meaning something is off
return;
@ -628,7 +627,7 @@ class ChatController extends State<Chat> {
return;
}
try {
await timeline.requestHistory(historyCount: _loadHistoryCount);
await timeline!.requestHistory(historyCount: _loadHistoryCount);
} catch (err) {
if (err is TimeoutException) {
// loading the history timed out...so let's do nothing
@ -662,7 +661,7 @@ class ChatController extends State<Chat> {
return sendEmojiAction(emoji.emoji);
}
Iterable<Event> _allReactionEvents;
late Iterable<Event> _allReactionEvents;
void cancelEmojiPicker() => setState(() => showEmojiPicker = false);
@ -671,13 +670,13 @@ class ChatController extends State<Chat> {
setState(() => showEmojiPicker = true);
}
void sendEmojiAction(String emoji) async {
void sendEmojiAction(String? emoji) async {
final events = List<Event>.from(selectedEvents);
setState(() => selectedEvents.clear());
for (final event in events) {
await room.sendReaction(
await room!.sendReaction(
event.eventId,
emoji,
emoji!,
);
}
}
@ -695,7 +694,7 @@ class ChatController extends State<Chat> {
void editSelectedEventAction() {
final client = currentRoomBundle.firstWhere(
(cl) => selectedEvents.first.senderId == cl.userID,
(cl) => selectedEvents.first.senderId == cl!.userID,
orElse: () => null);
if (client == null) {
return;
@ -704,9 +703,9 @@ class ChatController extends State<Chat> {
setState(() {
pendingText = sendController.text;
editEvent = selectedEvents.first;
inputText = sendController.text = editEvent
.getDisplayEvent(timeline)
.getLocalizedBody(MatrixLocals(L10n.of(context)),
inputText = sendController.text = editEvent!
.getDisplayEvent(timeline!)
.getLocalizedBody(MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false, hideReply: true);
selectedEvents.clear();
});
@ -718,29 +717,29 @@ class ChatController extends State<Chat> {
await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).goToTheNewRoom,
message: room
.getState(EventTypes.RoomTombstone)
title: L10n.of(context)!.goToTheNewRoom,
message: room!
.getState(EventTypes.RoomTombstone)!
.parsedTombstoneContent
.body,
okLabel: L10n.of(context).ok,
cancelLabel: L10n.of(context).cancel,
okLabel: L10n.of(context)!.ok,
cancelLabel: L10n.of(context)!.cancel,
)) {
return;
}
final result = await showFutureLoadingDialog(
context: context,
future: () => room.client.joinRoom(room
.getState(EventTypes.RoomTombstone)
future: () => room!.client.joinRoom(room!
.getState(EventTypes.RoomTombstone)!
.parsedTombstoneContent
.replacementRoom),
);
await showFutureLoadingDialog(
context: context,
future: room.leave,
future: room!.leave,
);
if (result.error == null) {
VRouter.of(context).toSegments(['rooms', result.result]);
VRouter.of(context).toSegments(['rooms', result.result!]);
}
}
@ -761,12 +760,12 @@ class ChatController extends State<Chat> {
}
}
int findChildIndexCallback(Key key, Map<String, int> thisEventsKeyMap) {
int? findChildIndexCallback(Key key, Map<String, int> thisEventsKeyMap) {
// this method is called very often. As such, it has to be optimized for speed.
if (key is! ValueKey) {
return null;
}
final eventId = (key as ValueKey).value;
final eventId = key.value;
if (eventId is! String) {
return null;
}
@ -809,11 +808,11 @@ class ChatController extends State<Chat> {
}
void onInputBarChanged(String text) {
if (text.endsWith(' ') && matrix.hasComplexBundles) {
if (text.endsWith(' ') && matrix!.hasComplexBundles) {
final clients = currentRoomBundle;
for (final client in clients) {
final prefix = client.sendPrefix;
if ((prefix?.isNotEmpty ?? false) &&
final prefix = client!.sendPrefix;
if ((prefix.isNotEmpty) &&
text.toLowerCase() == '${prefix.toLowerCase()} ') {
setSendingClient(client);
setState(() {
@ -828,7 +827,7 @@ class ChatController extends State<Chat> {
typingCoolDown = Timer(const Duration(seconds: 2), () {
typingCoolDown = null;
currentlyTyping = false;
room.setTyping(false);
room!.setTyping(false);
});
typingTimeout ??= Timer(const Duration(seconds: 30), () {
typingTimeout = null;
@ -836,12 +835,13 @@ class ChatController extends State<Chat> {
});
if (!currentlyTyping) {
currentlyTyping = true;
room.setTyping(true, timeout: const Duration(seconds: 30).inMilliseconds);
room!
.setTyping(true, timeout: const Duration(seconds: 30).inMilliseconds);
}
setState(() => inputText = text);
}
void showEventInfo([Event event]) =>
void showEventInfo([Event? event]) =>
(event ?? selectedEvents.single).showInfoDialog(context);
void cancelReplyEventAction() => setState(() {

View file

@ -1,5 +1,3 @@
//@dart=2.12
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
@ -19,41 +17,42 @@ class ChatAppBarTitle extends StatelessWidget {
@override
Widget build(BuildContext context) {
final room = controller.room;
if (room == null) {
return Container();
}
if (controller.selectedEvents.isNotEmpty) {
return Text(controller.selectedEvents.length.toString());
}
final directChatMatrixID = controller.room.directChatMatrixID;
final directChatMatrixID = room.directChatMatrixID;
return ListTile(
leading: Avatar(
mxContent: controller.room.avatar,
name: controller.room.displayname,
mxContent: room.avatar,
name: room.displayname,
),
contentPadding: EdgeInsets.zero,
onTap: directChatMatrixID != null
? () => showModalBottomSheet(
context: context,
builder: (c) => UserBottomSheet(
user: controller.room.getUserByMXIDSync(directChatMatrixID),
user: room.getUserByMXIDSync(directChatMatrixID),
outerContext: context,
onMention: () => controller.sendController.text +=
'${controller.room.getUserByMXIDSync(directChatMatrixID).mention} ',
'${room.getUserByMXIDSync(directChatMatrixID).mention} ',
),
)
: () => VRouter.of(context)
.toSegments(['rooms', controller.room.id, 'details']),
title: Text(
controller.room
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
: () => VRouter.of(context).toSegments(['rooms', room.id, 'details']),
title: Text(room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
maxLines: 1),
subtitle: StreamBuilder<Object>(
stream: Matrix.of(context)
.client
.onPresence
.stream
.where((p) => p.senderId == controller.room.directChatMatrixID)
.where((p) => p.senderId == room.directChatMatrixID)
.rateLimit(const Duration(seconds: 1)),
builder: (context, snapshot) => Text(
controller.room.getLocalizedStatus(context),
room.getLocalizedStatus(context),
maxLines: 1,
//overflow: TextOverflow.ellipsis,
),

View file

@ -1,5 +1,3 @@
//@dart=2.12
import 'package:flutter/material.dart';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';

View file

@ -13,7 +13,7 @@ import 'input_bar.dart';
class ChatInputRow extends StatelessWidget {
final ChatController controller;
const ChatInputRow(this.controller, {Key key}) : super(key: key);
const ChatInputRow(this.controller, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -30,14 +30,14 @@ class ChatInputRow extends StatelessWidget {
child: Row(
children: <Widget>[
const Icon(Icons.keyboard_arrow_left_outlined),
Text(L10n.of(context).forward),
Text(L10n.of(context)!.forward),
],
),
),
),
controller.selectedEvents.length == 1
? controller.selectedEvents.first
.getDisplayEvent(controller.timeline)
.getDisplayEvent(controller.timeline!)
.status
.isSent
? SizedBox(
@ -46,7 +46,7 @@ class ChatInputRow extends StatelessWidget {
onPressed: controller.replyAction,
child: Row(
children: <Widget>[
Text(L10n.of(context).reply),
Text(L10n.of(context)!.reply),
const Icon(Icons.keyboard_arrow_right),
],
),
@ -58,7 +58,7 @@ class ChatInputRow extends StatelessWidget {
onPressed: controller.sendAgainAction,
child: Row(
children: <Widget>[
Text(L10n.of(context).tryToSendAgain),
Text(L10n.of(context)!.tryToSendAgain),
const SizedBox(width: 4),
const Icon(Icons.send_outlined, size: 16),
],
@ -88,7 +88,7 @@ class ChatInputRow extends StatelessWidget {
foregroundColor: Colors.white,
child: Icon(Icons.video_call_outlined),
),
title: Text(L10n.of(context).videoCall),
title: Text(L10n.of(context)!.videoCall),
contentPadding: const EdgeInsets.all(0),
),
),
@ -100,7 +100,7 @@ class ChatInputRow extends StatelessWidget {
foregroundColor: Colors.white,
child: Icon(Icons.attachment_outlined),
),
title: Text(L10n.of(context).sendFile),
title: Text(L10n.of(context)!.sendFile),
contentPadding: const EdgeInsets.all(0),
),
),
@ -112,7 +112,7 @@ class ChatInputRow extends StatelessWidget {
foregroundColor: Colors.white,
child: Icon(Icons.image_outlined),
),
title: Text(L10n.of(context).sendImage),
title: Text(L10n.of(context)!.sendImage),
contentPadding: const EdgeInsets.all(0),
),
),
@ -125,7 +125,7 @@ class ChatInputRow extends StatelessWidget {
foregroundColor: Colors.white,
child: Icon(Icons.camera_alt_outlined),
),
title: Text(L10n.of(context).openCamera),
title: Text(L10n.of(context)!.openCamera),
contentPadding: const EdgeInsets.all(0),
),
),
@ -138,11 +138,11 @@ class ChatInputRow extends StatelessWidget {
foregroundColor: Colors.white,
child: Icon(Icons.videocam_outlined),
),
title: Text(L10n.of(context).openVideoCamera),
title: Text(L10n.of(context)!.openVideoCamera),
contentPadding: const EdgeInsets.all(0),
),
),
if (controller.room
if (controller.room!
.getImagePacks(ImagePackUsage.sticker)
.isNotEmpty)
PopupMenuItem<String>(
@ -153,7 +153,7 @@ class ChatInputRow extends StatelessWidget {
foregroundColor: Colors.white,
child: Icon(Icons.emoji_emotions_outlined),
),
title: Text(L10n.of(context).sendSticker),
title: Text(L10n.of(context)!.sendSticker),
contentPadding: const EdgeInsets.all(0),
),
),
@ -166,7 +166,7 @@ class ChatInputRow extends StatelessWidget {
foregroundColor: Colors.white,
child: Icon(Icons.gps_fixed_outlined),
),
title: Text(L10n.of(context).shareLocation),
title: Text(L10n.of(context)!.shareLocation),
contentPadding: const EdgeInsets.all(0),
),
),
@ -176,11 +176,11 @@ class ChatInputRow extends StatelessWidget {
Container(
height: 56,
alignment: Alignment.center,
child: EncryptionButton(controller.room),
child: EncryptionButton(controller.room!),
),
if (controller.matrix.isMultiAccount &&
controller.matrix.hasComplexBundles &&
controller.matrix.currentBundle.length > 1)
if (controller.matrix!.isMultiAccount &&
controller.matrix!.hasComplexBundles &&
controller.matrix!.currentBundle!.length > 1)
Container(
height: 56,
alignment: Alignment.center,
@ -190,7 +190,7 @@ class ChatInputRow extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: InputBar(
room: controller.room,
room: controller.room!,
minLines: 1,
maxLines: 8,
autofocus: !PlatformInfos.isMobile,
@ -201,7 +201,7 @@ class ChatInputRow extends StatelessWidget {
focusNode: controller.inputFocus,
controller: controller.sendController,
decoration: InputDecoration(
hintText: L10n.of(context).writeAMessage,
hintText: L10n.of(context)!.writeAMessage,
hintMaxLines: 1,
border: InputBorder.none,
enabledBorder: InputBorder.none,
@ -216,7 +216,7 @@ class ChatInputRow extends StatelessWidget {
height: 56,
alignment: Alignment.center,
child: IconButton(
tooltip: L10n.of(context).voiceMessage,
tooltip: L10n.of(context)!.voiceMessage,
icon: const Icon(Icons.mic_none_outlined),
onPressed: controller.voiceMessageAction,
),
@ -228,7 +228,7 @@ class ChatInputRow extends StatelessWidget {
child: IconButton(
icon: const Icon(Icons.send_outlined),
onPressed: controller.send,
tooltip: L10n.of(context).send,
tooltip: L10n.of(context)!.send,
),
),
],
@ -239,11 +239,11 @@ class ChatInputRow extends StatelessWidget {
class _ChatAccountPicker extends StatelessWidget {
final ChatController controller;
const _ChatAccountPicker(this.controller, {Key key}) : super(key: key);
const _ChatAccountPicker(this.controller, {Key? key}) : super(key: key);
void _popupMenuButtonSelected(String mxid) {
final client = controller.matrix.currentBundle
.firstWhere((cl) => cl.userID == mxid, orElse: () => null);
final client = controller.matrix!.currentBundle!
.firstWhere((cl) => cl!.userID == mxid, orElse: () => null);
if (client == null) {
Logs().w('Attempted to switch to a non-existing client $mxid');
return;
@ -258,23 +258,23 @@ class _ChatAccountPicker extends StatelessWidget {
return Padding(
padding: const EdgeInsets.all(8.0),
child: FutureBuilder<Profile>(
future: controller.sendingClient.ownProfile,
future: controller.sendingClient!.ownProfile,
builder: (context, snapshot) => PopupMenuButton<String>(
onSelected: _popupMenuButtonSelected,
itemBuilder: (BuildContext context) => clients
.map((client) => PopupMenuItem<String>(
value: client.userID,
value: client!.userID,
child: FutureBuilder<Profile>(
future: client.ownProfile,
builder: (context, snapshot) => ListTile(
leading: Avatar(
mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ??
client.userID.localpart,
client.userID!.localpart,
size: 20,
),
title:
Text(snapshot.data?.displayName ?? client.userID),
Text(snapshot.data?.displayName ?? client.userID!),
contentPadding: const EdgeInsets.all(0),
),
),
@ -283,7 +283,7 @@ class _ChatAccountPicker extends StatelessWidget {
child: Avatar(
mxContent: snapshot.data?.avatarUrl,
name: snapshot.data?.displayName ??
controller.matrix.client.userID.localpart,
controller.matrix!.client.userID!.localpart,
size: 20,
),
),

View file

@ -34,31 +34,31 @@ enum _EventContextAction { info, report }
class ChatView extends StatelessWidget {
final ChatController controller;
const ChatView(this.controller, {Key key}) : super(key: key);
const ChatView(this.controller, {Key? key}) : super(key: key);
List<Widget> _appBarActions(BuildContext context) => controller.selectMode
? [
if (controller.canEditSelectedEvents)
IconButton(
icon: const Icon(Icons.edit_outlined),
tooltip: L10n.of(context).edit,
tooltip: L10n.of(context)!.edit,
onPressed: controller.editSelectedEventAction,
),
IconButton(
icon: const Icon(Icons.copy_outlined),
tooltip: L10n.of(context).copy,
tooltip: L10n.of(context)!.copy,
onPressed: controller.copyEventsAction,
),
if (controller.canSaveSelectedEvent)
IconButton(
icon: Icon(Icons.adaptive.share),
tooltip: L10n.of(context).share,
tooltip: L10n.of(context)!.share,
onPressed: controller.saveSelectedEvent,
),
if (controller.canRedactSelectedEvents)
IconButton(
icon: const Icon(Icons.delete_outlined),
tooltip: L10n.of(context).redactMessage,
tooltip: L10n.of(context)!.redactMessage,
onPressed: controller.redactEventsAction,
),
if (controller.selectedEvents.length == 1)
@ -82,7 +82,7 @@ class ChatView extends StatelessWidget {
children: [
const Icon(Icons.info_outlined),
const SizedBox(width: 12),
Text(L10n.of(context).messageInfo),
Text(L10n.of(context)!.messageInfo),
],
),
),
@ -96,7 +96,7 @@ class ChatView extends StatelessWidget {
color: Colors.red,
),
const SizedBox(width: 12),
Text(L10n.of(context).reportMessage),
Text(L10n.of(context)!.reportMessage),
],
),
),
@ -104,29 +104,30 @@ class ChatView extends StatelessWidget {
),
]
: [
ChatSettingsPopupMenu(controller.room, !controller.room.isDirectChat),
ChatSettingsPopupMenu(
controller.room!, !controller.room!.isDirectChat),
];
@override
Widget build(BuildContext context) {
controller.matrix ??= Matrix.of(context);
final client = controller.matrix.client;
final client = controller.matrix!.client;
controller.sendingClient ??= client;
controller.room = controller.sendingClient.getRoomById(controller.roomId);
controller.room = controller.sendingClient!.getRoomById(controller.roomId!);
if (controller.room == null) {
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).oopsSomethingWentWrong),
title: Text(L10n.of(context)!.oopsSomethingWentWrong),
),
body: Center(
child: Text(L10n.of(context).youAreNoLongerParticipatingInThisChat),
child: Text(L10n.of(context)!.youAreNoLongerParticipatingInThisChat),
),
);
}
if (controller.room.membership == Membership.invite) {
if (controller.room!.membership == Membership.invite) {
showFutureLoadingDialog(
context: context, future: () => controller.room.join());
context: context, future: () => controller.room!.join());
}
final bottomSheetPadding = FluffyThemes.isColumnMode(context) ? 16.0 : 8.0;
final horizontalPadding = FluffyThemes.isColumnMode(context) ? 8.0 : 0.0;
@ -139,7 +140,7 @@ class ChatView extends StatelessWidget {
}
},
child: StreamBuilder(
stream: controller.room.onUpdate.stream
stream: controller.room!.onUpdate.stream
.rateLimit(const Duration(milliseconds: 250)),
builder: (context, snapshot) => Scaffold(
appBar: AppBar(
@ -152,10 +153,10 @@ class ChatView extends StatelessWidget {
? IconButton(
icon: const Icon(Icons.close),
onPressed: controller.clearSelectedEvents,
tooltip: L10n.of(context).close,
tooltip: L10n.of(context)!.close,
color: Theme.of(context).colorScheme.primary,
)
: UnreadBadgeBackButton(roomId: controller.roomId),
: UnreadBadgeBackButton(roomId: controller.roomId!),
titleSpacing: 0,
title: ChatAppBarTitle(controller),
actions: _appBarActions(context),
@ -167,7 +168,7 @@ class ChatView extends StatelessWidget {
child: FloatingActionButton(
onPressed: controller.scrollDown,
foregroundColor:
Theme.of(context).textTheme.bodyText2.color,
Theme.of(context).textTheme.bodyText2!.color,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
mini: true,
child: Icon(Icons.arrow_downward_outlined,
@ -184,7 +185,7 @@ class ChatView extends StatelessWidget {
children: <Widget>[
if (Matrix.of(context).wallpaper != null)
Image.file(
Matrix.of(context).wallpaper,
Matrix.of(context).wallpaper!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
@ -240,7 +241,7 @@ class ChatView extends StatelessWidget {
controller.filteredEvents.length +
1
? controller
.timeline.isRequestingHistory
.timeline!.isRequestingHistory
? const Center(
child:
CircularProgressIndicator
@ -259,7 +260,7 @@ class ChatView extends StatelessWidget {
onPressed: controller
.requestHistory,
child: Text(
L10n.of(context)
L10n.of(context)!
.loadMore),
),
)
@ -337,7 +338,7 @@ class ChatView extends StatelessWidget {
selected: controller
.selectedEvents
.any((e) => e.eventId == controller.filteredEvents[i - 1].eventId),
timeline: controller.timeline,
timeline: controller.timeline!,
nextEvent: i < controller.filteredEvents.length ? controller.filteredEvents[i] : null),
),
);
@ -352,8 +353,8 @@ class ChatView extends StatelessWidget {
},
)),
),
if (controller.room.canSendDefaultMessages &&
controller.room.membership == Membership.join)
if (controller.room!.canSendDefaultMessages &&
controller.room!.membership == Membership.join)
Container(
margin: EdgeInsets.only(
bottom: bottomSheetPadding,

View file

@ -1,4 +1,3 @@
//@dart=2.12
// This file is auto-generated using scripts/generate_command_hints_glue.sh.
import 'package:flutter_gen/gen_l10n/l10n.dart';

View file

@ -12,13 +12,13 @@ import '../../widgets/matrix.dart';
class EncryptionButton extends StatefulWidget {
final Room room;
const EncryptionButton(this.room, {Key key}) : super(key: key);
const EncryptionButton(this.room, {Key? key}) : super(key: key);
@override
_EncryptionButtonState createState() => _EncryptionButtonState();
}
class _EncryptionButtonState extends State<EncryptionButton> {
StreamSubscription _onSyncSub;
StreamSubscription? _onSyncSub;
void _enableEncryptionAction() async {
if (widget.room.encrypted) {
@ -29,20 +29,20 @@ class _EncryptionButtonState extends State<EncryptionButton> {
await showOkAlertDialog(
useRootNavigator: false,
context: context,
okLabel: L10n.of(context).ok,
message: L10n.of(context).noEncryptionForPublicRooms,
okLabel: L10n.of(context)!.ok,
message: L10n.of(context)!.noEncryptionForPublicRooms,
);
return;
}
if (await showOkCancelAlertDialog(
useRootNavigator: false,
context: context,
title: L10n.of(context).enableEncryption,
title: L10n.of(context)!.enableEncryption,
message: widget.room.client.encryptionEnabled
? L10n.of(context).enableEncryptionWarning
: L10n.of(context).needPantalaimonWarning,
okLabel: L10n.of(context).yes,
cancelLabel: L10n.of(context).cancel,
? L10n.of(context)!.enableEncryptionWarning
: L10n.of(context)!.needPantalaimonWarning,
okLabel: L10n.of(context)!.yes,
cancelLabel: L10n.of(context)!.cancel,
) ==
OkCancelResult.ok) {
await showFutureLoadingDialog(
@ -50,7 +50,7 @@ class _EncryptionButtonState extends State<EncryptionButton> {
future: () => widget.room.enableEncryption(),
);
// we want to enable the lock icon
setState(() => null);
setState(() {});
}
}
@ -68,22 +68,22 @@ class _EncryptionButtonState extends State<EncryptionButton> {
.onSync
.stream
.where((s) => s.deviceLists != null)
.listen((s) => setState(() => null));
.listen((s) => setState(() {}));
}
return FutureBuilder<List<User>>(
future:
widget.room.encrypted ? widget.room.requestParticipants() : null,
builder: (BuildContext context, snapshot) {
Color color;
Color? color;
if (widget.room.encrypted && snapshot.hasData) {
final users = snapshot.data;
final users = snapshot.data!;
users.removeWhere((u) =>
!{Membership.invite, Membership.join}.contains(u.membership) ||
!widget.room.client.userDeviceKeys.containsKey(u.id));
var allUsersValid = true;
var oneUserInvalid = false;
for (final u in users) {
final status = widget.room.client.userDeviceKeys[u.id].verified;
final status = widget.room.client.userDeviceKeys[u.id]!.verified;
if (status != UserVerifiedStatus.verified) {
allUsersValid = false;
}
@ -99,8 +99,8 @@ class _EncryptionButtonState extends State<EncryptionButton> {
}
return IconButton(
tooltip: widget.room.encrypted
? L10n.of(context).encrypted
: L10n.of(context).encryptionNotEnabled,
? L10n.of(context)!.encrypted
: L10n.of(context)!.encryptionNotEnabled,
icon: Icon(
widget.room.encrypted
? Icons.lock_outlined

View file

@ -13,7 +13,7 @@ extension EventInfoDialogExtension on Event {
void showInfoDialog(BuildContext context) => showModalBottomSheet(
context: context,
builder: (context) =>
EventInfoDialog(l10n: L10n.of(context), event: this),
EventInfoDialog(l10n: L10n.of(context)!, event: this),
);
}
@ -21,9 +21,9 @@ class EventInfoDialog extends StatelessWidget {
final Event event;
final L10n l10n;
const EventInfoDialog({
@required this.event,
@required this.l10n,
Key key,
required this.event,
required this.l10n,
Key? key,
}) : super(key: key);
String get prettyJson {
@ -37,11 +37,11 @@ class EventInfoDialog extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).messageInfo),
title: Text(L10n.of(context)!.messageInfo),
leading: IconButton(
icon: const Icon(Icons.arrow_downward_outlined),
onPressed: Navigator.of(context, rootNavigator: false).pop,
tooltip: L10n.of(context).close,
tooltip: L10n.of(context)!.close,
),
),
body: ListView(
@ -51,19 +51,19 @@ class EventInfoDialog extends StatelessWidget {
mxContent: event.sender.avatarUrl,
name: event.sender.calcDisplayname(),
),
title: Text(L10n.of(context).sender),
title: Text(L10n.of(context)!.sender),
subtitle:
Text('${event.sender.calcDisplayname()} [${event.senderId}]'),
),
ListTile(
title: Text(L10n.of(context).time),
title: Text(L10n.of(context)!.time),
subtitle: Text(event.originServerTs.localizedTime(context)),
),
ListTile(
title: Text(L10n.of(context).messageType),
title: Text(L10n.of(context)!.messageType),
subtitle: Text(event.humanreadableType),
),
ListTile(title: Text('${L10n.of(context).sourceCode}:')),
ListTile(title: Text('${L10n.of(context)!.sourceCode}:')),
Padding(
padding: const EdgeInsets.all(12.0),
child: Material(

View file

@ -1,5 +1,3 @@
//@dart=2.12
import 'dart:async';
import 'dart:io';

View file

@ -13,17 +13,17 @@ import '../../../utils/url_launcher.dart';
class HtmlMessage extends StatelessWidget {
final String html;
final int maxLines;
final int? maxLines;
final Room room;
final TextStyle defaultTextStyle;
final TextStyle linkStyle;
final double emoteSize;
final TextStyle? defaultTextStyle;
final TextStyle? linkStyle;
final double? emoteSize;
const HtmlMessage({
Key key,
this.html,
Key? key,
required this.html,
this.maxLines,
this.room,
required this.room,
this.defaultTextStyle,
this.linkStyle,
this.emoteSize,
@ -52,7 +52,7 @@ class HtmlMessage extends StatelessWidget {
defaultTextStyle: defaultTextStyle,
emoteSize: emoteSize,
linkStyle: linkStyle ??
themeData.textTheme.bodyText2.copyWith(
themeData.textTheme.bodyText2!.copyWith(
color: themeData.colorScheme.secondary,
decoration: TextDecoration.underline,
),
@ -60,11 +60,11 @@ class HtmlMessage extends StatelessWidget {
maxLines: maxLines,
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
onPillTap: (url) => UrlLauncher(context, url).launchUrl(),
getMxcUrl: (String mxc, double width, double height,
{bool animated = false}) {
getMxcUrl: (String mxc, double? width, double? height,
{bool? animated = false}) {
final ratio = MediaQuery.of(context).devicePixelRatio;
return Uri.parse(mxc)
?.getThumbnail(
.getThumbnail(
matrix.client,
width: (width ?? 800) * ratio,
height: (height ?? 800) * ratio,
@ -92,9 +92,6 @@ class HtmlMessage extends StatelessWidget {
return await matrix.store.getItem('${SettingKeys.codeLanguage}.$key');
},
getPillInfo: (String url) async {
if (room == null) {
return null;
}
final identityParts = url.parseIdentifierIntoParts();
final identifier = identityParts?.primaryIdentifier;
if (identifier == null) {
@ -108,13 +105,10 @@ class HtmlMessage extends StatelessWidget {
}
// there might still be a profile...
final profile = await room.client.getProfileFromUserId(identifier);
if (profile != null) {
return {
'displayname': profile.displayName,
'avatar_url': profile.avatarUrl.toString(),
};
}
return null;
return {
'displayname': profile.displayName,
'avatar_url': profile.avatarUrl.toString(),
};
}
if (identifier.sigil == '#') {
// we have an alias pill
@ -128,7 +122,7 @@ class HtmlMessage extends StatelessWidget {
// we have a room!
return {
'displayname':
r.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
r.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
'avatar_url': r.getState('m.room.avatar')?.content['url'],
};
}
@ -143,12 +137,12 @@ class HtmlMessage extends StatelessWidget {
}
return {
'displayname':
r.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
r.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
'avatar_url': r.getState('m.room.avatar')?.content['url'],
};
}
return null;
},
} as Future<Map<String, dynamic>> Function(String)?,
);
}
}

View file

@ -19,13 +19,13 @@ class ImageBubble extends StatefulWidget {
final bool tapToView;
final BoxFit fit;
final bool maxSize;
final Color backgroundColor;
final Color? backgroundColor;
final bool thumbnailOnly;
final bool animated;
final double width;
final double height;
final void Function() onLoaded;
final void Function() onTap;
final void Function()? onLoaded;
final void Function()? onTap;
const ImageBubble(
this.event, {
@ -39,7 +39,7 @@ class ImageBubble extends StatefulWidget {
this.height = 300,
this.animated = false,
this.onTap,
Key key,
Key? key,
}) : super(key: key);
@override
@ -48,17 +48,17 @@ class ImageBubble extends StatefulWidget {
class _ImageBubbleState extends State<ImageBubble> {
// for plaintext: holds the http URL for the thumbnail
String thumbnailUrl;
String? thumbnailUrl;
// for plaintext. holds the http URL for the thumbnial, without the animated flag
String thumbnailUrlNoAnimated;
String? thumbnailUrlNoAnimated;
// for plaintext: holds the http URL of the original
String attachmentUrl;
MatrixFile _file;
MatrixFile _thumbnail;
String? attachmentUrl;
MatrixFile? _file;
MatrixFile? _thumbnail;
bool _requestedThumbnailOnFailure = false;
// In case we have animated = false, this will hold the first frame so that we make
// sure that things are never animated
Widget _firstFrame;
Widget? _firstFrame;
// the mimetypes that we know how to render, from the flutter Image widget
final _knownMimetypes = <String>{
@ -82,8 +82,8 @@ class _ImageBubbleState extends State<ImageBubble> {
? widget.event.thumbnailMimetype.toLowerCase()
: widget.event.attachmentMimetype.toLowerCase();
MatrixFile get _displayFile => _file ?? _thumbnail;
String get displayUrl => widget.thumbnailOnly ? thumbnailUrl : attachmentUrl;
MatrixFile? get _displayFile => _file ?? _thumbnail;
String? get displayUrl => widget.thumbnailOnly ? thumbnailUrl : attachmentUrl;
dynamic _error;
@ -91,14 +91,14 @@ class _ImageBubbleState extends State<ImageBubble> {
try {
final res = await widget.event
.downloadAndDecryptAttachmentCached(getThumbnail: getThumbnail);
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance!.addPostFrameCallback((_) {
if (getThumbnail) {
if (mounted) {
setState(() => _thumbnail = res);
}
} else {
if (widget.onLoaded != null) {
widget.onLoaded();
widget.onLoaded!();
}
if (mounted) {
setState(() => _file = res);
@ -106,7 +106,7 @@ class _ImageBubbleState extends State<ImageBubble> {
}
});
} catch (err) {
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance!.addPostFrameCallback((_) {
if (mounted) {
setState(() => _error = err);
}
@ -114,7 +114,7 @@ class _ImageBubbleState extends State<ImageBubble> {
}
}
Widget frameBuilder(_, Widget child, int frame, __) {
Widget frameBuilder(_, Widget child, int? frame, __) {
// as servers might return animated gifs as thumbnails and we want them to *not* play
// animated, we'll have to store the first frame in a variable and display that instead
if (widget.animated) {
@ -135,12 +135,14 @@ class _ImageBubbleState extends State<ImageBubble> {
key: ValueKey(key),
fit: widget.fit,
),
network: (String url) => SvgPicture.network(
url,
key: ValueKey(url),
placeholderBuilder: (context) => getPlaceholderWidget(),
fit: widget.fit,
),
network: (String? url) => url == null
? Container()
: SvgPicture.network(
url,
key: ValueKey(url),
placeholderBuilder: (context) => getPlaceholderWidget(),
fit: widget.fit,
),
);
_contentRenderers['image/lottie+json'] = _ImageBubbleContentRenderer(
memory: (Uint8List bytes, String key) => Lottie.memory(
@ -151,14 +153,16 @@ class _ImageBubbleState extends State<ImageBubble> {
getErrorWidget(context, error),
animate: widget.animated,
),
network: (String url) => Lottie.network(
url,
key: ValueKey(url),
fit: widget.fit,
errorBuilder: (context, error, stacktrace) =>
getErrorWidget(context, error),
animate: widget.animated,
),
network: (String? url) => url == null
? Container()
: Lottie.network(
url,
key: ValueKey(url),
fit: widget.fit,
errorBuilder: (context, error, stacktrace) =>
getErrorWidget(context, error),
animate: widget.animated,
),
);
// add all the custom content renderer mimetypes to the known mimetypes set
@ -203,7 +207,7 @@ class _ImageBubbleState extends State<ImageBubble> {
OutlinedButton.icon(
style: OutlinedButton.styleFrom(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
primary: Theme.of(context).textTheme.bodyText1.color,
primary: Theme.of(context).textTheme.bodyText1!.color,
),
icon: const Icon(Icons.download_outlined),
onPressed: () => widget.event.saveFile(context),
@ -215,7 +219,7 @@ class _ImageBubbleState extends State<ImageBubble> {
),
),
const SizedBox(height: 8),
if (widget.event.sizeString != null) Text(widget.event.sizeString),
if (widget.event.sizeString != null) Text(widget.event.sizeString!),
const SizedBox(height: 8),
Text((error ?? _error).toString()),
],
@ -223,8 +227,8 @@ class _ImageBubbleState extends State<ImageBubble> {
);
}
Widget getPlaceholderWidget({Widget child}) {
Widget blurhash;
Widget getPlaceholderWidget({Widget? child}) {
Widget? blurhash;
if (widget.event.infoMap['xyz.amorgan.blurhash'] is String) {
final ratio =
widget.event.infoMap['w'] is int && widget.event.infoMap['h'] is int
@ -265,16 +269,16 @@ class _ImageBubbleState extends State<ImageBubble> {
: widget.event.thumbnailMxcUrl.toString();
final mimetype = getMimetype(!isOriginal);
if (_contentRenderers.containsKey(mimetype)) {
return _contentRenderers[mimetype].memory(_displayFile.bytes, key);
return _contentRenderers[mimetype]!.memory!(_displayFile!.bytes, key);
} else {
return Image.memory(
_displayFile.bytes,
_displayFile!.bytes,
key: ValueKey(key),
fit: widget.fit,
errorBuilder: (context, error, stacktrace) {
if (widget.event.hasThumbnail && !_requestedThumbnailOnFailure) {
_requestedThumbnailOnFailure = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance!.addPostFrameCallback((_) {
setState(() {
_file = null;
_requestFile(getThumbnail: true);
@ -299,12 +303,12 @@ class _ImageBubbleState extends State<ImageBubble> {
final mimetype = getMimetype(_requestedThumbnailOnFailure);
if (displayUrl == attachmentUrl &&
_contentRenderers.containsKey(mimetype)) {
return _contentRenderers[mimetype].network(displayUrl);
return _contentRenderers[mimetype]!.network!(displayUrl);
} else {
return CachedNetworkImage(
// as we change the url on-error we need a key so that the widget actually updates
key: ValueKey(displayUrl),
imageUrl: displayUrl,
imageUrl: displayUrl!,
placeholder: (context, url) {
if (!widget.thumbnailOnly &&
displayUrl != thumbnailUrl &&
@ -313,7 +317,7 @@ class _ImageBubbleState extends State<ImageBubble> {
return FutureBuilder<bool>(
future: (() async {
return await DefaultCacheManager()
.getFileFromCache(thumbnailUrl) !=
.getFileFromCache(thumbnailUrl!) !=
null;
})(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
@ -321,8 +325,8 @@ class _ImageBubbleState extends State<ImageBubble> {
return getPlaceholderWidget();
}
final effectiveUrl = snapshot.data == true
? thumbnailUrl
: thumbnailUrlNoAnimated;
? thumbnailUrl!
: thumbnailUrlNoAnimated!;
return CachedNetworkImage(
key: ValueKey(effectiveUrl),
imageUrl: effectiveUrl,
@ -348,7 +352,7 @@ class _ImageBubbleState extends State<ImageBubble> {
// the image failed to load but the event has a thumbnail attached....so we can
// try to load this one!
_requestedThumbnailOnFailure = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
WidgetsBinding.instance!.addPostFrameCallback((_) {
setState(() {
thumbnailUrl = widget.event
.getAttachmentUrl(
@ -382,11 +386,11 @@ class _ImageBubbleState extends State<ImageBubble> {
OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
primary: Theme.of(context).textTheme.bodyText1.color,
primary: Theme.of(context).textTheme.bodyText1!.color,
),
onPressed: () => onTap(context),
child: Text(
L10n.of(context).tapToShowImage,
L10n.of(context)!.tapToShowImage,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
@ -394,7 +398,7 @@ class _ImageBubbleState extends State<ImageBubble> {
),
if (widget.event.sizeString != null) ...[
const SizedBox(height: 8),
Text(widget.event.sizeString),
Text(widget.event.sizeString!),
]
],
));
@ -451,7 +455,7 @@ class _ImageBubbleState extends State<ImageBubble> {
void onTap(BuildContext context) {
if (widget.onTap != null) {
widget.onTap();
widget.onTap!();
return;
}
if (!widget.tapToView) return;
@ -476,8 +480,8 @@ class _ImageBubbleState extends State<ImageBubble> {
}
class _ImageBubbleContentRenderer {
final Widget Function(Uint8List, String) memory;
final Widget Function(String) network;
final Widget Function(Uint8List, String)? memory;
final Widget Function(String?)? network;
_ImageBubbleContentRenderer({this.memory, this.network});
}

View file

@ -11,13 +11,13 @@ class MapBubble extends StatelessWidget {
final double height;
final double radius;
const MapBubble({
this.latitude,
this.longitude,
required this.latitude,
required this.longitude,
this.zoom = 14.0,
this.width = 400,
this.height = 400,
this.radius = 10.0,
Key key,
Key? key,
}) : super(key: key);
@override

View file

@ -16,14 +16,14 @@ import 'verification_request_content.dart';
class Message extends StatelessWidget {
final Event event;
final Event nextEvent;
final void Function(Event) onSelect;
final void Function(Event) onAvatarTab;
final void Function(Event) onInfoTab;
final void Function(String) scrollToEventId;
final Event? nextEvent;
final void Function(Event)? onSelect;
final void Function(Event)? onAvatarTab;
final void Function(Event)? onInfoTab;
final void Function(String)? scrollToEventId;
final void Function(String) unfold;
final bool longPressSelect;
final bool selected;
final bool? longPressSelect;
final bool? selected;
final Timeline timeline;
const Message(this.event,
@ -33,10 +33,10 @@ class Message extends StatelessWidget {
this.onInfoTab,
this.onAvatarTab,
this.scrollToEventId,
@required this.unfold,
required this.unfold,
this.selected,
this.timeline,
Key key})
required this.timeline,
Key? key})
: super(key: key);
/// Indicates wheither the user may use a mouse instead
@ -61,14 +61,14 @@ class Message extends StatelessWidget {
var color = Theme.of(context).appBarTheme.backgroundColor;
final displayTime = event.type == EventTypes.RoomCreate ||
nextEvent == null ||
!event.originServerTs.sameEnvironment(nextEvent.originServerTs);
!event.originServerTs.sameEnvironment(nextEvent!.originServerTs);
final sameSender = nextEvent != null &&
[
EventTypes.Message,
EventTypes.Sticker,
EventTypes.Encrypted,
].contains(nextEvent.type)
? nextEvent.sender.id == event.sender.id && !displayTime
].contains(nextEvent!.type)
? nextEvent!.sender.id == event.sender.id && !displayTime
: false;
final textColor = ownMessage
? Colors.white
@ -119,7 +119,7 @@ class Message extends StatelessWidget {
: Avatar(
mxContent: event.sender.avatarUrl,
name: event.sender.calcDisplayname(),
onTap: () => onAvatarTab(event),
onTap: () => onAvatarTab!(event),
),
Expanded(
child: Column(
@ -152,10 +152,11 @@ class Message extends StatelessWidget {
clipBehavior: Clip.antiAlias,
child: InkWell(
onHover: (b) => useMouse = true,
onTap: !useMouse && longPressSelect
? () => null
: () => onSelect(event),
onLongPress: !longPressSelect ? null : () => onSelect(event),
onTap: !useMouse && longPressSelect!
? () {}
: () => onSelect!(event),
onLongPress:
!longPressSelect! ? null : () => onSelect!(event),
borderRadius: borderRadius,
child: Container(
decoration: BoxDecoration(
@ -175,13 +176,13 @@ class Message extends StatelessWidget {
children: <Widget>[
if (event.relationshipType ==
RelationshipTypes.reply)
FutureBuilder<Event>(
FutureBuilder<Event?>(
future: event.getReplyEvent(timeline),
builder: (BuildContext context, snapshot) {
final replyEvent = snapshot.hasData
? snapshot.data
? snapshot.data!
: Event(
eventId: event.relationshipEventId,
eventId: event.relationshipEventId!,
content: {
'msgtype': 'm.text',
'body': '...'
@ -195,7 +196,7 @@ class Message extends StatelessWidget {
return InkWell(
onTap: () {
if (scrollToEventId != null) {
scrollToEventId(replyEvent.eventId);
scrollToEventId!(replyEvent.eventId);
}
},
child: AbsorbPointer(
@ -300,7 +301,7 @@ class Message extends StatelessWidget {
return Center(
child: Container(
color: selected
color: selected!
? Theme.of(context).primaryColor.withAlpha(100)
: Theme.of(context).primaryColor.withAlpha(0),
constraints:

View file

@ -22,10 +22,10 @@ import 'sticker.dart';
class MessageContent extends StatelessWidget {
final Event event;
final Color textColor;
final void Function(Event) onInfoTab;
final Color? textColor;
final void Function(Event)? onInfoTab;
const MessageContent(this.event, {this.onInfoTab, Key key, this.textColor})
const MessageContent(this.event, {this.onInfoTab, Key? key, this.textColor})
: super(key: key);
void _verifyOrRequestKey(BuildContext context) async {
@ -33,15 +33,15 @@ class MessageContent extends StatelessWidget {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
event.type == EventTypes.Encrypted
? L10n.of(context).needPantalaimonWarning
? L10n.of(context)!.needPantalaimonWarning
: event.getLocalizedBody(
MatrixLocals(L10n.of(context)),
MatrixLocals(L10n.of(context)!),
),
)));
return;
}
final client = Matrix.of(context).client;
if (client.isUnknownSession && client.encryption.crossSigning.enabled) {
if (client.isUnknownSession && client.encryption!.crossSigning.enabled) {
await BootstrapDialog(
client: Matrix.of(context).client,
).show(context);
@ -55,7 +55,7 @@ class MessageContent extends StatelessWidget {
);
if (success.error == null) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(L10n.of(context).requestToReadOlderMessages)));
content: Text(L10n.of(context)!.requestToReadOlderMessages)));
}
}
}
@ -83,17 +83,17 @@ class MessageContent extends StatelessWidget {
if (PlatformInfos.isMobile) {
return AudioPlayerWidget(
event,
color: textColor,
color: textColor!,
);
}
return MessageDownloadContent(event, textColor);
return MessageDownloadContent(event, textColor!);
case MessageTypes.Video:
if (PlatformInfos.isMobile || PlatformInfos.isWeb) {
return EventVideoPlayer(event);
}
return MessageDownloadContent(event, textColor);
return MessageDownloadContent(event, textColor!);
case MessageTypes.File:
return MessageDownloadContent(event, textColor);
return MessageDownloadContent(event, textColor!);
case MessageTypes.Text:
case MessageTypes.Notice:
@ -115,7 +115,7 @@ class MessageContent extends StatelessWidget {
fontSize: bigEmotes ? fontSize * 3 : fontSize,
),
linkStyle: TextStyle(
color: textColor.withAlpha(150),
color: textColor!.withAlpha(150),
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration: TextDecoration.underline,
),
@ -131,14 +131,12 @@ class MessageContent extends StatelessWidget {
textColor: buttonTextColor,
onPressed: () => _verifyOrRequestKey(context),
icon: const Icon(Icons.lock_outline),
label: L10n.of(context).encrypted,
label: L10n.of(context)!.encrypted,
);
case MessageTypes.Location:
final geoUri =
Uri.tryParse(event.content.tryGet<String>('geo_uri'));
if (geoUri != null &&
geoUri.scheme == 'geo' &&
geoUri.path != null) {
Uri.tryParse(event.content.tryGet<String>('geo_uri')!);
if (geoUri != null && geoUri.scheme == 'geo') {
final latlong = geoUri.path
.split(';')
.first
@ -152,8 +150,8 @@ class MessageContent extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
MapBubble(
latitude: latlong.first,
longitude: latlong.last,
latitude: latlong.first!,
longitude: latlong.last!,
),
const SizedBox(height: 6),
OutlinedButton.icon(
@ -161,7 +159,7 @@ class MessageContent extends StatelessWidget {
onPressed:
UrlLauncher(context, geoUri.toString()).launchUrl,
label: Text(
L10n.of(context).openInMaps,
L10n.of(context)!.openInMaps,
style: TextStyle(color: textColor),
),
),
@ -177,24 +175,24 @@ class MessageContent extends StatelessWidget {
return _ButtonContent(
onPressed: () => launch(event.body),
icon: const Icon(Icons.phone_outlined, color: Colors.green),
label: L10n.of(context).videoCall,
label: L10n.of(context)!.videoCall,
textColor: buttonTextColor,
);
}
if (event.redacted) {
return _ButtonContent(
label: L10n.of(context)
label: L10n.of(context)!
.redactedAnEvent(event.sender.calcDisplayname()),
icon: const Icon(Icons.delete_outlined),
textColor: buttonTextColor,
onPressed: () => onInfoTab(event),
onPressed: () => onInfoTab!(event),
);
}
final bigEmotes = event.onlyEmotes &&
event.numberEmotes > 0 &&
event.numberEmotes <= 10;
return LinkText(
text: event.getLocalizedBody(MatrixLocals(L10n.of(context)),
text: event.getLocalizedBody(MatrixLocals(L10n.of(context)!),
hideReply: true),
textStyle: TextStyle(
color: textColor,
@ -202,24 +200,22 @@ class MessageContent extends StatelessWidget {
decoration: event.redacted ? TextDecoration.lineThrough : null,
),
linkStyle: TextStyle(
color: textColor.withAlpha(150),
color: textColor!.withAlpha(150),
fontSize: bigEmotes ? fontSize * 3 : fontSize,
decoration: TextDecoration.underline,
),
onLinkTap: (url) => UrlLauncher(context, url).launchUrl(),
);
}
break;
default:
return _ButtonContent(
label: L10n.of(context)
label: L10n.of(context)!
.userSentUnknownEvent(event.sender.calcDisplayname(), event.type),
icon: const Icon(Icons.info_outlined),
textColor: buttonTextColor,
onPressed: () => onInfoTab(event),
onPressed: () => onInfoTab!(event),
);
}
return Container(); // else flutter analyze complains
}
}
@ -227,14 +223,14 @@ class _ButtonContent extends StatelessWidget {
final void Function() onPressed;
final String label;
final Icon icon;
final Color textColor;
final Color? textColor;
const _ButtonContent({
@required this.label,
@required this.icon,
@required this.textColor,
@required this.onPressed,
Key key,
required this.label,
required this.icon,
required this.textColor,
required this.onPressed,
Key? key,
}) : super(key: key);
@override

View file

@ -1,5 +1,3 @@
//@dart=2.12
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';

View file

@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart' show IterableExtension;
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart';
@ -14,14 +15,14 @@ class MessageReactions extends StatelessWidget {
final Event event;
final Timeline timeline;
const MessageReactions(this.event, this.timeline, {Key key})
const MessageReactions(this.event, this.timeline, {Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
final allReactionEvents =
event.aggregatedEvents(timeline, RelationshipTypes.reaction);
final reactionMap = <String, _ReactionEntry>{};
final reactionMap = <String?, _ReactionEntry>{};
final client = Matrix.of(context).client;
for (final e in allReactionEvents) {
@ -35,9 +36,9 @@ class MessageReactions extends StatelessWidget {
reactors: [],
);
}
reactionMap[key].count++;
reactionMap[key].reactors.add(e.sender);
reactionMap[key].reacted |= e.senderId == e.room.client.userID;
reactionMap[key]!.count++;
reactionMap[key]!.reactors!.add(e.sender);
reactionMap[key]!.reacted |= e.senderId == e.room.client.userID;
}
}
@ -52,11 +53,9 @@ class MessageReactions extends StatelessWidget {
reacted: r.reacted,
onTap: () {
if (r.reacted) {
final evt = allReactionEvents.firstWhere(
(e) =>
e.senderId == e.room.client.userID &&
e.content['m.relates_to']['key'] == r.key,
orElse: () => null);
final evt = allReactionEvents.firstWhereOrNull((e) =>
e.senderId == e.room.client.userID &&
e.content['m.relates_to']['key'] == r.key);
if (evt != null) {
showFutureLoadingDialog(
context: context,
@ -67,7 +66,7 @@ class MessageReactions extends StatelessWidget {
showFutureLoadingDialog(
context: context,
future: () =>
event.room.sendReaction(event.eventId, r.key));
event.room.sendReaction(event.eventId, r.key!));
}
},
onLongPress: () async => await _AdaptableReactorsDialog(
@ -91,11 +90,11 @@ class MessageReactions extends StatelessWidget {
}
class _Reaction extends StatelessWidget {
final String reactionKey;
final int count;
final bool reacted;
final void Function() onTap;
final void Function() onLongPress;
final String? reactionKey;
final int? count;
final bool? reacted;
final void Function()? onTap;
final void Function()? onLongPress;
const _Reaction({
this.reactionKey,
@ -113,11 +112,11 @@ class _Reaction extends StatelessWidget {
final color = Theme.of(context).scaffoldBackgroundColor;
final fontSize = DefaultTextStyle.of(context).style.fontSize;
Widget content;
if (reactionKey.startsWith('mxc://')) {
final src = Uri.parse(reactionKey)?.getThumbnail(
if (reactionKey!.startsWith('mxc://')) {
final src = Uri.parse(reactionKey!).getThumbnail(
Matrix.of(context).client,
width: 9999,
height: fontSize * MediaQuery.of(context).devicePixelRatio,
height: fontSize! * MediaQuery.of(context).devicePixelRatio,
method: ThumbnailMethod.scale,
);
content = Row(
@ -136,7 +135,7 @@ class _Reaction extends StatelessWidget {
],
);
} else {
var renderKey = Characters(reactionKey);
var renderKey = Characters(reactionKey!);
if (renderKey.length > 10) {
renderKey = renderKey.getRange(0, 9) + Characters('');
}
@ -147,13 +146,13 @@ class _Reaction extends StatelessWidget {
));
}
return InkWell(
onTap: () => onTap != null ? onTap() : null,
onLongPress: () => onLongPress != null ? onLongPress() : null,
onTap: () => onTap != null ? onTap!() : null,
onLongPress: () => onLongPress != null ? onLongPress!() : null,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: Container(
decoration: BoxDecoration(
color: color,
border: reacted
border: reacted!
? Border.all(
width: 1,
color: Theme.of(context).primaryColor,
@ -169,25 +168,30 @@ class _Reaction extends StatelessWidget {
}
class _ReactionEntry {
String key;
String? key;
int count;
bool reacted;
List<User> reactors;
List<User>? reactors;
_ReactionEntry({this.key, this.count, this.reacted, this.reactors});
_ReactionEntry({
this.key,
required this.count,
required this.reacted,
this.reactors,
});
}
class _AdaptableReactorsDialog extends StatelessWidget {
final Client client;
final _ReactionEntry reactionEntry;
final Client? client;
final _ReactionEntry? reactionEntry;
const _AdaptableReactorsDialog({
Key key,
Key? key,
this.client,
this.reactionEntry,
}) : super(key: key);
Future<bool> show(BuildContext context) => PlatformInfos.isCupertinoStyle
Future<bool?> show(BuildContext context) => PlatformInfos.isCupertinoStyle
? showCupertinoDialog(
context: context,
builder: (context) => this,
@ -209,20 +213,20 @@ class _AdaptableReactorsDialog extends StatelessWidget {
runSpacing: 4.0,
alignment: WrapAlignment.center,
children: <Widget>[
for (var reactor in reactionEntry.reactors)
for (var reactor in reactionEntry!.reactors!)
Chip(
avatar: Avatar(
mxContent: reactor.avatarUrl,
name: reactor.displayName,
client: client,
),
label: Text(reactor.displayName),
label: Text(reactor.displayName!),
),
],
),
);
final title = Center(child: Text(reactionEntry.key));
final title = Center(child: Text(reactionEntry!.key!));
return PlatformInfos.isCupertinoStyle
? CupertinoAlertDialog(

View file

@ -10,21 +10,23 @@ import 'html_message.dart';
class ReplyContent extends StatelessWidget {
final Event replyEvent;
final bool lightText;
final Timeline timeline;
final Timeline? timeline;
const ReplyContent(this.replyEvent,
{this.lightText = false, Key key, this.timeline})
: super(key: key);
const ReplyContent(
this.replyEvent, {
this.lightText = false,
Key? key,
this.timeline,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget replyBody;
final displayEvent = replyEvent != null && timeline != null
? replyEvent.getDisplayEvent(timeline)
: replyEvent;
final timeline = this.timeline;
final displayEvent =
timeline != null ? replyEvent.getDisplayEvent(timeline) : replyEvent;
final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor;
if (displayEvent != null &&
AppConfig.renderHtml &&
if (AppConfig.renderHtml &&
[EventTypes.Message, EventTypes.Encrypted]
.contains(displayEvent.type) &&
[MessageTypes.Text, MessageTypes.Notice, MessageTypes.Emote]
@ -32,16 +34,16 @@ class ReplyContent extends StatelessWidget {
!displayEvent.redacted &&
displayEvent.content['format'] == 'org.matrix.custom.html' &&
displayEvent.content['formatted_body'] is String) {
String html = displayEvent.content['formatted_body'];
String? html = displayEvent.content['formatted_body'];
if (displayEvent.messageType == MessageTypes.Emote) {
html = '* $html';
}
replyBody = HtmlMessage(
html: html,
html: html!,
defaultTextStyle: TextStyle(
color: lightText
? Colors.white
: Theme.of(context).textTheme.bodyText2.color,
: Theme.of(context).textTheme.bodyText2!.color,
fontSize: fontSize,
),
maxLines: 1,
@ -50,18 +52,17 @@ class ReplyContent extends StatelessWidget {
);
} else {
replyBody = Text(
displayEvent?.getLocalizedBody(
MatrixLocals(L10n.of(context)),
withSenderNamePrefix: false,
hideReply: true,
) ??
'',
displayEvent.getLocalizedBody(
MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false,
hideReply: true,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: lightText
? Colors.white
: Theme.of(context).textTheme.bodyText2.color,
: Theme.of(context).textTheme.bodyText2!.color,
fontSize: fontSize,
),
);
@ -81,7 +82,7 @@ class ReplyContent extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
(displayEvent?.sender?.calcDisplayname() ?? '') + ':',
displayEvent.sender.calcDisplayname() + ':',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(

View file

@ -9,16 +9,16 @@ import '../../../config/app_config.dart';
class StateMessage extends StatelessWidget {
final Event event;
final void Function(String) unfold;
const StateMessage(this.event, {@required this.unfold, Key key})
const StateMessage(this.event, {required this.unfold, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
if (event.unsigned['im.fluffychat.collapsed_state_event'] == true) {
if (event.unsigned!['im.fluffychat.collapsed_state_event'] == true) {
return Container();
}
final int counter =
event.unsigned['im.fluffychat.collapsed_state_event_count'] ?? 0;
event.unsigned!['im.fluffychat.collapsed_state_event_count'] ?? 0;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
@ -40,18 +40,18 @@ class StateMessage extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
event.getLocalizedBody(MatrixLocals(L10n.of(context))),
event.getLocalizedBody(MatrixLocals(L10n.of(context)!)),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14 * AppConfig.fontSizeFactor,
color: Theme.of(context).textTheme.bodyText2.color,
color: Theme.of(context).textTheme.bodyText2!.color,
decoration:
event.redacted ? TextDecoration.lineThrough : null,
),
),
if (counter != 0)
Text(
L10n.of(context).moreEvents(counter),
L10n.of(context)!.moreEvents(counter),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14 * AppConfig.fontSizeFactor,

View file

@ -10,14 +10,14 @@ import 'image_bubble.dart';
class Sticker extends StatefulWidget {
final Event event;
const Sticker(this.event, {Key key}) : super(key: key);
const Sticker(this.event, {Key? key}) : super(key: key);
@override
_StickerState createState() => _StickerState();
}
class _StickerState extends State<Sticker> {
bool animated;
bool? animated;
@override
Widget build(BuildContext context) {
@ -31,7 +31,7 @@ class _StickerState extends State<Sticker> {
showOkAlertDialog(
context: context,
message: widget.event.body,
okLabel: L10n.of(context).ok,
okLabel: L10n.of(context)!.ok,
);
},
animated: animated ?? AppConfig.autoplayImages,

View file

@ -9,7 +9,8 @@ class VerificationRequestContent extends StatelessWidget {
final Event event;
final Timeline timeline;
const VerificationRequestContent({this.event, this.timeline, Key key})
const VerificationRequestContent(
{required this.event, required this.timeline, Key? key})
: super(key: key);
@override
@ -50,10 +51,10 @@ class VerificationRequestContent extends StatelessWidget {
Text(canceled
? 'Error ${cancel.first.content.tryGet<String>('code')}: ${cancel.first.content.tryGet<String>('reason')}'
: (fullyDone
? L10n.of(context).verifySuccess
? L10n.of(context)!.verifySuccess
: (started
? L10n.of(context).loadingPleaseWait
: L10n.of(context).newVerificationRequest)))
? L10n.of(context)!.loadingPleaseWait
: L10n.of(context)!.newVerificationRequest)))
],
),
),

View file

@ -1,5 +1,3 @@
//@dart=2.12
import 'dart:io';
import 'package:flutter/foundation.dart';

View file

@ -15,19 +15,19 @@ import 'command_hints.dart';
class InputBar extends StatelessWidget {
final Room room;
final int minLines;
final int maxLines;
final TextInputType keyboardType;
final TextInputAction textInputAction;
final ValueChanged<String> onSubmitted;
final FocusNode focusNode;
final TextEditingController controller;
final InputDecoration decoration;
final ValueChanged<String> onChanged;
final bool autofocus;
final int? minLines;
final int? maxLines;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final ValueChanged<String>? onSubmitted;
final FocusNode? focusNode;
final TextEditingController? controller;
final InputDecoration? decoration;
final ValueChanged<String>? onChanged;
final bool? autofocus;
const InputBar({
this.room,
required this.room,
this.minLines,
this.maxLines,
this.keyboardType,
@ -38,22 +38,23 @@ class InputBar extends StatelessWidget {
this.onChanged,
this.autofocus,
this.textInputAction,
Key key,
Key? key,
}) : super(key: key);
List<Map<String, String>> getSuggestions(String text) {
if (controller.selection.baseOffset != controller.selection.extentOffset ||
controller.selection.baseOffset < 0) {
List<Map<String, String?>> getSuggestions(String text) {
if (controller!.selection.baseOffset !=
controller!.selection.extentOffset ||
controller!.selection.baseOffset < 0) {
return []; // no entries if there is selected text
}
final searchText =
controller.text.substring(0, controller.selection.baseOffset);
final ret = <Map<String, String>>[];
controller!.text.substring(0, controller!.selection.baseOffset);
final List<Map<String, String?>> ret = <Map<String, String>>[];
const maxResults = 30;
final commandMatch = RegExp(r'^\/([\w]*)$').firstMatch(searchText);
if (commandMatch != null) {
final commandSearch = commandMatch[1].toLowerCase();
final commandSearch = commandMatch[1]!.toLowerCase();
for (final command in room.client.commands.keys) {
if (command.contains(commandSearch)) {
ret.add({
@ -69,7 +70,7 @@ class InputBar extends StatelessWidget {
RegExp(r'(?:\s|^):(?:([-\w]+)~)?([-\w]+)$').firstMatch(searchText);
if (emojiMatch != null) {
final packSearch = emojiMatch[1];
final emoteSearch = emojiMatch[2].toLowerCase();
final emoteSearch = emojiMatch[2]!.toLowerCase();
final emotePacks = room.getImagePacks(ImagePackUsage.emoticon);
if (packSearch == null || packSearch.isEmpty) {
for (final pack in emotePacks.entries) {
@ -93,16 +94,16 @@ class InputBar extends StatelessWidget {
}
}
} else if (emotePacks[packSearch] != null) {
for (final emote in emotePacks[packSearch].images.entries) {
for (final emote in emotePacks[packSearch]!.images.entries) {
if (emote.key.toLowerCase().contains(emoteSearch)) {
ret.add({
'type': 'emote',
'name': emote.key,
'pack': packSearch,
'pack_avatar_url':
emotePacks[packSearch].pack.avatarUrl?.toString(),
emotePacks[packSearch]!.pack.avatarUrl?.toString(),
'pack_display_name':
emotePacks[packSearch].pack.displayName ?? packSearch,
emotePacks[packSearch]!.pack.displayName ?? packSearch,
'mxc': emote.value.url.toString(),
});
}
@ -114,11 +115,11 @@ class InputBar extends StatelessWidget {
}
final userMatch = RegExp(r'(?:\s|^)@([-\w]+)$').firstMatch(searchText);
if (userMatch != null) {
final userSearch = userMatch[1].toLowerCase();
final userSearch = userMatch[1]!.toLowerCase();
for (final user in room.getParticipants()) {
if ((user.displayName != null &&
(user.displayName.toLowerCase().contains(userSearch) ||
slugify(user.displayName.toLowerCase())
(user.displayName!.toLowerCase().contains(userSearch) ||
slugify(user.displayName!.toLowerCase())
.contains(userSearch))) ||
user.id.split(':')[0].toLowerCase().contains(userSearch)) {
ret.add({
@ -136,7 +137,7 @@ class InputBar extends StatelessWidget {
}
final roomMatch = RegExp(r'(?:\s|^)#([-\w]+)$').firstMatch(searchText);
if (roomMatch != null) {
final roomSearch = roomMatch[1].toLowerCase();
final roomSearch = roomMatch[1]!.toLowerCase();
for (final r in room.client.rooms) {
if (r.getState(EventTypes.RoomTombstone) != null) {
continue; // we don't care about tombstoned rooms
@ -155,12 +156,10 @@ class InputBar extends StatelessWidget {
.split(':')[0]
.toLowerCase()
.contains(roomSearch))))) ||
(r.name != null && r.name.toLowerCase().contains(roomSearch))) {
(r.name.toLowerCase().contains(roomSearch))) {
ret.add({
'type': 'room',
'mxid': (r.canonicalAlias != null && r.canonicalAlias.isNotEmpty)
? r.canonicalAlias
: r.id,
'mxid': (r.canonicalAlias.isNotEmpty) ? r.canonicalAlias : r.id,
'displayname': r.displayname,
'avatar_url': r.avatar?.toString(),
});
@ -175,14 +174,14 @@ class InputBar extends StatelessWidget {
Widget buildSuggestion(
BuildContext context,
Map<String, String> suggestion,
Client client,
Map<String, String?> suggestion,
Client? client,
) {
const size = 30.0;
const padding = EdgeInsets.all(4.0);
if (suggestion['type'] == 'command') {
final command = suggestion['name'];
final hint = commandHint(L10n.of(context), command);
final command = suggestion['name']!;
final hint = commandHint(L10n.of(context)!, command);
return Tooltip(
message: hint,
waitDuration: const Duration(days: 1), // don't show on hover
@ -206,7 +205,7 @@ class InputBar extends StatelessWidget {
}
if (suggestion['type'] == 'emote') {
final ratio = MediaQuery.of(context).devicePixelRatio;
final url = Uri.parse(suggestion['mxc'] ?? '')?.getThumbnail(
final url = Uri.parse(suggestion['mxc'] ?? '').getThumbnail(
room.client,
width: size * ratio,
height: size * ratio,
@ -224,7 +223,7 @@ class InputBar extends StatelessWidget {
height: size,
),
const SizedBox(width: 6),
Text(suggestion['name']),
Text(suggestion['name']!),
Expanded(
child: Align(
alignment: Alignment.centerRight,
@ -239,7 +238,7 @@ class InputBar extends StatelessWidget {
size: size * 0.9,
client: client,
)
: Text(suggestion['pack_display_name']),
: Text(suggestion['pack_display_name']!),
),
),
),
@ -262,7 +261,7 @@ class InputBar extends StatelessWidget {
client: client,
),
const SizedBox(width: 6),
Text(suggestion['displayname'] ?? suggestion['mxid']),
Text(suggestion['displayname'] ?? suggestion['mxid']!),
],
),
);
@ -270,16 +269,16 @@ class InputBar extends StatelessWidget {
return Container();
}
void insertSuggestion(_, Map<String, String> suggestion) {
void insertSuggestion(_, Map<String, String?> suggestion) {
final replaceText =
controller.text.substring(0, controller.selection.baseOffset);
controller!.text.substring(0, controller!.selection.baseOffset);
var startText = '';
final afterText = replaceText == controller.text
final afterText = replaceText == controller!.text
? ''
: controller.text.substring(controller.selection.baseOffset + 1);
: controller!.text.substring(controller!.selection.baseOffset + 1);
var insertText = '';
if (suggestion['type'] == 'command') {
insertText = suggestion['name'] + ' ';
insertText = suggestion['name']! + ' ';
startText = replaceText.replaceAllMapped(
RegExp(r'^(\/[\w]*)$'),
(Match m) => '/' + insertText,
@ -304,29 +303,29 @@ class InputBar extends StatelessWidget {
break;
}
}
insertText = ':${isUnique ? '' : insertPack + '~'}$insertEmote: ';
insertText = ':${isUnique ? '' : insertPack! + '~'}$insertEmote: ';
startText = replaceText.replaceAllMapped(
RegExp(r'(\s|^)(:(?:[-\w]+~)?[-\w]+)$'),
(Match m) => '${m[1]}$insertText',
);
}
if (suggestion['type'] == 'user') {
insertText = suggestion['mention'] + ' ';
insertText = suggestion['mention']! + ' ';
startText = replaceText.replaceAllMapped(
RegExp(r'(\s|^)(@[-\w]+)$'),
(Match m) => '${m[1]}$insertText',
);
}
if (suggestion['type'] == 'room') {
insertText = suggestion['mxid'] + ' ';
insertText = suggestion['mxid']! + ' ';
startText = replaceText.replaceAllMapped(
RegExp(r'(\s|^)(#[-\w]+)$'),
(Match m) => '${m[1]}$insertText',
);
}
if (insertText.isNotEmpty && startText.isNotEmpty) {
controller.text = startText + afterText;
controller.selection = TextSelection(
controller!.text = startText + afterText;
controller!.selection = TextSelection(
baseOffset: startText.length,
extentOffset: startText.length,
);
@ -351,13 +350,13 @@ class InputBar extends StatelessWidget {
? {}
: {
NewLineIntent: CallbackAction(onInvoke: (i) {
final val = controller.value;
final val = controller!.value;
final selection = val.selection.start;
final messageWithoutNewLine =
controller.text.substring(0, val.selection.start) +
controller!.text.substring(0, val.selection.start) +
'\n' +
controller.text.substring(val.selection.end);
controller.value = TextEditingValue(
controller!.text.substring(val.selection.end);
controller!.value = TextEditingValue(
text: messageWithoutNewLine,
selection: TextSelection.fromPosition(
TextPosition(offset: selection + 1),
@ -366,11 +365,11 @@ class InputBar extends StatelessWidget {
return null;
}),
SubmitLineIntent: CallbackAction(onInvoke: (i) {
onSubmitted(controller.text);
onSubmitted!(controller!.text);
return null;
}),
},
child: TypeAheadField<Map<String, String>>(
child: TypeAheadField<Map<String, String?>>(
direction: AxisDirection.up,
hideOnEmpty: true,
hideOnLoading: true,
@ -381,31 +380,31 @@ class InputBar extends StatelessWidget {
textFieldConfiguration: TextFieldConfiguration(
minLines: minLines,
maxLines: maxLines,
keyboardType: keyboardType,
keyboardType: keyboardType!,
textInputAction: textInputAction,
autofocus: autofocus,
autofocus: autofocus!,
onSubmitted: (text) {
// fix for library for now
// it sets the types for the callback incorrectly
onSubmitted(text);
onSubmitted!(text);
},
//focusNode: focusNode,
controller: controller,
decoration: decoration,
decoration: decoration!,
focusNode: focusNode,
onChanged: (text) {
// fix for the library for now
// it sets the types for the callback incorrectly
onChanged(text);
onChanged!(text);
},
textCapitalization: TextCapitalization.sentences,
),
suggestionsCallback: getSuggestions,
itemBuilder: (c, s) =>
buildSuggestion(c, s, Matrix.of(context).client),
onSuggestionSelected: (Map<String, String> suggestion) =>
onSuggestionSelected: (Map<String, String?> suggestion) =>
insertSuggestion(context, suggestion),
errorBuilder: (BuildContext context, Object error) => Container(),
errorBuilder: (BuildContext context, Object? error) => Container(),
loadingBuilder: (BuildContext context) =>
Container(), // fix loading briefly flickering a dark box
noItemsFoundBuilder: (BuildContext context) =>

View file

@ -8,14 +8,14 @@ import 'package:fluffychat/pages/chat/chat.dart';
class ReactionsPicker extends StatelessWidget {
final ChatController controller;
const ReactionsPicker(this.controller, {Key key}) : super(key: key);
const ReactionsPicker(this.controller, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
if (controller.showEmojiPicker) return Container();
final display = controller.editEvent == null &&
controller.replyEvent == null &&
controller.room.canSendDefaultMessages &&
controller.room!.canSendDefaultMessages &&
controller.selectedEvents.isNotEmpty;
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
@ -28,8 +28,9 @@ class ReactionsPicker extends StatelessWidget {
}
final emojis = List<String>.from(AppEmojis.emojis);
final allReactionEvents = controller.selectedEvents.first
.aggregatedEvents(controller.timeline, RelationshipTypes.reaction)
?.where((event) =>
.aggregatedEvents(
controller.timeline!, RelationshipTypes.reaction)
.where((event) =>
event.senderId == event.room.client.userID &&
event.type == 'm.reaction');

View file

@ -1,5 +1,3 @@
//@dart=2.12
import 'dart:async';
import 'package:flutter/cupertino.dart';

View file

@ -9,7 +9,7 @@ import 'events/reply_content.dart';
class ReplyDisplay extends StatelessWidget {
final ChatController controller;
const ReplyDisplay(this.controller, {Key key}) : super(key: key);
const ReplyDisplay(this.controller, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -23,16 +23,16 @@ class ReplyDisplay extends StatelessWidget {
child: Row(
children: <Widget>[
IconButton(
tooltip: L10n.of(context).close,
tooltip: L10n.of(context)!.close,
icon: const Icon(Icons.close),
onPressed: controller.cancelReplyEventAction,
),
Expanded(
child: controller.replyEvent != null
? ReplyContent(controller.replyEvent,
timeline: controller.timeline)
? ReplyContent(controller.replyEvent!,
timeline: controller.timeline!)
: _EditContent(controller.editEvent
?.getDisplayEvent(controller.timeline)),
?.getDisplayEvent(controller.timeline!)),
),
],
),
@ -42,7 +42,7 @@ class ReplyDisplay extends StatelessWidget {
}
class _EditContent extends StatelessWidget {
final Event event;
final Event? event;
const _EditContent(this.event);
@ -60,7 +60,7 @@ class _EditContent extends StatelessWidget {
Container(width: 15.0),
Text(
event?.getLocalizedBody(
MatrixLocals(L10n.of(context)),
MatrixLocals(L10n.of(context)!),
withSenderNamePrefix: false,
hideReply: true,
) ??
@ -68,7 +68,7 @@ class _EditContent extends StatelessWidget {
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
color: Theme.of(context).textTheme.bodyText2.color,
color: Theme.of(context).textTheme.bodyText2!.color,
),
),
],

View file

@ -8,12 +8,12 @@ import 'package:fluffychat/widgets/matrix.dart';
class SeenByRow extends StatelessWidget {
final ChatController controller;
const SeenByRow(this.controller, {Key key}) : super(key: key);
const SeenByRow(this.controller, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final seenByUsers = controller.room.getSeenByUsers(
controller.timeline,
final seenByUsers = controller.room!.getSeenByUsers(
controller.timeline!,
controller.filteredEvents,
controller.unfolded,
);

View file

@ -1,5 +1,3 @@
//@dart=2.12
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';

View file

@ -15,8 +15,8 @@ class SendLocationDialog extends StatefulWidget {
final Room room;
const SendLocationDialog({
this.room,
Key key,
required this.room,
Key? key,
}) : super(key: key);
@override
@ -27,8 +27,8 @@ class _SendLocationDialogState extends State<SendLocationDialog> {
bool disabled = false;
bool denied = false;
bool isSending = false;
Position position;
Error error;
Position? position;
Object? error;
@override
void initState() {
@ -75,9 +75,9 @@ class _SendLocationDialogState extends State<SendLocationDialog> {
void sendAction() async {
setState(() => isSending = true);
final body =
'https://www.openstreetmap.org/?mlat=${position.latitude}&mlon=${position.longitude}#map=16/${position.latitude}/${position.longitude}';
'https://www.openstreetmap.org/?mlat=${position!.latitude}&mlon=${position!.longitude}#map=16/${position!.latitude}/${position!.longitude}';
final uri =
'geo:${position.latitude},${position.longitude};u=${position.accuracy}';
'geo:${position!.latitude},${position!.longitude};u=${position!.accuracy}';
await showFutureLoadingDialog(
context: context,
future: () => widget.room.sendLocation(body, uri),
@ -90,16 +90,16 @@ class _SendLocationDialogState extends State<SendLocationDialog> {
Widget contentWidget;
if (position != null) {
contentWidget = MapBubble(
latitude: position.latitude,
longitude: position.longitude,
latitude: position!.latitude,
longitude: position!.longitude,
);
} else if (disabled) {
contentWidget = Text(L10n.of(context).locationDisabledNotice);
contentWidget = Text(L10n.of(context)!.locationDisabledNotice);
} else if (denied) {
contentWidget = Text(L10n.of(context).locationPermissionDeniedNotice);
contentWidget = Text(L10n.of(context)!.locationPermissionDeniedNotice);
} else if (error != null) {
contentWidget =
Text(L10n.of(context).errorObtainingLocation(error.toString()));
Text(L10n.of(context)!.errorObtainingLocation(error.toString()));
} else {
contentWidget = Row(
mainAxisSize: MainAxisSize.min,
@ -107,38 +107,38 @@ class _SendLocationDialogState extends State<SendLocationDialog> {
children: [
const CupertinoActivityIndicator(),
const SizedBox(width: 12),
Text(L10n.of(context).obtainingLocation),
Text(L10n.of(context)!.obtainingLocation),
],
);
}
if (PlatformInfos.isCupertinoStyle) {
return CupertinoAlertDialog(
title: Text(L10n.of(context).shareLocation),
title: Text(L10n.of(context)!.shareLocation),
content: contentWidget,
actions: [
CupertinoDialogAction(
onPressed: Navigator.of(context, rootNavigator: false).pop,
child: Text(L10n.of(context).cancel),
child: Text(L10n.of(context)!.cancel),
),
CupertinoDialogAction(
onPressed: isSending ? null : sendAction,
child: Text(L10n.of(context).send),
child: Text(L10n.of(context)!.send),
),
],
);
}
return AlertDialog(
title: Text(L10n.of(context).shareLocation),
title: Text(L10n.of(context)!.shareLocation),
content: contentWidget,
actions: [
TextButton(
onPressed: Navigator.of(context, rootNavigator: false).pop,
child: Text(L10n.of(context).cancel),
child: Text(L10n.of(context)!.cancel),
),
if (position != null)
TextButton(
onPressed: isSending ? null : sendAction,
child: Text(L10n.of(context).send),
child: Text(L10n.of(context)!.send),
),
],
);

View file

@ -10,14 +10,14 @@ import 'events/image_bubble.dart';
class StickerPickerDialog extends StatefulWidget {
final Room room;
const StickerPickerDialog({this.room, Key key}) : super(key: key);
const StickerPickerDialog({required this.room, Key? key}) : super(key: key);
@override
StickerPickerDialogState createState() => StickerPickerDialogState();
}
class StickerPickerDialogState extends State<StickerPickerDialog> {
String searchFilter;
String? searchFilter;
@override
Widget build(BuildContext context) {
@ -26,14 +26,14 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
// ignore: prefer_function_declarations_over_variables
final _packBuilder = (BuildContext context, int packIndex) {
final pack = stickerPacks[packSlugs[packIndex]];
final pack = stickerPacks[packSlugs[packIndex]]!;
final filteredImagePackImageEntried = pack.images.entries.toList();
if (searchFilter?.isNotEmpty ?? false) {
filteredImagePackImageEntried.removeWhere((e) =>
!(e.key.toLowerCase().contains(searchFilter.toLowerCase()) ||
!(e.key.toLowerCase().contains(searchFilter!.toLowerCase()) ||
(e.value.body
?.toLowerCase()
?.contains(searchFilter.toLowerCase()) ??
.contains(searchFilter!.toLowerCase()) ??
false)));
}
final imageKeys =
@ -62,7 +62,7 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (BuildContext context, int imageIndex) {
final image = pack.images[imageKeys[imageIndex]];
final image = pack.images[imageKeys[imageIndex]]!;
final fakeEvent = Event.fromJson(<String, dynamic>{
'type': EventTypes.Sticker,
'content': <String, dynamic>{
@ -116,7 +116,7 @@ class StickerPickerDialogState extends State<StickerPickerDialog> {
),
title: DefaultAppBarSearchField(
autofocus: false,
hintText: L10n.of(context).search,
hintText: L10n.of(context)!.search,
suffix: const Icon(Icons.search_outlined),
onChanged: (s) => setState(() => searchFilter = s),
),

View file

@ -7,11 +7,11 @@ import 'chat.dart';
class TombstoneDisplay extends StatelessWidget {
final ChatController controller;
const TombstoneDisplay(this.controller, {Key key}) : super(key: key);
const TombstoneDisplay(this.controller, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
if (controller.room.getState(EventTypes.RoomTombstone) == null) {
if (controller.room!.getState(EventTypes.RoomTombstone) == null) {
return Container();
}
return SizedBox(
@ -26,14 +26,14 @@ class TombstoneDisplay extends StatelessWidget {
child: const Icon(Icons.upgrade_outlined),
),
title: Text(
controller.room
.getState(EventTypes.RoomTombstone)
controller.room!
.getState(EventTypes.RoomTombstone)!
.parsedTombstoneContent
.body,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(L10n.of(context).goToTheNewRoom),
subtitle: Text(L10n.of(context)!.goToTheNewRoom),
onTap: controller.goToNewRoomAction,
),
),

View file

@ -8,11 +8,11 @@ import 'package:fluffychat/widgets/matrix.dart';
class TypingIndicators extends StatelessWidget {
final ChatController controller;
const TypingIndicators(this.controller, {Key key}) : super(key: key);
const TypingIndicators(this.controller, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final typingUsers = controller.room.typingUsers
final typingUsers = controller.room!.typingUsers
..removeWhere((u) => u.stateKey == Matrix.of(context).client.userID);
const topPadding = 20.0;
const bottomPadding = 4.0;