This commit is contained in:
otsmr 2025-03-22 19:14:25 +01:00
parent d9741ee877
commit 06feb2d18c
10 changed files with 178 additions and 52 deletions

View file

@ -85,6 +85,8 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
for (final message in widget.messages) { for (final message in widget.messages) {
if (icons.length == 2) break; if (icons.length == 2) break;
if (kindsAlreadyShown.contains(message.kind)) continue;
kindsAlreadyShown.add(message.kind);
MessageSendState state = messageSendStateFromMessage(message); MessageSendState state = messageSendStateFromMessage(message);
late Color color; late Color color;
@ -94,14 +96,13 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
getMessageColorFromType(TextMessageContent(text: ""), twonlyColor); getMessageColorFromType(TextMessageContent(text: ""), twonlyColor);
} else { } else {
MessageContent? content = MessageContent.fromJson( MessageContent? content = MessageContent.fromJson(
message.kind, jsonDecode(message.contentJson!)); message.kind,
jsonDecode(message.contentJson!),
);
if (content == null) continue; if (content == null) continue;
color = getMessageColorFromType(content, twonlyColor); color = getMessageColorFromType(content, twonlyColor);
} }
if (kindsAlreadyShown.contains(message.kind)) continue;
kindsAlreadyShown.add(message.kind);
Widget icon = Placeholder(); Widget icon = Placeholder();
switch (state) { switch (state) {
@ -140,6 +141,11 @@ class _MessageSendStateIconState extends State<MessageSendStateIcon> {
break; break;
} }
if (message.kind == MessageKind.storedMediaFile) {
icon = FaIcon(FontAwesomeIcons.floppyDisk, size: 12, color: color);
text = "Stored in gallery";
}
icons.add(icon); icons.add(icon);
} }

View file

@ -76,13 +76,13 @@ class MessagesDao extends DatabaseAccessor<TwonlyDatabase>
.get(); .get();
} }
Future openedAllTextMessages(int contactId) { Future openedAllNonMediaMessages(int contactId) {
final updates = MessagesCompanion(openedAt: Value(DateTime.now())); final updates = MessagesCompanion(openedAt: Value(DateTime.now()));
return (update(messages) return (update(messages)
..where((t) => ..where((t) =>
t.contactId.equals(contactId) & t.contactId.equals(contactId) &
t.openedAt.isNull() & t.openedAt.isNull() &
t.kind.equals(MessageKind.textMessage.name))) t.kind.equals(MessageKind.media.name).not()))
.write(updates); .write(updates);
} }

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
enum MessageKind { enum MessageKind {
textMessage, textMessage,
storedMediaFile,
media, media,
contactRequest, contactRequest,
profileChange, profileChange,
@ -20,7 +21,7 @@ Map<String, Color> messageKindColors = {
Color getMessageColorFromType(MessageContent content, Color primary) { Color getMessageColorFromType(MessageContent content, Color primary) {
Color color; Color color;
if (content is TextMessageContent) { if (content is TextMessageContent || content is StoredMediaFileContent) {
color = messageKindColors["text"]!; color = messageKindColors["text"]!;
} else { } else {
if (content is MediaMessageContent) { if (content is MediaMessageContent) {
@ -96,6 +97,8 @@ class MessageContent {
return TextMessageContent.fromJson(json); return TextMessageContent.fromJson(json);
case MessageKind.profileChange: case MessageKind.profileChange:
return ProfileContent.fromJson(json); return ProfileContent.fromJson(json);
case MessageKind.storedMediaFile:
return StoredMediaFileContent.fromJson(json);
default: default:
return null; return null;
} }
@ -172,6 +175,20 @@ class TextMessageContent extends MessageContent {
} }
} }
class StoredMediaFileContent extends MessageContent {
int messageId;
StoredMediaFileContent({required this.messageId});
static StoredMediaFileContent fromJson(Map json) {
return StoredMediaFileContent(messageId: json['messageId']);
}
@override
Map toJson() {
return {'messageId': messageId};
}
}
class ProfileContent extends MessageContent { class ProfileContent extends MessageContent {
String avatarSvg; String avatarSvg;
String displayName; String displayName;

View file

@ -15,6 +15,7 @@ class UserData {
String? avatarSvg; String? avatarSvg;
String? avatarJson; String? avatarJson;
int? avatarCounter; int? avatarCounter;
int? defaultShowTime;
final int userId; final int userId;

View file

@ -13,7 +13,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
) )
..avatarSvg = json['avatarSvg'] as String? ..avatarSvg = json['avatarSvg'] as String?
..avatarJson = json['avatarJson'] as String? ..avatarJson = json['avatarJson'] as String?
..avatarCounter = (json['avatarCounter'] as num?)?.toInt(); ..avatarCounter = (json['avatarCounter'] as num?)?.toInt()
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt();
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'username': instance.username, 'username': instance.username,
@ -21,5 +22,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'avatarSvg': instance.avatarSvg, 'avatarSvg': instance.avatarSvg,
'avatarJson': instance.avatarJson, 'avatarJson': instance.avatarJson,
'avatarCounter': instance.avatarCounter, 'avatarCounter': instance.avatarCounter,
'defaultShowTime': instance.defaultShowTime,
'userId': instance.userId, 'userId': instance.userId,
}; };

View file

@ -221,18 +221,28 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
break; break;
default: default:
if (message.kind != MessageKind.textMessage && if (message.kind != MessageKind.textMessage &&
message.kind != MessageKind.media) { message.kind != MessageKind.media &&
message.kind != MessageKind.storedMediaFile) {
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());
bool acknowledgeByUser = false;
DateTime? openedAt;
if (message.kind == MessageKind.storedMediaFile) {
acknowledgeByUser = true;
openedAt = DateTime.now();
}
final update = MessagesCompanion( final update = MessagesCompanion(
contactId: Value(fromUserId), contactId: Value(fromUserId),
kind: Value(message.kind), kind: Value(message.kind),
messageOtherId: Value(message.messageId), messageOtherId: Value(message.messageId),
contentJson: Value(content), contentJson: Value(content),
acknowledgeByServer: Value(true), acknowledgeByServer: Value(true),
acknowledgeByUser: Value(acknowledgeByUser),
openedAt: Value(openedAt),
downloadState: Value(message.kind == MessageKind.media downloadState: Value(message.kind == MessageKind.media
? DownloadState.pending ? DownloadState.pending
: DownloadState.downloaded), : DownloadState.downloaded),

View file

@ -149,6 +149,7 @@ String getPushNotificationText(String key, String userName) {
"newImage": "%userName% hat dir ein Bild gesendet.", "newImage": "%userName% hat dir ein Bild gesendet.",
"contactRequest": "%userName% möchte sich mir dir vernetzen.", "contactRequest": "%userName% möchte sich mir dir vernetzen.",
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.", "acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
"storedMediaFile": "%userName% hat dein Bild gespeichert."
}; };
} else { } else {
pushNotificationText = { pushNotificationText = {
@ -158,6 +159,7 @@ String getPushNotificationText(String key, String userName) {
"newImage": "%userName% has sent you an image.", "newImage": "%userName% has sent you an image.",
"contactRequest": "%userName% wants to connect with you.", "contactRequest": "%userName% wants to connect with you.",
"acceptRequest": "%userName% is now connected with you.", "acceptRequest": "%userName% is now connected with you.",
"storedMediaFile": "%userName% has stored your image."
}; };
} }
@ -199,6 +201,11 @@ Future localPushNotificationNewMessage(
msg = getPushNotificationText("acceptRequest", getContactDisplayName(user)); msg = getPushNotificationText("acceptRequest", getContactDisplayName(user));
} }
if (message.kind == my.MessageKind.storedMediaFile) {
msg =
getPushNotificationText("storedMediaFile", getContactDisplayName(user));
}
if (msg == "") { if (msg == "") {
Logger("localPushNotificationNewMessage") Logger("localPushNotificationNewMessage")
.shout("No push notification type defined!"); .shout("No push notification type defined!");

View file

@ -10,6 +10,7 @@ import 'package:twonly/src/database/twonly_database.dart';
import 'package:twonly/src/providers/api/media.dart'; import 'package:twonly/src/providers/api/media.dart';
import 'package:twonly/src/providers/send_next_media_to.dart'; import 'package:twonly/src/providers/send_next_media_to.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/camera_to_share/share_image_view.dart'; import 'package:twonly/src/views/camera_to_share/share_image_view.dart';
import 'dart:async'; import 'dart:async';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -35,7 +36,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
bool _imageSaved = false; bool _imageSaved = false;
bool _imageSaving = false; bool _imageSaving = false;
bool _isRealTwonly = false; bool _isRealTwonly = false;
int _maxShowTime = 18; int maxShowTime = 999999;
String? sendNextMediaToUserName; String? sendNextMediaToUserName;
ImageItem currentImage = ImageItem(); ImageItem currentImage = ImageItem();
@ -44,9 +45,20 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initAsync();
loadImage(widget.imageBytes); loadImage(widget.imageBytes);
} }
void initAsync() async {
final user = await getUser();
if (user == null) return;
if (user.defaultShowTime != null) {
setState(() {
maxShowTime = user.defaultShowTime!;
});
}
}
@override @override
void dispose() { void dispose() {
layers.clear(); layers.clear();
@ -114,24 +126,25 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
NotificationBadge( NotificationBadge(
count: _maxShowTime == 999999 ? "" : _maxShowTime.toString(), count: maxShowTime == 999999 ? "" : maxShowTime.toString(),
// count: "", // count: "",
child: ActionButton( child: ActionButton(
FontAwesomeIcons.stopwatch, FontAwesomeIcons.stopwatch,
tooltipText: context.lang.protectAsARealTwonly, tooltipText: context.lang.protectAsARealTwonly,
// disable: _isRealTwonly,
onPressed: () async { onPressed: () async {
if (_maxShowTime == 999999) { if (maxShowTime == 999999) {
_maxShowTime = 4; maxShowTime = 4;
} else if (_maxShowTime >= 22) { } else if (maxShowTime >= 22) {
_maxShowTime = 999999; maxShowTime = 999999;
} else { } else {
_maxShowTime = _maxShowTime + 4; maxShowTime = maxShowTime + 8;
} }
setState(() {}); setState(() {});
var user = await getUser();
// _maxShowTime = if (user != null) {
// _isRealTwonly = !_isRealTwonly; user.defaultShowTime = maxShowTime;
updateUser(user);
}
}, },
), ),
), ),
@ -145,7 +158,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
onPressed: () async { onPressed: () async {
_isRealTwonly = !_isRealTwonly; _isRealTwonly = !_isRealTwonly;
if (_isRealTwonly) { if (_isRealTwonly) {
_maxShowTime = 12; maxShowTime = 12;
} }
setState(() {}); setState(() {});
}, },
@ -369,7 +382,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
builder: (context) => ShareImageView( builder: (context) => ShareImageView(
imageBytesFuture: imageBytes, imageBytesFuture: imageBytes,
isRealTwonly: _isRealTwonly, isRealTwonly: _isRealTwonly,
maxShowTime: _maxShowTime, maxShowTime: maxShowTime,
), ),
), ),
); );
@ -387,7 +400,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
[sendNextMediaToUserId], [sendNextMediaToUserId],
imageBytes!, imageBytes!,
_isRealTwonly, _isRealTwonly,
_maxShowTime, maxShowTime,
); );
Navigator.popUntil(context, (route) => route.isFirst); Navigator.popUntil(context, (route) => route.isFirst);
globalUpdateOfHomeViewPageIndex(1); globalUpdateOfHomeViewPageIndex(1);
@ -402,7 +415,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
builder: (context) => ShareImageView( builder: (context) => ShareImageView(
imageBytesFuture: imageBytes, imageBytesFuture: imageBytes,
isRealTwonly: _isRealTwonly, isRealTwonly: _isRealTwonly,
maxShowTime: _maxShowTime, maxShowTime: maxShowTime,
), ),
), ),
); );

