use improved message content

This commit is contained in:
otsmr 2025-02-04 22:42:18 +01:00
parent d3bc0dc135
commit 2e7b0edce3
15 changed files with 260 additions and 190 deletions

View file

@ -20,11 +20,11 @@ class FlameCounterWidget extends StatelessWidget {
const SizedBox(width: 5), const SizedBox(width: 5),
Text( Text(
user.flameCounter.toString(), user.flameCounter.toString(),
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 13),
), ),
Text( Text(
(maxTotalMediaCounter == user.totalMediaCounter) ? "❤️‍🔥" : "🔥", (maxTotalMediaCounter == user.totalMediaCounter) ? "❤️‍🔥" : "🔥",
style: const TextStyle(fontSize: 10), style: const TextStyle(fontSize: 14),
), ),
], ],
); );

View file

@ -9,6 +9,7 @@ class Layer {
bool isEditing; bool isEditing;
bool isDeleted; bool isDeleted;
bool hasCustomActionButtons; bool hasCustomActionButtons;
bool showCustomButtons;
Layer({ Layer({
this.offset = const Offset(0, 0), this.offset = const Offset(0, 0),
@ -16,6 +17,7 @@ class Layer {
this.isEditing = false, this.isEditing = false,
this.isDeleted = false, this.isDeleted = false,
this.hasCustomActionButtons = false, this.hasCustomActionButtons = false,
this.showCustomButtons = true,
this.rotation = 0, this.rotation = 0,
this.scale = 1, this.scale = 1,
}); });

View file

@ -98,7 +98,7 @@ class _DrawLayerState extends State<DrawLayer> {
), ),
), ),
), ),
if (widget.layerData.isEditing) if (widget.layerData.isEditing && widget.layerData.showCustomButtons)
Positioned( Positioned(
top: 5, top: 5,
left: 5, left: 5,
@ -143,7 +143,7 @@ class _DrawLayerState extends State<DrawLayer> {
], ],
), ),
), ),
if (widget.layerData.isEditing) if (widget.layerData.isEditing && widget.layerData.showCustomButtons)
Positioned( Positioned(
right: 20, right: 20,
top: 50, top: 50,

View file

@ -70,11 +70,10 @@ class MessageSendStateIcon extends StatelessWidget {
} }
bool isDownloading = false; bool isDownloading = false;
if (message.messageContent != null && final content = message.messageContent;
message.messageContent!.downloadToken != null) { if (message.messageReceived && content is MediaMessageContent) {
final test = context.watch<DownloadChangeProvider>().currentlyDownloading; final test = context.watch<DownloadChangeProvider>().currentlyDownloading;
isDownloading = isDownloading = test.contains(content.downloadToken.toString());
test.contains(message.messageContent!.downloadToken.toString());
} }
if (isDownloading) { if (isDownloading) {

View file

@ -1,7 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:twonly/src/utils/json.dart';
part 'message.g.dart';
enum MessageKind { enum MessageKind {
textMessage, textMessage,
@ -38,24 +35,17 @@ extension MessageKindExtension on MessageKind {
} }
} }
// so _$MessageKindEnumMap gets generated // TODO: use message as base class, remove kind and flatten content
@JsonSerializable()
class _MessageKind {
MessageKind? kind;
}
@JsonSerializable()
class Message { class Message {
@Int64Converter()
final MessageKind kind; final MessageKind kind;
final MessageContent? content; final MessageContent content;
final int? messageId; final int? messageId;
DateTime timestamp; DateTime timestamp;
Message( Message(
{required this.kind, {required this.kind,
this.messageId, this.messageId,
this.content, required this.content,
required this.timestamp}); required this.timestamp});
@override @override
@ -63,19 +53,85 @@ class Message {
return 'Message(kind: $kind, content: $content, timestamp: $timestamp)'; return 'Message(kind: $kind, content: $content, timestamp: $timestamp)';
} }
factory Message.fromJson(Map<String, dynamic> json) => static Message fromJson(Map<String, dynamic> json) => Message(
_$MessageFromJson(json); kind: MessageKindExtension.fromString(json["kind"]),
Map<String, dynamic> toJson() => _$MessageToJson(this); messageId: (json['messageId'] as num?)?.toInt(),
content:
MessageContent.fromJson(json['content'] as Map<String, dynamic>),
timestamp: DateTime.parse(json['timestamp'] as String),
);
Map<String, dynamic> toJson() => <String, dynamic>{
'kind': kind.name,
'content': content.toJson(),
'messageId': messageId,
'timestamp': timestamp.toIso8601String(),
};
} }
@JsonSerializable()
class MessageContent { class MessageContent {
final String? text; MessageContent();
final List<int>? downloadToken;
MessageContent({required this.text, required this.downloadToken}); static MessageContent fromJson(Map json) {
switch (json['type']) {
case 'MediaMessageContent':
return MediaMessageContent.fromJson(json);
case 'TextMessageContent':
return TextMessageContent.fromJson(json);
default:
return MessageContent();
}
}
factory MessageContent.fromJson(Map<String, dynamic> json) => Map toJson() {
_$MessageContentFromJson(json); return {};
Map<String, dynamic> toJson() => _$MessageContentToJson(this); }
}
class MediaMessageContent extends MessageContent {
final List<int> downloadToken;
final int maxShowTime;
final bool isRealTwonly;
MediaMessageContent({
required this.downloadToken,
required this.maxShowTime,
required this.isRealTwonly,
});
static MediaMessageContent fromJson(Map json) {
return MediaMessageContent(
downloadToken: List<int>.from(json['downloadToken']),
maxShowTime: json['maxShowTime'],
isRealTwonly: json['isRealTwonly'],
);
}
@override
Map toJson() {
return {
'type': 'MediaMessageContent',
'downloadToken': downloadToken,
'isRealTwonly': isRealTwonly,
'maxShowTime': maxShowTime,
};
}
}
class TextMessageContent extends MessageContent {
final String text;
TextMessageContent({required this.text});
static TextMessageContent fromJson(Map json) {
return TextMessageContent(
text: json['text'],
);
}
@override
Map toJson() {
return {
'type': 'TextMessageContent',
'text': text,
};
}
} }

View file

@ -1,56 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'message.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_MessageKind _$MessageKindFromJson(Map<String, dynamic> json) => _MessageKind()
..kind = $enumDecodeNullable(_$MessageKindEnumMap, json['kind']);
Map<String, dynamic> _$MessageKindToJson(_MessageKind instance) =>
<String, dynamic>{
'kind': _$MessageKindEnumMap[instance.kind],
};
const _$MessageKindEnumMap = {
MessageKind.textMessage: 'textMessage',
MessageKind.image: 'image',
MessageKind.video: 'video',
MessageKind.contactRequest: 'contactRequest',
MessageKind.rejectRequest: 'rejectRequest',
MessageKind.acceptRequest: 'acceptRequest',
MessageKind.opened: 'opened',
MessageKind.ack: 'ack',
};
Message _$MessageFromJson(Map<String, dynamic> json) => Message(
kind: $enumDecode(_$MessageKindEnumMap, json['kind']),
messageId: (json['messageId'] as num?)?.toInt(),
content: json['content'] == null
? null
: MessageContent.fromJson(json['content'] as Map<String, dynamic>),
timestamp: DateTime.parse(json['timestamp'] as String),
);
Map<String, dynamic> _$MessageToJson(Message instance) => <String, dynamic>{
'kind': _$MessageKindEnumMap[instance.kind]!,
'content': instance.content,
'messageId': instance.messageId,
'timestamp': instance.timestamp.toIso8601String(),
};
MessageContent _$MessageContentFromJson(Map<String, dynamic> json) =>
MessageContent(
text: json['text'] as String?,
downloadToken: (json['downloadToken'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
);
Map<String, dynamic> _$MessageContentToJson(MessageContent instance) =>
<String, dynamic>{
'text': instance.text,
'downloadToken': instance.downloadToken,
};

View file

@ -39,6 +39,8 @@ class DbMessage {
return isMedia(); return isMedia();
} }
bool get messageReceived => messageOtherId != null;
bool isMedia() { bool isMedia() {
return messageKind == MessageKind.image || messageKind == MessageKind.video; return messageKind == MessageKind.image || messageKind == MessageKind.video;
} }
@ -84,7 +86,7 @@ class DbMessages extends CvModelBase {
final messageKind = CvField<int>(columnMessageKind); final messageKind = CvField<int>(columnMessageKind);
static const columnMessageContentJson = "message_json"; static const columnMessageContentJson = "message_json";
final messageContentJson = CvField<String?>(columnMessageContentJson); final messageContentJson = CvField<String>(columnMessageContentJson);
static const columnMessageOpenedAt = "message_opened_at"; static const columnMessageOpenedAt = "message_opened_at";
final messageOpenedAt = CvField<DateTime?>(columnMessageOpenedAt); final messageOpenedAt = CvField<DateTime?>(columnMessageOpenedAt);
@ -112,7 +114,7 @@ class DbMessages extends CvModelBase {
$columnMessageKind INTEGER NOT NULL, $columnMessageKind INTEGER NOT NULL,
$columnMessageAcknowledgeByUser INTEGER NOT NULL DEFAULT 0, $columnMessageAcknowledgeByUser INTEGER NOT NULL DEFAULT 0,
$columnMessageAcknowledgeByServer INTEGER NOT NULL DEFAULT 0, $columnMessageAcknowledgeByServer INTEGER NOT NULL DEFAULT 0,
$columnMessageContentJson TEXT DEFAULT NULL, $columnMessageContentJson TEXT NOT NULL,
$columnMessageOpenedAt DATETIME DEFAULT NULL, $columnMessageOpenedAt DATETIME DEFAULT NULL,
$columnSendOrReceivedAt DATETIME DEFAULT CURRENT_TIMESTAMP, $columnSendOrReceivedAt DATETIME DEFAULT CURRENT_TIMESTAMP,
$columnUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP $columnUpdatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
@ -145,12 +147,12 @@ class DbMessages extends CvModelBase {
return null; return null;
} }
static Future<int?> insertMyMessage(int userIdFrom, MessageKind kind, static Future<int?> insertMyMessage(
{String? jsonContent}) async { int userIdFrom, MessageKind kind, MessageContent content) async {
try { try {
int messageId = await dbProvider.db!.insert(tableName, { int messageId = await dbProvider.db!.insert(tableName, {
columnMessageKind: kind.index, columnMessageKind: kind.index,
columnMessageContentJson: jsonContent, columnMessageContentJson: jsonEncode(content.toJson()),
columnOtherUserId: userIdFrom, columnOtherUserId: userIdFrom,
columnSendOrReceivedAt: DateTime.now().toIso8601String() columnSendOrReceivedAt: DateTime.now().toIso8601String()
}); });
@ -235,7 +237,9 @@ class DbMessages extends CvModelBase {
List<DbMessage> receivedByOther = messages List<DbMessage> receivedByOther = messages
.where((c) => c.messageOtherId != null && c.messageOpenedAt == null) .where((c) => c.messageOtherId != null && c.messageOpenedAt == null)
.toList(); .toList();
if (receivedByOther.isNotEmpty) return receivedByOther[0]; if (receivedByOther.isNotEmpty) {
return receivedByOther[receivedByOther.length - 1];
}
// check if there is a message which was not ack by the server // check if there is a message which was not ack by the server
List<DbMessage> notAckByServer = List<DbMessage> notAckByServer =
@ -343,19 +347,16 @@ class DbMessages extends CvModelBase {
if (messageOpenedAt != null) { if (messageOpenedAt != null) {
messageOpenedAt = DateTime.tryParse(fromDb[i][columnMessageOpenedAt]); messageOpenedAt = DateTime.tryParse(fromDb[i][columnMessageOpenedAt]);
} }
dynamic content = fromDb[i][columnMessageContentJson]; int? messageOtherId = fromDb[i][columnMessageOtherId];
if (content != null) { MessageContent content = MessageContent.fromJson(
content = MessageContent.fromJson( jsonDecode(fromDb[i][columnMessageContentJson]));
jsonDecode(fromDb[i][columnMessageContentJson]));
}
MessageKind messageKind = MessageKind messageKind =
MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]); MessageKindExtension.fromIndex(fromDb[i][columnMessageKind]);
bool isDownloaded = true; bool isDownloaded = true;
if (messageKind == MessageKind.image || if (messageOtherId != null) {
messageKind == MessageKind.video) { if (content is MediaMessageContent) {
// when the media was send from the user itself the content is null // when the media was send from the user itself the content is null
if (content != null) { isDownloaded = await isMediaDownloaded(content.downloadToken);
isDownloaded = await isMediaDownloaded(content.downloadToken!);
} }
} }
parsedUsers.add( parsedUsers.add(
@ -363,7 +364,7 @@ class DbMessages extends CvModelBase {
sendOrReceivedAt: sendOrReceivedAt:
DateTime.tryParse(fromDb[i][columnSendOrReceivedAt])!, DateTime.tryParse(fromDb[i][columnSendOrReceivedAt])!,
messageId: fromDb[i][columnMessageId], messageId: fromDb[i][columnMessageId],
messageOtherId: fromDb[i][columnMessageOtherId], messageOtherId: messageOtherId,
otherUserId: fromDb[i][columnOtherUserId], otherUserId: fromDb[i][columnOtherUserId],
messageKind: messageKind, messageKind: messageKind,
messageContent: content, messageContent: content,

View file

@ -30,7 +30,9 @@ Future tryTransmitMessages() async {
Uint8List? bytes = box.get("retransmit-$msgId-textmessage"); Uint8List? bytes = box.get("retransmit-$msgId-textmessage");
if (bytes != null) { if (bytes != null) {
Result resp = await apiProvider.sendTextMessage( Result resp = await apiProvider.sendTextMessage(
Int64(retransmit[i].otherUserId), bytes); Int64(retransmit[i].otherUserId),
bytes,
);
if (resp.isSuccess) { if (resp.isSuccess) {
DbMessages.acknowledgeMessageByServer(msgId); DbMessages.acknowledgeMessageByServer(msgId);
@ -42,7 +44,16 @@ Future tryTransmitMessages() async {
Uint8List? encryptedMedia = await box.get("retransmit-$msgId-media"); Uint8List? encryptedMedia = await box.get("retransmit-$msgId-media");
if (encryptedMedia != null) { if (encryptedMedia != null) {
uploadMediaFile(msgId, Int64(retransmit[i].otherUserId), encryptedMedia); final content = retransmit[i].messageContent;
if (content is MediaMessageContent) {
uploadMediaFile(
msgId,
Int64(retransmit[i].otherUserId),
encryptedMedia,
content.isRealTwonly,
content.maxShowTime,
);
}
} }
} }
} }
@ -75,11 +86,13 @@ Future<Result> encryptAndSendMessage(Int64 userId, Message msg) async {
} }
Future sendTextMessage(Int64 target, String message) async { Future sendTextMessage(Int64 target, String message) async {
MessageContent content = MessageContent(text: message, downloadToken: null); MessageContent content = TextMessageContent(text: message);
int? messageId = await DbMessages.insertMyMessage( int? messageId = await DbMessages.insertMyMessage(
target.toInt(), MessageKind.textMessage, target.toInt(),
jsonContent: jsonEncode(content.toJson())); MessageKind.textMessage,
content,
);
if (messageId == null) return; if (messageId == null) return;
Message msg = Message( Message msg = Message(
@ -94,7 +107,12 @@ Future sendTextMessage(Int64 target, String message) async {
// this will send the media file and ensures retransmission when errors occur // this will send the media file and ensures retransmission when errors occur
Future uploadMediaFile( Future uploadMediaFile(
int messageId, Int64 target, Uint8List encryptedMedia) async { int messageId,
Int64 target,
Uint8List encryptedMedia,
bool isRealTwonly,
int maxShowTime,
) async {
Box box = await getMediaStorage(); Box box = await getMediaStorage();
if ((await box.get("retransmit-$messageId-media") == null)) { if ((await box.get("retransmit-$messageId-media") == null)) {
@ -133,15 +151,31 @@ Future uploadMediaFile(
Message( Message(
kind: MessageKind.image, kind: MessageKind.image,
messageId: messageId, messageId: messageId,
content: MessageContent(text: null, downloadToken: uploadToken), content: MediaMessageContent(
downloadToken: uploadToken,
maxShowTime: maxShowTime,
isRealTwonly: isRealTwonly,
),
timestamp: DateTime.now(), timestamp: DateTime.now(),
), ),
); );
} }
Future encryptAndUploadMediaFile(Int64 target, Uint8List imageBytes) async { Future encryptAndUploadMediaFile(
int? messageId = Int64 target,
await DbMessages.insertMyMessage(target.toInt(), MessageKind.image); Uint8List imageBytes,
bool isRealTwonly,
int maxShowTime,
) async {
int? messageId = await DbMessages.insertMyMessage(
target.toInt(),
MessageKind.image,
MediaMessageContent(
downloadToken: [],
maxShowTime: maxShowTime,
isRealTwonly: isRealTwonly,
));
// isRealTwonly,
if (messageId == null) return; if (messageId == null) return;
Uint8List? encryptBytes = await SignalHelper.encryptBytes(imageBytes, target); Uint8List? encryptBytes = await SignalHelper.encryptBytes(imageBytes, target);
@ -151,11 +185,16 @@ Future encryptAndUploadMediaFile(Int64 target, Uint8List imageBytes) async {
return; return;
} }
await uploadMediaFile(messageId, target, encryptBytes); await uploadMediaFile(
messageId, target, encryptBytes, isRealTwonly, maxShowTime);
} }
Future sendImage(List<Int64> userIds, Uint8List imageBytes, bool isRealTwonly, Future sendImage(
int maxShowTime) async { List<Int64> userIds,
Uint8List imageBytes,
bool isRealTwonly,
int maxShowTime,
) async {
// 1. set notifier provider // 1. set notifier provider
Uint8List? imageBytesCompressed = await getCompressedImage(imageBytes); Uint8List? imageBytesCompressed = await getCompressedImage(imageBytes);
@ -165,7 +204,12 @@ Future sendImage(List<Int64> userIds, Uint8List imageBytes, bool isRealTwonly,
} }
for (int i = 0; i < userIds.length; i++) { for (int i = 0; i < userIds.length; i++) {
encryptAndUploadMediaFile(userIds[i], imageBytesCompressed); encryptAndUploadMediaFile(
userIds[i],
imageBytesCompressed,
isRealTwonly,
maxShowTime,
);
} }
} }
@ -198,6 +242,7 @@ Future userOpenedOtherMessage(int fromUserId, int messageOtherId) async {
Message( Message(
kind: MessageKind.opened, kind: MessageKind.opened,
messageId: messageOtherId, messageId: messageOtherId,
content: MessageContent(),
timestamp: DateTime.now(), timestamp: DateTime.now(),
), ),
); );

View file

@ -129,7 +129,7 @@ Future<client.Response> handleNewMessage(
Logger("handleServerMessages") Logger("handleServerMessages")
.shout("Got unknown MessageKind $message"); .shout("Got unknown MessageKind $message");
} else { } else {
String content = jsonEncode(message.content!.toJson()); String content = jsonEncode(message.content.toJson());
int? messageId = await DbMessages.insertOtherMessage( int? messageId = await DbMessages.insertOtherMessage(
fromUserId.toInt(), message.kind, message.messageId!, content); fromUserId.toInt(), message.kind, message.messageId!, content);
@ -142,6 +142,7 @@ Future<client.Response> handleNewMessage(
Message( Message(
kind: MessageKind.ack, kind: MessageKind.ack,
messageId: message.messageId!, messageId: message.messageId!,
content: MessageContent(),
timestamp: DateTime.now(), timestamp: DateTime.now(),
), ),
); );
@ -151,13 +152,13 @@ Future<client.Response> handleNewMessage(
await DbContacts.checkAndUpdateFlames(fromUserId.toInt(), await DbContacts.checkAndUpdateFlames(fromUserId.toInt(),
timestamp: message.timestamp); timestamp: message.timestamp);
dynamic content = message.content!; final content = message.content;
List<int> downloadToken = content.downloadToken; if (content is MediaMessageContent) {
List<int> downloadToken = content.downloadToken;
Box box = await getMediaStorage(); Box box = await getMediaStorage();
box.put("${downloadToken}_fromUserId", fromUserId.toInt()); box.put("${downloadToken}_fromUserId", fromUserId.toInt());
tryDownloadMedia(downloadToken);
tryDownloadMedia(downloadToken); }
} }
} }
} }

View file

@ -99,7 +99,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
child: ActionButton( child: ActionButton(
FontAwesomeIcons.stopwatch, FontAwesomeIcons.stopwatch,
tooltipText: context.lang.protectAsARealTwonly, tooltipText: context.lang.protectAsARealTwonly,
disable: _isRealTwonly, // disable: _isRealTwonly,
onPressed: () async { onPressed: () async {
if (_maxShowTime == 999999) { if (_maxShowTime == 999999) {
_maxShowTime = 4; _maxShowTime = 4;
@ -184,7 +184,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
Uint8List? image; Uint8List? image;
if (layers.length > 1) { if (layers.length > 1) {
for (var x in layers) {
x.showCustomButtons = false;
}
image = await screenshotController.capture(pixelRatio: pixelRatio); image = await screenshotController.capture(pixelRatio: pixelRatio);
for (var x in layers) {
x.showCustomButtons = true;
}
} else if (layers.length == 1) { } else if (layers.length == 1) {
if (layers.first is BackgroundLayerData) { if (layers.first is BackgroundLayerData) {
image = (layers.first as BackgroundLayerData).image.bytes; image = (layers.first as BackgroundLayerData).image.bytes;
@ -313,14 +319,13 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
const SizedBox(width: 20), const SizedBox(width: 20),
FilledButton.icon( FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane), icon: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async { onPressed: () {
Uint8List? imageBytes = await getMergedImage(); Future<Uint8List?> imageBytes = getMergedImage();
if (imageBytes == null || !context.mounted) return;
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ShareImageView( builder: (context) => ShareImageView(
imageBytes: imageBytes, imageBytesFuture: imageBytes,
isRealTwonly: _isRealTwonly, isRealTwonly: _isRealTwonly,
maxShowTime: _maxShowTime, maxShowTime: _maxShowTime,
), ),

View file

@ -15,10 +15,10 @@ import 'package:twonly/src/views/home_view.dart';
class ShareImageView extends StatefulWidget { class ShareImageView extends StatefulWidget {
const ShareImageView( const ShareImageView(
{super.key, {super.key,
required this.imageBytes, required this.imageBytesFuture,
required this.isRealTwonly, required this.isRealTwonly,
required this.maxShowTime}); required this.maxShowTime});
final Uint8List imageBytes; final Future<Uint8List?> imageBytesFuture;
final bool isRealTwonly; final bool isRealTwonly;
final int maxShowTime; final int maxShowTime;
@ -31,21 +31,23 @@ class _ShareImageView extends State<ShareImageView> {
List<Contact> _otherUsers = []; List<Contact> _otherUsers = [];
List<Contact> _bestFriends = []; List<Contact> _bestFriends = [];
int maxTotalMediaCounter = 0; int maxTotalMediaCounter = 0;
Uint8List? imageBytes;
final HashSet<Int64> _selectedUserIds = HashSet<Int64>(); final HashSet<Int64> _selectedUserIds = HashSet<Int64>();
final TextEditingController searchUserName = TextEditingController(); final TextEditingController searchUserName = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadUsers(); _loadAsync();
} }
Future<void> _loadUsers() async { Future<void> _loadAsync() async {
final users = await DbContacts.getActiveUsers(); final users = await DbContacts.getActiveUsers();
setState(() { setState(() {
_users = users; _users = users;
_updateUsers(_users); _updateUsers(_users);
}); });
imageBytes = await widget.imageBytesFuture;
} }
Future _updateUsers(List<Contact> users) async { Future _updateUsers(List<Contact> users) async {
@ -153,11 +155,23 @@ class _ShareImageView extends State<ShareImageView> {
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
FilledButton.icon( FilledButton.icon(
icon: FaIcon(FontAwesomeIcons.solidPaperPlane), icon: imageBytes == null
? SizedBox(
height: 12,
width: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Theme.of(context).colorScheme.inversePrimary,
),
)
: FaIcon(FontAwesomeIcons.solidPaperPlane),
onPressed: () async { onPressed: () async {
if (imageBytes == null || _selectedUserIds.isEmpty) {
return;
}
sendImage( sendImage(
_selectedUserIds.toList(), _selectedUserIds.toList(),
widget.imageBytes, imageBytes!,
widget.isRealTwonly, widget.isRealTwonly,
widget.maxShowTime, widget.maxShowTime,
); );
@ -168,10 +182,14 @@ class _ShareImageView extends State<ShareImageView> {
globalUpdateOfHomeViewPageIndex(0); globalUpdateOfHomeViewPageIndex(0);
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30), EdgeInsets.symmetric(vertical: 10, horizontal: 30),
), ),
), backgroundColor: WidgetStateProperty.all<Color>(
imageBytes == null || _selectedUserIds.isEmpty
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary,
)),
label: Text( label: Text(
context.lang.shareImagedEditorSendImage, context.lang.shareImagedEditorSendImage,
style: TextStyle(fontSize: 17), style: TextStyle(fontSize: 17),

View file

@ -25,40 +25,46 @@ class ChatListEntry extends StatelessWidget {
MessageSendState state = message.getSendState(); MessageSendState state = message.getSendState();
bool isDownloading = false; bool isDownloading = false;
if (message.messageContent != null && List<int> token = [];
message.messageContent!.downloadToken != null) {
final content = message.messageContent;
if (message.messageReceived && content is MediaMessageContent) {
token = content.downloadToken;
isDownloading = context isDownloading = context
.watch<DownloadChangeProvider>() .watch<DownloadChangeProvider>()
.currentlyDownloading .currentlyDownloading
.contains(message.messageContent!.downloadToken!.toString()); .contains(token.toString());
} }
Widget child = Container(); Widget child = Container();
switch (message.messageKind) { switch (message.messageKind) {
case MessageKind.textMessage: case MessageKind.textMessage:
child = Container( if (content is TextMessageContent) {
constraints: BoxConstraints( child = Container(
maxWidth: MediaQuery.of(context).size.width * 0.8, constraints: BoxConstraints(
), maxWidth: MediaQuery.of(context).size.width * 0.8,
padding: EdgeInsets.symmetric(
vertical: 4, horizontal: 10), // Add some padding around the text
decoration: BoxDecoration(
color: right
? const Color.fromARGB(107, 124, 77, 255)
: const Color.fromARGB(
83, 68, 137, 255), // Set the background color
borderRadius: BorderRadius.circular(12.0), // Set border radius
),
child: Text(
message.messageContent!.text!,
style: TextStyle(
color: Colors.white, // Set text color for contrast
fontSize: 17,
), ),
textAlign: TextAlign.left, // Center the text padding: EdgeInsets.symmetric(
), vertical: 4,
); horizontal: 10), // Add some padding around the text
decoration: BoxDecoration(
color: right
? const Color.fromARGB(107, 124, 77, 255)
: const Color.fromARGB(
83, 68, 137, 255), // Set the background color
borderRadius: BorderRadius.circular(12.0), // Set border radius
),
child: Text(
content.text,
style: TextStyle(
color: Colors.white, // Set text color for contrast
fontSize: 17,
),
textAlign: TextAlign.left, // Center the text
),
);
}
break; break;
case MessageKind.image: case MessageKind.image:
Color color = Color color =
@ -74,7 +80,6 @@ class ChatListEntry extends StatelessWidget {
}), }),
); );
} else { } else {
List<int> token = message.messageContent!.downloadToken!;
tryDownloadMedia(token, force: true); tryDownloadMedia(token, force: true);
} }
} }

View file

@ -6,6 +6,7 @@ import 'package:twonly/src/components/message_send_state_icon.dart';
import 'package:twonly/src/components/notification_badge.dart'; import 'package:twonly/src/components/notification_badge.dart';
import 'package:twonly/src/components/user_context_menu.dart'; import 'package:twonly/src/components/user_context_menu.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart'; import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/providers/contacts_change_provider.dart';
@ -19,20 +20,6 @@ import 'package:twonly/src/views/profile_view.dart';
import 'package:twonly/src/views/chats/search_username_view.dart'; import 'package:twonly/src/views/chats/search_username_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ChatItem {
const ChatItem(
{required this.username,
required this.flames,
required this.userId,
required this.state,
required this.lastMessageInSeconds});
final String username;
final int lastMessageInSeconds;
final int flames;
final int userId;
final MessageSendState state;
}
/// Displays a list of SampleItems. /// Displays a list of SampleItems.
class ChatListView extends StatefulWidget { class ChatListView extends StatefulWidget {
const ChatListView({super.key}); const ChatListView({super.key});
@ -169,7 +156,6 @@ class UserListItem extends StatefulWidget {
} }
class _UserListItem extends State<UserListItem> { class _UserListItem extends State<UserListItem> {
int flames = 0;
int lastMessageInSeconds = 0; int lastMessageInSeconds = 0;
@override @override
@ -186,13 +172,15 @@ class _UserListItem extends State<UserListItem> {
MessageSendState state = widget.lastMessage.getSendState(); MessageSendState state = widget.lastMessage.getSendState();
bool isDownloading = false; bool isDownloading = false;
if (widget.lastMessage.messageContent != null && final content = widget.lastMessage.messageContent;
widget.lastMessage.messageContent!.downloadToken != null) { List<int> token = [];
if (widget.lastMessage.messageReceived && content is MediaMessageContent) {
token = content.downloadToken;
isDownloading = context isDownloading = context
.watch<DownloadChangeProvider>() .watch<DownloadChangeProvider>()
.currentlyDownloading .currentlyDownloading
.contains( .contains(token.toString());
widget.lastMessage.messageContent!.downloadToken!.toString());
} }
return UserContextMenu( return UserContextMenu(
@ -216,7 +204,6 @@ class _UserListItem extends State<UserListItem> {
onTap: () { onTap: () {
if (isDownloading) return; if (isDownloading) return;
if (!widget.lastMessage.isDownloaded) { if (!widget.lastMessage.isDownloaded) {
List<int> token = widget.lastMessage.messageContent!.downloadToken!;
tryDownloadMedia(token, force: true); tryDownloadMedia(token, force: true);
return; return;
} }

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/components/media_view_sizing.dart'; import 'package:twonly/src/components/media_view_sizing.dart';
import 'package:twonly/src/model/contacts_model.dart'; import 'package:twonly/src/model/contacts_model.dart';
import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/model/messages_model.dart'; import 'package:twonly/src/model/messages_model.dart';
import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api.dart';
@ -25,10 +26,13 @@ class _MediaViewerViewState extends State<MediaViewerView> {
} }
Future _initAsync() async { Future _initAsync() async {
List<int> token = widget.message.messageContent!.downloadToken!; final content = widget.message.messageContent;
_imageByte = if (content is MediaMessageContent) {
await getDownloadedMedia(token, widget.message.messageOtherId!); List<int> token = content.downloadToken;
setState(() {}); _imageByte =
await getDownloadedMedia(token, widget.message.messageOtherId!);
setState(() {});
}
} }
@override @override

View file

@ -45,6 +45,7 @@ class _SearchUsernameView extends State<SearchUsernameView> {
Message( Message(
kind: MessageKind.contactRequest, kind: MessageKind.contactRequest,
timestamp: DateTime.now(), timestamp: DateTime.now(),
content: MessageContent(),
), ),
); );
} }
@ -179,6 +180,7 @@ class _ContactsListViewState extends State<ContactsListView> {
Message( Message(
kind: MessageKind.rejectRequest, kind: MessageKind.rejectRequest,
timestamp: DateTime.now(), timestamp: DateTime.now(),
content: MessageContent(),
), ),
); );
}, },
@ -193,6 +195,7 @@ class _ContactsListViewState extends State<ContactsListView> {
Message( Message(
kind: MessageKind.acceptRequest, kind: MessageKind.acceptRequest,
timestamp: DateTime.now(), timestamp: DateTime.now(),
content: MessageContent(),
), ),
); );
}, },