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

View file

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

View file

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

View file

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

View file

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

View file

@ -221,18 +221,28 @@ Future<client.Response> handleNewMessage(int fromUserId, Uint8List body) async {
break;
default:
if (message.kind != MessageKind.textMessage &&
message.kind != MessageKind.media) {
message.kind != MessageKind.media &&
message.kind != MessageKind.storedMediaFile) {
Logger("handleServerMessages")
.shout("Got unknown MessageKind $message");
} else {
String content = jsonEncode(message.content!.toJson());
bool acknowledgeByUser = false;
DateTime? openedAt;
if (message.kind == MessageKind.storedMediaFile) {
acknowledgeByUser = true;
openedAt = DateTime.now();
}
final update = MessagesCompanion(
contactId: Value(fromUserId),
kind: Value(message.kind),
messageOtherId: Value(message.messageId),
contentJson: Value(content),
acknowledgeByServer: Value(true),
acknowledgeByUser: Value(acknowledgeByUser),
openedAt: Value(openedAt),
downloadState: Value(message.kind == MessageKind.media
? DownloadState.pending
: DownloadState.downloaded),

View file

@ -149,6 +149,7 @@ String getPushNotificationText(String key, String userName) {
"newImage": "%userName% hat dir ein Bild gesendet.",
"contactRequest": "%userName% möchte sich mir dir vernetzen.",
"acceptRequest": "%userName% ist jetzt mit dir vernetzt.",
"storedMediaFile": "%userName% hat dein Bild gespeichert."
};
} else {
pushNotificationText = {
@ -158,6 +159,7 @@ String getPushNotificationText(String key, String userName) {
"newImage": "%userName% has sent you an image.",
"contactRequest": "%userName% wants to connect 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));
}
if (message.kind == my.MessageKind.storedMediaFile) {
msg =
getPushNotificationText("storedMediaFile", getContactDisplayName(user));
}
if (msg == "") {
Logger("localPushNotificationNewMessage")
.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/send_next_media_to.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 'dart:async';
import 'package:flutter/services.dart';
@ -35,7 +36,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
bool _imageSaved = false;
bool _imageSaving = false;
bool _isRealTwonly = false;
int _maxShowTime = 18;
int maxShowTime = 999999;
String? sendNextMediaToUserName;
ImageItem currentImage = ImageItem();
@ -44,9 +45,20 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
@override
void initState() {
super.initState();
initAsync();
loadImage(widget.imageBytes);
}
void initAsync() async {
final user = await getUser();
if (user == null) return;
if (user.defaultShowTime != null) {
setState(() {
maxShowTime = user.defaultShowTime!;
});
}
}
@override
void dispose() {
layers.clear();
@ -114,24 +126,25 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
),
const SizedBox(height: 8),
NotificationBadge(
count: _maxShowTime == 999999 ? "" : _maxShowTime.toString(),
count: maxShowTime == 999999 ? "" : maxShowTime.toString(),
// count: "",
child: ActionButton(
FontAwesomeIcons.stopwatch,
tooltipText: context.lang.protectAsARealTwonly,
// disable: _isRealTwonly,
onPressed: () async {
if (_maxShowTime == 999999) {
_maxShowTime = 4;
} else if (_maxShowTime >= 22) {
_maxShowTime = 999999;
if (maxShowTime == 999999) {
maxShowTime = 4;
} else if (maxShowTime >= 22) {
maxShowTime = 999999;
} else {
_maxShowTime = _maxShowTime + 4;
maxShowTime = maxShowTime + 8;
}
setState(() {});
// _maxShowTime =
// _isRealTwonly = !_isRealTwonly;
var user = await getUser();
if (user != null) {
user.defaultShowTime = maxShowTime;
updateUser(user);
}
},
),
),
@ -145,7 +158,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
onPressed: () async {
_isRealTwonly = !_isRealTwonly;
if (_isRealTwonly) {
_maxShowTime = 12;
maxShowTime = 12;
}
setState(() {});
},
@ -369,7 +382,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
builder: (context) => ShareImageView(
imageBytesFuture: imageBytes,
isRealTwonly: _isRealTwonly,
maxShowTime: _maxShowTime,
maxShowTime: maxShowTime,
),
),
);
@ -387,7 +400,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
[sendNextMediaToUserId],
imageBytes!,
_isRealTwonly,
_maxShowTime,
maxShowTime,
);
Navigator.popUntil(context, (route) => route.isFirst);
globalUpdateOfHomeViewPageIndex(1);
@ -402,7 +415,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
builder: (context) => ShareImageView(
imageBytesFuture: imageBytes,
isRealTwonly: _isRealTwonly,
maxShowTime: _maxShowTime,
maxShowTime: maxShowTime,
),
),
);

View file

@ -74,7 +74,9 @@ class ChatListEntry extends StatelessWidget {
}
} else if (content is MediaMessageContent && !content.isVideo) {
Color color = getMessageColorFromType(
content, Theme.of(context).colorScheme.primary);
content,
Theme.of(context).colorScheme.primary,
);
child = GestureDetector(
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(
@ -181,9 +203,8 @@ class _ChatItemDetailsViewState extends State<ChatItemDetailsView> {
notifyContactAboutOpeningMessage(widget.userid, msg.messageOtherId!);
}
}
if (updated) {
twonlyDatabase.messagesDao.openedAllTextMessages(widget.userid);
} else {
twonlyDatabase.messagesDao.openedAllNonMediaMessages(widget.userid);
if (!updated) {
// The stream should be get an update, so only update the UI when all are opened
setState(() {
messages = msgs;

View file

@ -45,6 +45,9 @@ class _MediaViewerViewState extends State<MediaViewerView> {
bool isRealTwonly = false;
bool isDownloading = false;
bool imageSaved = false;
bool imageSaving = false;
List<Message> allMediaFiles = [];
late StreamSubscription<List<Message>> _subscription;
@ -95,10 +98,11 @@ class _MediaViewerViewState extends State<MediaViewerView> {
MediaMessageContent.fromJson(jsonDecode(current.contentJson!));
setState(() {
// reset current image values
imageBytes = null;
canBeSeenUntil = null;
maxShowTime = 999999;
imageSaving = false;
imageSaved = false;
progress = 0;
isDownloading = false;
isRealTwonly = false;
@ -281,7 +285,7 @@ class _MediaViewerViewState extends State<MediaViewerView> {
),
AnimatedPositioned(
duration: Duration(milliseconds: 200), // Animation duration
bottom: showShortReactions ? 130 : 90,
bottom: showShortReactions ? 100 : 90,
left: showShortReactions ? 0 : 150,
right: showShortReactions ? 0 : 150,
curve: Curves.linearToEaseOut,
@ -341,32 +345,65 @@ class _MediaViewerViewState extends State<MediaViewerView> {
),
if (imageBytes != null)
Positioned(
bottom: 30,
bottom: 0,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
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: 30),
if (maxShowTime == 999999)
OutlinedButton(
style: OutlinedButton.styleFrom(
iconColor: imageSaved
? Theme.of(context).colorScheme.outline
: Theme.of(context).colorScheme.primary,
foregroundColor: imageSaved
? Theme.of(context).colorScheme.outline
: Theme.of(context).colorScheme.primary,
),
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),
IconButton(
icon: SizedBox(
width: 40,
height: 40,
width: 30,
height: 30,
child: GridView.count(
crossAxisCount: 2,
children: List.generate(
@ -391,14 +428,10 @@ class _MediaViewerViewState extends State<MediaViewerView> {
showShortReactions = !showShortReactions;
selectedShortReaction = -1;
});
// context.read<SendNextMediaTo>().updateSendNextMediaTo(
// widget.otherUser.userId.toInt());
// globalUpdateOfHomeViewPageIndex(0);
// Navigator.popUntil(context, (route) => route.isFirst);
},
style: ButtonStyle(
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(
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),
),
),
),