mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-01-15 11:18:41 +00:00
parent
59bfec9667
commit
89174a9c63
6 changed files with 158 additions and 112 deletions
|
|
@ -24,7 +24,7 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId = "eu.twonly"
|
applicationId = "eu.twonly.testing"
|
||||||
multiDexEnabled true
|
multiDexEnabled true
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||||
import 'package:twonly/src/providers/contacts_change_provider.dart';
|
import 'package:twonly/src/providers/contacts_change_provider.dart';
|
||||||
import 'package:twonly/src/providers/settings_change_provider.dart';
|
import 'package:twonly/src/providers/settings_change_provider.dart';
|
||||||
import 'package:twonly/src/services/notification_service.dart';
|
import 'package:twonly/src/services/notification_service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
|
||||||
import 'src/app.dart';
|
import 'src/app.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:cv/cv.dart';
|
import 'package:cv/cv.dart';
|
||||||
import 'package:fixnum/fixnum.dart';
|
import 'package:fixnum/fixnum.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||||
import 'package:twonly/globals.dart';
|
import 'package:twonly/globals.dart';
|
||||||
|
|
@ -52,8 +51,6 @@ class DbContacts extends CvModelBase {
|
||||||
static const columnCreatedAt = "created_at";
|
static const columnCreatedAt = "created_at";
|
||||||
final createdAt = CvField<DateTime>(columnCreatedAt);
|
final createdAt = CvField<DateTime>(columnCreatedAt);
|
||||||
|
|
||||||
static const nextFlameCounterInSeconds = kDebugMode ? 60 : 60 * 60 * 24;
|
|
||||||
|
|
||||||
static Future setupDatabaseTable(Database db) async {
|
static Future setupDatabaseTable(Database db) async {
|
||||||
String createTableString = """
|
String createTableString = """
|
||||||
CREATE TABLE IF NOT EXISTS $tableName (
|
CREATE TABLE IF NOT EXISTS $tableName (
|
||||||
|
|
@ -100,9 +97,9 @@ class DbContacts extends CvModelBase {
|
||||||
return await _getAllUsers();
|
return await _getAllUsers();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future checkAndUpdateFlames(int userId, {DateTime? timestamp}) async {
|
static Future updateTotalMediaCounter(
|
||||||
timestamp ??= DateTime.now();
|
int userId,
|
||||||
|
) async {
|
||||||
List<Map<String, dynamic>> result = await dbProvider.db!.query(
|
List<Map<String, dynamic>> result = await dbProvider.db!.query(
|
||||||
tableName,
|
tableName,
|
||||||
columns: [columnTotalMediaCounter],
|
columns: [columnTotalMediaCounter],
|
||||||
|
|
@ -112,7 +109,7 @@ class DbContacts extends CvModelBase {
|
||||||
|
|
||||||
if (result.isNotEmpty) {
|
if (result.isNotEmpty) {
|
||||||
int totalMediaCounter = result.first.cast()[columnTotalMediaCounter];
|
int totalMediaCounter = result.first.cast()[columnTotalMediaCounter];
|
||||||
_updateFlameCounter(userId, totalMediaCounter + 1);
|
_updateTotalMediaCounter(userId, totalMediaCounter + 1);
|
||||||
globalCallBackOnContactChange();
|
globalCallBackOnContactChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +202,8 @@ class DbContacts extends CvModelBase {
|
||||||
await _update(userId, updates);
|
await _update(userId, updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future _updateFlameCounter(int userId, int totalMediaCounter) async {
|
static Future _updateTotalMediaCounter(
|
||||||
|
int userId, int totalMediaCounter) async {
|
||||||
Map<String, dynamic> updates = {columnTotalMediaCounter: totalMediaCounter};
|
Map<String, dynamic> updates = {columnTotalMediaCounter: totalMediaCounter};
|
||||||
await _update(userId, updates, notifyFlutter: false);
|
await _update(userId, updates, notifyFlutter: false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,10 +110,6 @@ Future uploadMediaFile(
|
||||||
) async {
|
) async {
|
||||||
Box box = await getMediaStorage();
|
Box box = await getMediaStorage();
|
||||||
|
|
||||||
if ((await box.get("retransmit-$messageId-media") == null)) {
|
|
||||||
await box.put("retransmit-$messageId-media", encryptedMedia);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<int>? uploadToken = await box.get("retransmit-$messageId-uploadtoken");
|
List<int>? uploadToken = await box.get("retransmit-$messageId-uploadtoken");
|
||||||
if (uploadToken == null) {
|
if (uploadToken == null) {
|
||||||
Result res = await apiProvider.getUploadToken();
|
Result res = await apiProvider.getUploadToken();
|
||||||
|
|
@ -130,20 +126,34 @@ Future uploadMediaFile(
|
||||||
|
|
||||||
if (uploadToken == null) return;
|
if (uploadToken == null) return;
|
||||||
|
|
||||||
bool wasSend = await apiProvider.uploadData(uploadToken, encryptedMedia, 0);
|
int offset = await box.get("retransmit-$messageId-offset") ?? 0;
|
||||||
|
|
||||||
|
int fragmentedTransportSize = 100000;
|
||||||
|
|
||||||
|
while (offset < encryptedMedia.length) {
|
||||||
|
int end = encryptedMedia.length;
|
||||||
|
if (offset + fragmentedTransportSize < encryptedMedia.length) {
|
||||||
|
end = offset + fragmentedTransportSize;
|
||||||
|
}
|
||||||
|
bool wasSend = await apiProvider.uploadData(
|
||||||
|
uploadToken, encryptedMedia.sublist(offset, end), offset);
|
||||||
|
|
||||||
Logger("api.dart").shout("UPDATE...");
|
|
||||||
// TODO: fragmented upload...
|
|
||||||
if (!wasSend) {
|
if (!wasSend) {
|
||||||
Logger("api.dart").shout("error while uploading media");
|
Logger("api.dart").shout("error while uploading media");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await box.put("retransmit-$messageId-offset", offset);
|
||||||
|
|
||||||
|
offset = end;
|
||||||
|
}
|
||||||
|
|
||||||
Logger("api.dart").shout("DOING UPDATE");
|
Logger("api.dart").shout("DOING UPDATE");
|
||||||
|
|
||||||
box.delete("retransmit-$messageId-media");
|
box.delete("retransmit-$messageId-media");
|
||||||
box.delete("retransmit-$messageId-uploadtoken");
|
box.delete("retransmit-$messageId-uploadtoken");
|
||||||
await DbContacts.checkAndUpdateFlames(target.toInt());
|
|
||||||
|
await DbContacts.updateTotalMediaCounter(target.toInt());
|
||||||
|
|
||||||
// Ensures the retransmit of the message
|
// Ensures the retransmit of the message
|
||||||
await encryptAndSendMessage(
|
await encryptAndSendMessage(
|
||||||
|
|
@ -161,15 +171,40 @@ Future uploadMediaFile(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future encryptAndUploadMediaFile(
|
class SendImage {
|
||||||
Int64 target,
|
final Int64 userId;
|
||||||
Uint8List imageBytes,
|
final Uint8List imageBytes;
|
||||||
bool isRealTwonly,
|
final bool isRealTwonly;
|
||||||
int maxShowTime,
|
final int maxShowTime;
|
||||||
) async {
|
DateTime? messageSendAt;
|
||||||
DateTime messageSendAt = DateTime.now();
|
int? messageId;
|
||||||
int? messageId = await DbMessages.insertMyMessage(
|
Uint8List? encryptBytes;
|
||||||
target.toInt(),
|
|
||||||
|
SendImage({
|
||||||
|
required this.userId,
|
||||||
|
required this.imageBytes,
|
||||||
|
required this.isRealTwonly,
|
||||||
|
required this.maxShowTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future upload() async {
|
||||||
|
if (messageId == null || encryptBytes == null || messageSendAt == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await uploadMediaFile(messageId!, userId, encryptBytes!, isRealTwonly,
|
||||||
|
maxShowTime, messageSendAt!);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future encryptAndStore() async {
|
||||||
|
encryptBytes = await SignalHelper.encryptBytes(imageBytes, userId);
|
||||||
|
if (encryptBytes == null) {
|
||||||
|
Logger("api.dart").shout("Error encrypting media! Aborting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageSendAt = DateTime.now();
|
||||||
|
messageId = await DbMessages.insertMyMessage(
|
||||||
|
userId.toInt(),
|
||||||
MessageKind.image,
|
MessageKind.image,
|
||||||
MediaMessageContent(
|
MediaMessageContent(
|
||||||
downloadToken: [],
|
downloadToken: [],
|
||||||
|
|
@ -177,19 +212,15 @@ Future encryptAndUploadMediaFile(
|
||||||
isRealTwonly: isRealTwonly,
|
isRealTwonly: isRealTwonly,
|
||||||
isVideo: false,
|
isVideo: false,
|
||||||
),
|
),
|
||||||
messageSendAt);
|
messageSendAt!,
|
||||||
// isRealTwonly,
|
);
|
||||||
|
// should only happen when there is no space left on the smartphone -> abort message
|
||||||
if (messageId == null) return;
|
if (messageId == null) return;
|
||||||
|
|
||||||
Uint8List? encryptBytes = await SignalHelper.encryptBytes(imageBytes, target);
|
Box box = await getMediaStorage();
|
||||||
if (encryptBytes == null) {
|
await box.put("retransmit-$messageId-media", encryptBytes);
|
||||||
await DbMessages.deleteMessageById(messageId);
|
// message is safe until now -> would be retransmitted if sending would fail..
|
||||||
Logger("api.dart").shout("Error encrypting media! Deleting media.");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await uploadMediaFile(messageId, target, encryptBytes, isRealTwonly,
|
|
||||||
maxShowTime, messageSendAt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future sendImage(
|
Future sendImage(
|
||||||
|
|
@ -198,22 +229,39 @@ Future sendImage(
|
||||||
bool isRealTwonly,
|
bool isRealTwonly,
|
||||||
int maxShowTime,
|
int maxShowTime,
|
||||||
) async {
|
) async {
|
||||||
// 1. set notifier provider
|
|
||||||
|
|
||||||
Uint8List? imageBytesCompressed = await getCompressedImage(imageBytes);
|
Uint8List? imageBytesCompressed = await getCompressedImage(imageBytes);
|
||||||
if (imageBytesCompressed == null) {
|
if (imageBytesCompressed == null) {
|
||||||
Logger("api.dart").shout("Error compressing image!");
|
Logger("api.dart").shout("Error compressing image!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageBytesCompressed.length >= 10000000) {
|
||||||
|
Logger("api.dart").shout("Image to big aborting!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SendImage> tasks = [];
|
||||||
|
|
||||||
for (int i = 0; i < userIds.length; i++) {
|
for (int i = 0; i < userIds.length; i++) {
|
||||||
encryptAndUploadMediaFile(
|
tasks.add(
|
||||||
userIds[i],
|
SendImage(
|
||||||
imageBytesCompressed,
|
userId: userIds[i],
|
||||||
isRealTwonly,
|
imageBytes: imageBytesCompressed,
|
||||||
maxShowTime,
|
isRealTwonly: isRealTwonly,
|
||||||
|
maxShowTime: maxShowTime,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// first step encrypt and store the encrypted image
|
||||||
|
for (SendImage task in tasks) {
|
||||||
|
await task.encryptAndStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// after the images are safely stored try do upload them one by one
|
||||||
|
for (SendImage task in tasks) {
|
||||||
|
await task.upload();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future tryDownloadMedia(int messageId, int fromUserId, List<int> mediaToken,
|
Future tryDownloadMedia(int messageId, int fromUserId, List<int> mediaToken,
|
||||||
|
|
|
||||||
|
|
@ -181,8 +181,7 @@ Future<client.Response> handleNewMessage(
|
||||||
|
|
||||||
if (message.kind == MessageKind.video ||
|
if (message.kind == MessageKind.video ||
|
||||||
message.kind == MessageKind.image) {
|
message.kind == MessageKind.image) {
|
||||||
await DbContacts.checkAndUpdateFlames(fromUserId.toInt(),
|
await DbContacts.updateTotalMediaCounter(fromUserId.toInt());
|
||||||
timestamp: message.timestamp);
|
|
||||||
if (!globalIsAppInBackground) {
|
if (!globalIsAppInBackground) {
|
||||||
final content = message.content;
|
final content = message.content;
|
||||||
if (content is MediaMessageContent) {
|
if (content is MediaMessageContent) {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import 'package:twonly/src/providers/download_change_provider.dart';
|
||||||
import 'package:twonly/src/providers/messages_change_provider.dart';
|
import 'package:twonly/src/providers/messages_change_provider.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/views/chats/chat_item_details_view.dart';
|
import 'package:twonly/src/views/chats/chat_item_details_view.dart';
|
||||||
import 'package:twonly/src/views/home_view.dart';
|
|
||||||
import 'package:twonly/src/views/chats/media_viewer_view.dart';
|
import 'package:twonly/src/views/chats/media_viewer_view.dart';
|
||||||
import 'package:twonly/src/views/settings/settings_main_view.dart';
|
import 'package:twonly/src/views/settings/settings_main_view.dart';
|
||||||
import 'package:twonly/src/views/chats/search_username_view.dart';
|
import 'package:twonly/src/views/chats/search_username_view.dart';
|
||||||
|
|
@ -40,13 +39,13 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
.where((c) => c.accepted)
|
.where((c) => c.accepted)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
List<Contact> activeUsers = allUsers
|
allUsers.sort((b, a) {
|
||||||
.where((x) => lastMessages.containsKey(x.userId.toInt()))
|
DbMessage? msgA = lastMessages[a.userId.toInt()];
|
||||||
.toList();
|
DbMessage? msgB = lastMessages[a.userId.toInt()];
|
||||||
activeUsers.sort((b, a) {
|
if (msgA == null) return 1;
|
||||||
return lastMessages[a.userId.toInt()]!
|
if (msgB == null) return -1;
|
||||||
.sendAt
|
|
||||||
.compareTo(lastMessages[b.userId.toInt()]!.sendAt);
|
return msgA.sendAt.compareTo(msgB.sendAt);
|
||||||
});
|
});
|
||||||
|
|
||||||
int maxTotalMediaCounter = 0;
|
int maxTotalMediaCounter = 0;
|
||||||
|
|
@ -101,38 +100,32 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: (activeUsers.isEmpty)
|
body: (allUsers.isEmpty)
|
||||||
? Center(
|
? Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
icon: Icon((allUsers.isEmpty)
|
icon: Icon(Icons.person_add),
|
||||||
? Icons.person_add
|
|
||||||
: Icons.camera_alt),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
(allUsers.isEmpty)
|
Navigator.push(
|
||||||
? Navigator.push(
|
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => SearchUsernameView(),
|
builder: (context) => SearchUsernameView(),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
: globalUpdateOfHomeViewPageIndex(0);
|
|
||||||
},
|
},
|
||||||
label: Text((allUsers.isEmpty)
|
label: Text(context.lang.chatListViewSearchUserNameBtn)),
|
||||||
? context.lang.chatListViewSearchUserNameBtn
|
|
||||||
: context.lang.chatListViewSendFirstTwonly)),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
restorationId: 'chat_list_view',
|
restorationId: 'chat_list_view',
|
||||||
itemCount: activeUsers.length,
|
itemCount: allUsers.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
final user = activeUsers[index];
|
final user = allUsers[index];
|
||||||
return UserListItem(
|
return UserListItem(
|
||||||
user: user,
|
user: user,
|
||||||
maxTotalMediaCounter: maxTotalMediaCounter,
|
maxTotalMediaCounter: maxTotalMediaCounter,
|
||||||
lastMessage: lastMessages[user.userId.toInt()]!,
|
lastMessage: lastMessages[user.userId.toInt()],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -142,7 +135,7 @@ class _ChatListViewState extends State<ChatListView> {
|
||||||
|
|
||||||
class UserListItem extends StatefulWidget {
|
class UserListItem extends StatefulWidget {
|
||||||
final Contact user;
|
final Contact user;
|
||||||
final DbMessage lastMessage;
|
final DbMessage? lastMessage;
|
||||||
final int maxTotalMediaCounter;
|
final int maxTotalMediaCounter;
|
||||||
|
|
||||||
const UserListItem(
|
const UserListItem(
|
||||||
|
|
@ -157,26 +150,29 @@ class UserListItem extends StatefulWidget {
|
||||||
|
|
||||||
class _UserListItem extends State<UserListItem> {
|
class _UserListItem extends State<UserListItem> {
|
||||||
int lastMessageInSeconds = 0;
|
int lastMessageInSeconds = 0;
|
||||||
|
MessageSendState state = MessageSendState.send;
|
||||||
|
bool isDownloading = false;
|
||||||
|
List<int> token = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
int lastMessageInSeconds =
|
if (widget.lastMessage != null) {
|
||||||
DateTime.now().difference(widget.lastMessage.sendAt).inSeconds;
|
lastMessageInSeconds =
|
||||||
|
DateTime.now().difference(widget.lastMessage!.sendAt).inSeconds;
|
||||||
|
|
||||||
MessageSendState state = widget.lastMessage.getSendState();
|
state = widget.lastMessage!.getSendState();
|
||||||
bool isDownloading = false;
|
|
||||||
|
|
||||||
final content = widget.lastMessage.messageContent;
|
final content = widget.lastMessage!.messageContent;
|
||||||
List<int> token = [];
|
|
||||||
|
|
||||||
if (widget.lastMessage.messageReceived && content is MediaMessageContent) {
|
if (widget.lastMessage!.messageReceived &&
|
||||||
|
content is MediaMessageContent) {
|
||||||
token = content.downloadToken;
|
token = content.downloadToken;
|
||||||
isDownloading = context
|
isDownloading = context
|
||||||
.watch<DownloadChangeProvider>()
|
.watch<DownloadChangeProvider>()
|
||||||
.currentlyDownloading
|
.currentlyDownloading
|
||||||
.contains(token.toString());
|
.contains(token.toString());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
int flameCounter = context
|
int flameCounter = context
|
||||||
.watch<MessagesChangeProvider>()
|
.watch<MessagesChangeProvider>()
|
||||||
.flamesCounter[widget.user.userId.toInt()] ??
|
.flamesCounter[widget.user.userId.toInt()] ??
|
||||||
|
|
@ -186,9 +182,11 @@ class _UserListItem extends State<UserListItem> {
|
||||||
user: widget.user,
|
user: widget.user,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(widget.user.displayName),
|
title: Text(widget.user.displayName),
|
||||||
subtitle: Row(
|
subtitle: (widget.lastMessage == null)
|
||||||
|
? Text("Tap to send your first image.")
|
||||||
|
: Row(
|
||||||
children: [
|
children: [
|
||||||
MessageSendStateIcon(widget.lastMessage),
|
MessageSendStateIcon(widget.lastMessage!),
|
||||||
Text("•"),
|
Text("•"),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -206,19 +204,23 @@ class _UserListItem extends State<UserListItem> {
|
||||||
),
|
),
|
||||||
leading: InitialsAvatar(displayName: widget.user.displayName),
|
leading: InitialsAvatar(displayName: widget.user.displayName),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (widget.lastMessage == null) {
|
||||||
|
print("TODO: implement sending to one person!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isDownloading) return;
|
if (isDownloading) return;
|
||||||
if (!widget.lastMessage.isDownloaded) {
|
if (!widget.lastMessage!.isDownloaded) {
|
||||||
tryDownloadMedia(widget.lastMessage.messageId,
|
tryDownloadMedia(widget.lastMessage!.messageId,
|
||||||
widget.lastMessage.otherUserId, token,
|
widget.lastMessage!.otherUserId, token,
|
||||||
force: true);
|
force: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state == MessageSendState.received &&
|
if (state == MessageSendState.received &&
|
||||||
widget.lastMessage.containsOtherMedia()) {
|
widget.lastMessage!.containsOtherMedia()) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) {
|
MaterialPageRoute(builder: (context) {
|
||||||
return MediaViewerView(widget.user, widget.lastMessage);
|
return MediaViewerView(widget.user, widget.lastMessage!);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue