Compare commits
2 Commits
db86136aa8
...
ab63bc44a6
Author | SHA1 | Date | |
---|---|---|---|
ab63bc44a6 | |||
ef15f15458 |
@ -260,6 +260,8 @@ files:
|
||||
quotedMessage:
|
||||
type: Message?
|
||||
deserialise: true
|
||||
editSid: String?
|
||||
editId: int?
|
||||
- name: SendFilesCommand
|
||||
extends: BackgroundCommand
|
||||
implements:
|
||||
|
@ -52,6 +52,7 @@ Future<void> createDatabase(Database db, int version) async {
|
||||
isUploading INTEGER NOT NULL,
|
||||
mediaSize INTEGER,
|
||||
isRetracted INTEGER,
|
||||
isEdited INTEGER NOT NULL,
|
||||
CONSTRAINT fk_quote FOREIGN KEY (quote_id) REFERENCES $messagesTable (id)
|
||||
)''',
|
||||
);
|
||||
|
@ -12,6 +12,7 @@ import 'package:moxxyv2/service/database/migrations/0000_conversations.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0000_conversations2.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0000_conversations3.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0000_language.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0000_lmc.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0000_retraction.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0000_retraction_conversation.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0000_shared_media.dart';
|
||||
@ -61,7 +62,7 @@ class DatabaseService {
|
||||
_db = await openDatabase(
|
||||
dbPath,
|
||||
password: key,
|
||||
version: 9,
|
||||
version: 10,
|
||||
onCreate: createDatabase,
|
||||
onConfigure: (db) async {
|
||||
// In order to do schema changes during database upgrades, we disable foreign
|
||||
@ -106,6 +107,10 @@ class DatabaseService {
|
||||
_log.finest('Running migration for database version 9');
|
||||
await upgradeFromV8ToV9(db);
|
||||
}
|
||||
if (oldVersion < 10) {
|
||||
_log.finest('Running migration for database version 10');
|
||||
await upgradeFromV9ToV10(db);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@ -451,6 +456,7 @@ class DatabaseService {
|
||||
Object? sid = notSpecified,
|
||||
bool? isRetracted,
|
||||
Object? thumbnailData = notSpecified,
|
||||
bool? isEdited,
|
||||
}) async {
|
||||
final md = (await _db.query(
|
||||
'Messages',
|
||||
@ -460,6 +466,9 @@ class DatabaseService {
|
||||
)).first;
|
||||
final m = Map<String, dynamic>.from(md);
|
||||
|
||||
if (body != notSpecified) {
|
||||
m['body'] = body as String?;
|
||||
}
|
||||
if (mediaUrl != notSpecified) {
|
||||
m['mediaUrl'] = mediaUrl as String?;
|
||||
}
|
||||
@ -526,6 +535,9 @@ class DatabaseService {
|
||||
if (thumbnailData != notSpecified) {
|
||||
m['thumbnailData'] = thumbnailData as String?;
|
||||
}
|
||||
if (isEdited != null) {
|
||||
m['isEdited'] = boolToInt(isEdited);
|
||||
}
|
||||
|
||||
await _db.update(
|
||||
'Messages',
|
||||
|
11
lib/service/database/migrations/0000_lmc.dart
Normal file
11
lib/service/database/migrations/0000_lmc.dart
Normal file
@ -0,0 +1,11 @@
|
||||
import 'package:moxxyv2/service/database/constants.dart';
|
||||
import 'package:moxxyv2/service/database/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/preference.dart';
|
||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||
|
||||
Future<void> upgradeFromV9ToV10(Database db) async {
|
||||
// Mark all messages as not edited
|
||||
await db.execute(
|
||||
'ALTER TABLE $messagesTable ADD COLUMN isEdited INTEGER NOT NULL DEFAULT ${boolToInt(false)};',
|
||||
);
|
||||
}
|
@ -247,7 +247,23 @@ Future<void> performSetOpenConversation(SetOpenConversationCommand command, { dy
|
||||
}
|
||||
|
||||
Future<void> performSendMessage(SendMessageCommand command, { dynamic extra }) async {
|
||||
await GetIt.I.get<XmppService>().sendMessage(
|
||||
final xs = GetIt.I.get<XmppService>();
|
||||
if (command.editSid != null && command.editId != null) {
|
||||
assert(command.recipients.length == 1, 'Edits must not be sent to multiple recipients');
|
||||
|
||||
await xs.sendMessageCorrection(
|
||||
command.editId!,
|
||||
command.body,
|
||||
command.editSid!,
|
||||
command.recipients.first,
|
||||
command.chatState.isNotEmpty
|
||||
? chatStateFromString(command.chatState)
|
||||
: null,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await xs.sendMessage(
|
||||
body: command.body,
|
||||
recipients: command.recipients,
|
||||
chatState: command.chatState.isNotEmpty
|
||||
|
@ -151,6 +151,7 @@ class MessageService {
|
||||
Object? sid = notSpecified,
|
||||
Object? thumbnailData = notSpecified,
|
||||
bool? isRetracted,
|
||||
bool? isEdited,
|
||||
}) async {
|
||||
final newMessage = await GetIt.I.get<DatabaseService>().updateMessage(
|
||||
id,
|
||||
@ -177,6 +178,7 @@ class MessageService {
|
||||
isRetracted: isRetracted,
|
||||
isMedia: isMedia,
|
||||
thumbnailData: thumbnailData,
|
||||
isEdited: isEdited,
|
||||
);
|
||||
|
||||
if (_messageCache.containsKey(newMessage.conversationJid)) {
|
||||
|
@ -124,6 +124,49 @@ class XmppService {
|
||||
|
||||
/// Returns the JID of the chat that is currently opened. Null, if none is open.
|
||||
String? getCurrentlyOpenedChatJid() => _currentlyOpenedChatJid;
|
||||
|
||||
/// Sends a message correction to [recipient] regarding the message with stanza id
|
||||
/// [oldId]. The old message's body gets corrected to [newBody]. [id] is the message's
|
||||
/// database id. [chatState] can be optionally specified to also include a chat state
|
||||
/// in the message.
|
||||
///
|
||||
/// This function handles updating the message and optionally the corresponding
|
||||
/// conversation.
|
||||
Future<void> sendMessageCorrection(int id, String newBody, String oldId, String recipient, ChatState? chatState) async {
|
||||
final ms = GetIt.I.get<MessageService>();
|
||||
final cs = GetIt.I.get<ConversationService>();
|
||||
final conn = GetIt.I.get<XmppConnection>();
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
|
||||
// Update the database
|
||||
final msg = await ms.updateMessage(
|
||||
id,
|
||||
isEdited: true,
|
||||
body: newBody,
|
||||
);
|
||||
sendEvent(MessageUpdatedEvent(message: msg));
|
||||
|
||||
final conv = await cs.getConversationByJid(msg.conversationJid);
|
||||
if (conv != null && conv.lastMessage?.id == id) {
|
||||
final newConv = await cs.updateConversation(
|
||||
conv.id,
|
||||
lastChangeTimestamp: timestamp,
|
||||
lastMessage: msg,
|
||||
);
|
||||
cs.setConversation(newConv);
|
||||
sendEvent(ConversationUpdatedEvent(conversation: newConv));
|
||||
}
|
||||
|
||||
// Send the correction
|
||||
conn.getManagerById<MessageManager>(messageManager)!.sendMessage(
|
||||
MessageDetails(
|
||||
to: recipient,
|
||||
body: newBody,
|
||||
lastMessageCorrectionId: oldId,
|
||||
chatState: chatState,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends a message to JIDs in [recipients] with the body of [body].
|
||||
Future<void> sendMessage({
|
||||
|
@ -54,6 +54,7 @@ class Message with _$Message {
|
||||
@Default(false) bool displayed,
|
||||
@Default(false) bool acked,
|
||||
@Default(false) bool isRetracted,
|
||||
@Default(false) bool isEdited,
|
||||
String? originId,
|
||||
Message? quotes,
|
||||
String? filename,
|
||||
@ -82,6 +83,7 @@ class Message with _$Message {
|
||||
'isDownloading': intToBool(json['isDownloading']! as int),
|
||||
'isUploading': intToBool(json['isUploading']! as int),
|
||||
'isRetracted': intToBool(json['isRetracted']! as int),
|
||||
'isEdited': intToBool(json['isEdited']! as int),
|
||||
}).copyWith(quotes: quotes);
|
||||
}
|
||||
|
||||
@ -105,6 +107,7 @@ class Message with _$Message {
|
||||
'isDownloading': boolToInt(isDownloading),
|
||||
'isUploading': boolToInt(isUploading),
|
||||
'isRetracted': boolToInt(isRetracted),
|
||||
'isEdited': boolToInt(isEdited),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,8 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
on<OwnJidReceivedEvent>(_onOwnJidReceived);
|
||||
on<OmemoSetEvent>(_onOmemoSet);
|
||||
on<MessageRetractedEvent>(_onMessageRetracted);
|
||||
on<MessageEditSelectedEvent>(_onMessageEditSelected);
|
||||
on<MessageEditCancelledEvent>(_onMessageEditCancelled);
|
||||
}
|
||||
/// The current chat state with the conversation partner
|
||||
ChatState _currentChatState;
|
||||
@ -115,6 +117,11 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
state.copyWith(
|
||||
conversation: conversation,
|
||||
quotedMessage: null,
|
||||
messageEditing: false,
|
||||
messageEditingOriginalBody: '',
|
||||
messageEditingId: null,
|
||||
messageEditingSid: null,
|
||||
showSendButton: false,
|
||||
),
|
||||
);
|
||||
|
||||
@ -174,24 +181,33 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
_stopComposeTimer();
|
||||
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
final result = await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
final r = await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
SendMessageCommand(
|
||||
recipients: [state.conversation!.jid],
|
||||
body: state.messageText,
|
||||
quotedMessage: state.quotedMessage,
|
||||
chatState: chatStateToString(ChatState.active),
|
||||
),
|
||||
) as events.MessageAddedEvent;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: List<Message>.from(<Message>[ ...state.messages, result.message ]),
|
||||
messageText: '',
|
||||
quotedMessage: null,
|
||||
showSendButton: false,
|
||||
emojiPickerVisible: false,
|
||||
editId: state.messageEditingId,
|
||||
editSid: state.messageEditingSid,
|
||||
),
|
||||
);
|
||||
|
||||
if (!state.messageEditing) {
|
||||
final result = r! as events.MessageAddedEvent;
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: List<Message>.from(<Message>[ ...state.messages, result.message ]),
|
||||
messageText: '',
|
||||
quotedMessage: null,
|
||||
showSendButton: false,
|
||||
emojiPickerVisible: false,
|
||||
messageEditing: false,
|
||||
messageEditingOriginalBody: '',
|
||||
messageEditingId: null,
|
||||
messageEditingSid: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onMessageQuoted(MessageQuotedEvent event, Emitter<ConversationState> emit) async {
|
||||
@ -352,4 +368,31 @@ class ConversationBloc extends Bloc<ConversationEvent, ConversationState> {
|
||||
awaitable: false,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onMessageEditSelected(MessageEditSelectedEvent event, Emitter<ConversationState> emit) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
messageText: event.message.body,
|
||||
quotedMessage: event.message.quotes,
|
||||
messageEditing: true,
|
||||
messageEditingOriginalBody: event.message.body,
|
||||
messageEditingId: event.message.id,
|
||||
messageEditingSid: event.message.sid,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onMessageEditCancelled(MessageEditCancelledEvent event, Emitter<ConversationState> emit) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
messageText: '',
|
||||
quotedMessage: null,
|
||||
messageEditing: false,
|
||||
messageEditingOriginalBody: '',
|
||||
messageEditingId: null,
|
||||
messageEditingSid: null,
|
||||
showSendButton: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -120,3 +120,14 @@ class MessageRetractedEvent extends ConversationEvent {
|
||||
MessageRetractedEvent(this.id);
|
||||
final String id;
|
||||
}
|
||||
|
||||
/// Triggered when a message has been selected for editing
|
||||
class MessageEditSelectedEvent extends ConversationEvent {
|
||||
MessageEditSelectedEvent(this.message);
|
||||
final Message message;
|
||||
}
|
||||
|
||||
/// Triggered when a message edit has been cancelled
|
||||
class MessageEditCancelledEvent extends ConversationEvent {
|
||||
MessageEditCancelledEvent();
|
||||
}
|
||||
|
@ -12,5 +12,9 @@ class ConversationState with _$ConversationState {
|
||||
@Default(null) Conversation? conversation,
|
||||
@Default('') String backgroundPath,
|
||||
@Default(false) bool emojiPickerVisible,
|
||||
@Default(false) bool messageEditing,
|
||||
@Default('') String messageEditingOriginalBody,
|
||||
@Default(null) String? messageEditingSid,
|
||||
@Default(null) int? messageEditingId,
|
||||
}) = _ConversationState;
|
||||
}
|
||||
|
@ -24,6 +24,16 @@ class ConversationBottomRow extends StatelessWidget {
|
||||
|
||||
return Colors.black;
|
||||
}
|
||||
|
||||
bool _shouldCancelEdit(ConversationState state) {
|
||||
return state.messageEditing && controller.text == state.messageEditingOriginalBody;
|
||||
}
|
||||
|
||||
IconData _getSpeeddialIcon(ConversationState state) {
|
||||
if (_shouldCancelEdit(state)) return Icons.clear;
|
||||
|
||||
return state.showSendButton ? Icons.send : Icons.add;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -34,7 +44,7 @@ class ConversationBottomRow extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: BlocBuilder<ConversationBloc, ConversationState>(
|
||||
buildWhen: (prev, next) => prev.showSendButton != next.showSendButton || prev.quotedMessage != next.quotedMessage || prev.emojiPickerVisible != next.emojiPickerVisible || prev.messageText != next.messageText,
|
||||
buildWhen: (prev, next) => prev.showSendButton != next.showSendButton || prev.quotedMessage != next.quotedMessage || prev.emojiPickerVisible != next.emojiPickerVisible || prev.messageText != next.messageText || prev.messageEditing != next.messageEditing || prev.messageEditingOriginalBody != next.messageEditingOriginalBody,
|
||||
builder: (context, state) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@ -131,13 +141,19 @@ class ConversationBottomRow extends StatelessWidget {
|
||||
width: 45,
|
||||
child: FittedBox(
|
||||
child: SpeedDial(
|
||||
icon: state.showSendButton ? Icons.send : Icons.add,
|
||||
icon: _getSpeeddialIcon(state),
|
||||
curve: Curves.bounceInOut,
|
||||
backgroundColor: primaryColor,
|
||||
// TODO(Unknown): Theme dependent?
|
||||
foregroundColor: Colors.white,
|
||||
openCloseDial: isSpeedDialOpen,
|
||||
onPress: () {
|
||||
if (_shouldCancelEdit(state)) {
|
||||
context.read<ConversationBloc>().add(MessageEditCancelledEvent());
|
||||
controller.text = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.showSendButton) {
|
||||
context.read<ConversationBloc>().add(MessageSentEvent());
|
||||
controller.text = '';
|
||||
|
@ -179,15 +179,17 @@ class ConversationPageState extends State<ConversationPage> with TickerProviderS
|
||||
onPressed: () => _retractMessage(context, item.originId!),
|
||||
),
|
||||
] : [],
|
||||
...item.canEdit(sentBySelf) ? [
|
||||
// TODO(Unknown): Also allow correcting older messages
|
||||
...item.canEdit(sentBySelf) && state.conversation!.lastMessage?.id == item.id ? [
|
||||
OverviewMenuItem(
|
||||
icon: Icons.edit,
|
||||
text: t.pages.conversation.edit,
|
||||
onPressed: () {
|
||||
showNotImplementedDialog(
|
||||
'editing',
|
||||
context,
|
||||
context.read<ConversationBloc>().add(
|
||||
MessageEditSelectedEvent(item),
|
||||
);
|
||||
_controller.text = item.body;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
] : [],
|
||||
|
@ -87,6 +87,15 @@ class MessageBubbleBottomState extends State<MessageBubbleBottom> {
|
||||
),
|
||||
),
|
||||
),
|
||||
...widget.message.isEdited ? [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 3),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
size: _bubbleBottomIconSize,
|
||||
),
|
||||
),
|
||||
] : [],
|
||||
...widget.message.encrypted ? [
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 3),
|
||||
|
@ -130,6 +130,14 @@
|
||||
<xmpp:version>1.0.0</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0308.html" />
|
||||
<xmpp:status>partial</xmpp:status>
|
||||
<xmpp:note xml:lang="en">Supports only sending corrections</xmpp:note>
|
||||
<xmpp:version>1.2.1</xmpp:version>
|
||||
</xmpp:SupportedXep>
|
||||
</implements>
|
||||
<implements>
|
||||
<xmpp:SupportedXep>
|
||||
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0333.html" />
|
||||
|
@ -718,14 +718,14 @@ packages:
|
||||
name: moxxmpp
|
||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
version: "0.1.6"
|
||||
moxxmpp_socket_tcp:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: moxxmpp_socket_tcp
|
||||
url: "https://git.polynom.me/api/packages/Moxxy/pub/"
|
||||
source: hosted
|
||||
version: "0.1.2+7"
|
||||
version: "0.1.2+8"
|
||||
moxxyv2_builders:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -59,10 +59,10 @@ dependencies:
|
||||
version: 0.1.15
|
||||
moxxmpp:
|
||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||
version: 0.1.5
|
||||
version: 0.1.6
|
||||
moxxmpp_socket_tcp:
|
||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||
version: 0.1.2+7
|
||||
version: 0.1.2+8
|
||||
moxxyv2_builders:
|
||||
hosted: https://git.polynom.me/api/packages/Moxxy/pub
|
||||
version: 0.1.0
|
||||
|
Loading…
Reference in New Issue
Block a user