View file

@ -74,7 +74,9 @@ class ChatListEntry extends StatelessWidget {
} }
} else if (content is MediaMessageContent && !content.isVideo) { } else if (content is MediaMessageContent && !content.isVideo) {
Color color = getMessageColorFromType( Color color = getMessageColorFromType(
content, Theme.of(context).colorScheme.primary); content,
Theme.of(context).colorScheme.primary,
);
child = GestureDetector( child = GestureDetector(
onTap: () { onTap: () {
@ -112,6 +114,26 @@ class ChatListEntry extends StatelessWidget {
), ),
), ),
); );
} else if (message.kind == MessageKind.storedMediaFile) {
child = Container(
padding: EdgeInsets.all(5),
width: 150,
decoration: BoxDecoration(
border: Border.all(
color: messageKindColors["text"]!,
width: 1.0,
),
borderRadius: BorderRadius.circular(12.0),
),
child: Align(
alignment: Alignment.centerRight,
child: MessageSendStateIcon(
[message],
mainAxisAlignment:
right ? MainAxisAlignment.center : MainAxisAlignment.center,
),
),
);
} }
return Align( return Align(
@ -181,9 +203,8 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
notifyContactAboutOpeningMessage(widget.userid, msg.messageOtherId!); notifyContactAboutOpeningMessage(widget.userid, msg.messageOtherId!);
} }
} }
if (updated) { twonlyDatabase.messagesDao.openedAllNonMediaMessages(widget.userid);
twonlyDatabase.messagesDao.openedAllTextMessages(widget.userid); if (!updated) {
} else {
// The stream should be get an update, so only update the UI when all are opened // The stream should be get an update, so only update the UI when all are opened
setState(() { setState(() {
messages = msgs; messages = msgs;

View file

@ -45,6 +45,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
bool isRealTwonly = false; bool isRealTwonly = false;
bool isDownloading = false; bool isDownloading = false;
bool imageSaved = false;
bool imageSaving = false;
List<Message> allMediaFiles = []; List<Message> allMediaFiles = [];
late StreamSubscription<List<Message>> _subscription; late StreamSubscription<List<Message>> _subscription;
@ -95,10 +98,11 @@ class _MediaViewerViewState extends State<MediaViewerView> {
MediaMessageContent.fromJson(jsonDecode(current.contentJson!)); MediaMessageContent.fromJson(jsonDecode(current.contentJson!));
setState(() { setState(() {
// reset current image values
imageBytes = null; imageBytes = null;
canBeSeenUntil = null; canBeSeenUntil = null;
maxShowTime = 999999; maxShowTime = 999999;
imageSaving = false;
imageSaved = false;
progress = 0; progress = 0;
isDownloading = false; isDownloading = false;
isRealTwonly = false; isRealTwonly = false;
@ -281,7 +285,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
AnimatedPositioned( AnimatedPositioned(
duration: Duration(milliseconds: 200), // Animation duration duration: Duration(milliseconds: 200), // Animation duration
bottom: showShortReactions ? 130 : 90, bottom: showShortReactions ? 100 : 90,
left: showShortReactions ? 0 : 150, left: showShortReactions ? 0 : 150,
right: showShortReactions ? 0 : 150, right: showShortReactions ? 0 : 150,
curve: Curves.linearToEaseOut, curve: Curves.linearToEaseOut,
@ -341,32 +345,65 @@ class _MediaViewerViewState extends State<MediaViewerView> {
), ),
if (imageBytes != null) if (imageBytes != null)
Positioned( Positioned(
bottom: 30, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
IconButton.outlined( if (maxShowTime == 999999)
icon: FaIcon(FontAwesomeIcons.camera), OutlinedButton(
onPressed: () async { style: OutlinedButton.styleFrom(
context iconColor: imageSaved
.read<SendNextMediaTo>() ? Theme.of(context).colorScheme.outline
.updateSendNextMediaTo(widget.userId.toInt()); : Theme.of(context).colorScheme.primary,
globalUpdateOfHomeViewPageIndex(0); foregroundColor: imageSaved
Navigator.popUntil(context, (route) => route.isFirst); ? Theme.of(context).colorScheme.outline
}, : Theme.of(context).colorScheme.primary,
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30),
), ),
onPressed: () async {
setState(() {
imageSaving = true;
});
encryptAndSendMessage(
null,
widget.userId,
MessageJson(
kind: MessageKind.storedMediaFile,
messageId: allMediaFiles.first.messageId,
content: StoredMediaFileContent(
messageId: allMediaFiles.first.messageId,
),
timestamp: DateTime.now(),
),
);
final res = await saveImageToGallery(imageBytes!);
if (res == null) {
setState(() {
imageSaving = false;
imageSaved = true;
});
}
},
child: Row(
children: [
imageSaving
? SizedBox(
width: 10,
height: 10,
child: CircularProgressIndicator(
strokeWidth: 1))
: imageSaved
? Icon(Icons.check)
: FaIcon(FontAwesomeIcons.floppyDisk),
],
), ),
), ),
SizedBox(width: 10), SizedBox(width: 10),
IconButton( IconButton(
icon: SizedBox( icon: SizedBox(
width: 40, width: 30,
height: 40, height: 30,
child: GridView.count( child: GridView.count(
crossAxisCount: 2, crossAxisCount: 2,
children: List.generate( children: List.generate(
@ -391,14 +428,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
showShortReactions = !showShortReactions; showShortReactions = !showShortReactions;
selectedShortReaction = -1; selectedShortReaction = -1;
}); });
// context.read<SendNextMediaTo>().updateSendNextMediaTo(
// widget.otherUser.userId.toInt());
// globalUpdateOfHomeViewPageIndex(0);
// Navigator.popUntil(context, (route) => route.isFirst);
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30), EdgeInsets.symmetric(vertical: 10, horizontal: 20),
), ),
), ),
), ),
@ -416,7 +449,23 @@ class _MediaViewerViewState extends State<MediaViewerView> {
}, },
style: ButtonStyle( style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>( padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 30), EdgeInsets.symmetric(vertical: 10, horizontal: 20),
),
),
),
SizedBox(width: 10),
IconButton.outlined(
icon: FaIcon(FontAwesomeIcons.camera),
onPressed: () async {
context
.read<SendNextMediaTo>()
.updateSendNextMediaTo(widget.userId.toInt());
globalUpdateOfHomeViewPageIndex(0);
Navigator.popUntil(context, (route) => route.isFirst);
},
style: ButtonStyle(
padding: WidgetStateProperty.all<EdgeInsets>(
EdgeInsets.symmetric(vertical: 10, horizontal: 20),
), ),
), ),
), ),