forked from cwtch.im/cwtch-ui
Merge pull request 'Save Quoted Messages in Drafts Too' (#642) from savequotes into trunk
Reviewed-on: cwtch.im/cwtch-ui#642 Reviewed-by: Dan Ballard <dan@openprivacy.ca>
This commit is contained in:
commit
1a70937898
|
@ -10,7 +10,8 @@ class EnvironmentConfig {
|
||||||
|
|
||||||
static void debugLog(String log) {
|
static void debugLog(String log) {
|
||||||
if (EnvironmentConfig.BUILD_VER == dev_version) {
|
if (EnvironmentConfig.BUILD_VER == dev_version) {
|
||||||
print(log);
|
String datetime = DateTime.now().toIso8601String();
|
||||||
|
print("$datetime $log");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,12 +53,6 @@ class AppState extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
int? get selectedIndex => _selectedIndex;
|
|
||||||
set selectedIndex(int? newVal) {
|
|
||||||
this._selectedIndex = newVal;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get disableFilePicker => _disableFilePicker;
|
bool get disableFilePicker => _disableFilePicker;
|
||||||
set disableFilePicker(bool newVal) {
|
set disableFilePicker(bool newVal) {
|
||||||
this._disableFilePicker = newVal;
|
this._disableFilePicker = newVal;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:cwtch/main.dart';
|
import 'package:cwtch/main.dart';
|
||||||
|
import 'package:cwtch/models/message_draft.dart';
|
||||||
import 'package:cwtch/models/profile.dart';
|
import 'package:cwtch/models/profile.dart';
|
||||||
import 'package:cwtch/widgets/messagerow.dart';
|
import 'package:cwtch/widgets/messagerow.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
@ -58,24 +59,28 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
|
|
||||||
int _antispamTickets = 0;
|
int _antispamTickets = 0;
|
||||||
String? _acnCircuit;
|
String? _acnCircuit;
|
||||||
String? _messageDraft;
|
MessageDraft _messageDraft = MessageDraft.empty();
|
||||||
|
|
||||||
ContactInfoState(this.profileOnion, this.identifier, this.onion,
|
ContactInfoState(
|
||||||
{nickname = "",
|
this.profileOnion,
|
||||||
isGroup = false,
|
this.identifier,
|
||||||
accepted = false,
|
this.onion, {
|
||||||
blocked = false,
|
nickname = "",
|
||||||
status = "",
|
isGroup = false,
|
||||||
imagePath = "",
|
accepted = false,
|
||||||
defaultImagePath = "",
|
blocked = false,
|
||||||
savePeerHistory = "DeleteHistoryConfirmed",
|
status = "",
|
||||||
numMessages = 0,
|
imagePath = "",
|
||||||
numUnread = 0,
|
defaultImagePath = "",
|
||||||
lastMessageTime,
|
savePeerHistory = "DeleteHistoryConfirmed",
|
||||||
server,
|
numMessages = 0,
|
||||||
archived = false,
|
numUnread = 0,
|
||||||
notificationPolicy = "ConversationNotificationPolicy.Default",
|
lastMessageTime,
|
||||||
pinned = false}) {
|
server,
|
||||||
|
archived = false,
|
||||||
|
notificationPolicy = "ConversationNotificationPolicy.Default",
|
||||||
|
pinned = false,
|
||||||
|
}) {
|
||||||
this._nickname = nickname;
|
this._nickname = nickname;
|
||||||
this._isGroup = isGroup;
|
this._isGroup = isGroup;
|
||||||
this._accepted = accepted;
|
this._accepted = accepted;
|
||||||
|
@ -95,13 +100,13 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
keys = Map<String, GlobalKey<MessageRowState>>();
|
keys = Map<String, GlobalKey<MessageRowState>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
String get nickname => this._nickname + (this._messageDraft != null && this._messageDraft != "" ? "*" : "");
|
String get nickname => this._nickname + (this._messageDraft.isNotEmpty() ? "*" : "");
|
||||||
|
|
||||||
String get savePeerHistory => this._savePeerHistory;
|
String get savePeerHistory => this._savePeerHistory;
|
||||||
|
|
||||||
String? get acnCircuit => this._acnCircuit;
|
String? get acnCircuit => this._acnCircuit;
|
||||||
|
|
||||||
String? get messageDraft => this._messageDraft;
|
MessageDraft get messageDraft => this._messageDraft;
|
||||||
|
|
||||||
set antispamTickets(int antispamTickets) {
|
set antispamTickets(int antispamTickets) {
|
||||||
this._antispamTickets = antispamTickets;
|
this._antispamTickets = antispamTickets;
|
||||||
|
@ -163,11 +168,15 @@ class ContactInfoState extends ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
set messageDraft(String? newVal) {
|
set messageDraft(MessageDraft newVal) {
|
||||||
this._messageDraft = newVal;
|
this._messageDraft = newVal;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void notifyMessageDraftUpdate() {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void selected() {
|
void selected() {
|
||||||
this._newMarkerMsgIndex = this._unreadMessages - 1;
|
this._newMarkerMsgIndex = this._unreadMessages - 1;
|
||||||
this._unreadMessages = 0;
|
this._unreadMessages = 0;
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
/// A "MessageDraft" structure that stores information about in-progress message drafts.
|
||||||
|
/// MessageDraft stores text, quoted replies, and attached images.
|
||||||
|
/// Only one draft is stored per conversation.
|
||||||
|
class MessageDraft extends ChangeNotifier {
|
||||||
|
String? _messageText;
|
||||||
|
QuotedReference? _quotedReference;
|
||||||
|
|
||||||
|
static MessageDraft empty() {
|
||||||
|
return MessageDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isNotEmpty() {
|
||||||
|
return this._messageText != null || this._quotedReference != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get messageText => _messageText;
|
||||||
|
|
||||||
|
set messageText(String? text) {
|
||||||
|
this._messageText = text;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
set quotedReference(int index) {
|
||||||
|
this._quotedReference = QuotedReference(index);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
QuotedReference? getQuotedMessage() {
|
||||||
|
return this._quotedReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearQuotedReference() {
|
||||||
|
this._quotedReference = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A QuotedReference encapsulates the state of replied-to message.
|
||||||
|
class QuotedReference {
|
||||||
|
int index;
|
||||||
|
QuotedReference(this.index);
|
||||||
|
}
|
|
@ -48,7 +48,6 @@ void selectConversation(BuildContext context, int handle) {
|
||||||
// triggers update in Double/TripleColumnView
|
// triggers update in Double/TripleColumnView
|
||||||
Provider.of<AppState>(context, listen: false).initialScrollIndex = unread;
|
Provider.of<AppState>(context, listen: false).initialScrollIndex = unread;
|
||||||
Provider.of<AppState>(context, listen: false).selectedConversation = handle;
|
Provider.of<AppState>(context, listen: false).selectedConversation = handle;
|
||||||
Provider.of<AppState>(context, listen: false).selectedIndex = null;
|
|
||||||
Provider.of<AppState>(context, listen: false).hoveredIndex = -1;
|
Provider.of<AppState>(context, listen: false).hoveredIndex = -1;
|
||||||
// if in singlepane mode, push to the stack
|
// if in singlepane mode, push to the stack
|
||||||
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
|
var isLandscape = Provider.of<AppState>(context, listen: false).isLandscape(context);
|
||||||
|
|
|
@ -9,6 +9,7 @@ import 'package:cwtch/models/appstate.dart';
|
||||||
import 'package:cwtch/models/chatmessage.dart';
|
import 'package:cwtch/models/chatmessage.dart';
|
||||||
import 'package:cwtch/models/contact.dart';
|
import 'package:cwtch/models/contact.dart';
|
||||||
import 'package:cwtch/models/message.dart';
|
import 'package:cwtch/models/message.dart';
|
||||||
|
import 'package:cwtch/models/message_draft.dart';
|
||||||
import 'package:cwtch/models/messagecache.dart';
|
import 'package:cwtch/models/messagecache.dart';
|
||||||
import 'package:cwtch/models/messages/quotedmessage.dart';
|
import 'package:cwtch/models/messages/quotedmessage.dart';
|
||||||
import 'package:cwtch/models/profile.dart';
|
import 'package:cwtch/models/profile.dart';
|
||||||
|
@ -66,7 +67,7 @@ class _MessageViewState extends State<MessageView> {
|
||||||
showDown = false;
|
showDown = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ctrlrCompose.text = Provider.of<ContactInfoState>(context, listen: false).messageDraft ?? "";
|
ctrlrCompose.text = Provider.of<ContactInfoState>(context, listen: false).messageDraft.messageText ?? "";
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,7 +227,7 @@ class _MessageViewState extends State<MessageView> {
|
||||||
child: MessageList(
|
child: MessageList(
|
||||||
scrollListener,
|
scrollListener,
|
||||||
)),
|
)),
|
||||||
bottomSheet: showPreview && showMessageFormattingPreview ? _buildPreviewBox() : _buildComposeBox(),
|
bottomSheet: showPreview && showMessageFormattingPreview ? _buildPreviewBox() : _buildComposeBox(context),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,10 +317,10 @@ class _MessageViewState extends State<MessageView> {
|
||||||
var lengthOk = (isGroup && actualMessageLength < GroupMessageLengthMax) || actualMessageLength <= P2PMessageLengthMax;
|
var lengthOk = (isGroup && actualMessageLength < GroupMessageLengthMax) || actualMessageLength <= P2PMessageLengthMax;
|
||||||
|
|
||||||
if (ctrlrCompose.value.text.isNotEmpty && lengthOk) {
|
if (ctrlrCompose.value.text.isNotEmpty && lengthOk) {
|
||||||
if (Provider.of<AppState>(context, listen: false).selectedConversation != null && Provider.of<AppState>(context, listen: false).selectedIndex != null) {
|
if (Provider.of<AppState>(context, listen: false).selectedConversation != null && Provider.of<ContactInfoState>(context, listen: false).messageDraft.getQuotedMessage() != null) {
|
||||||
var conversationId = Provider.of<AppState>(context, listen: false).selectedConversation!;
|
var conversationId = Provider.of<AppState>(context, listen: false).selectedConversation!;
|
||||||
MessageCache? cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationId)?.messageCache;
|
MessageCache? cache = Provider.of<ProfileInfoState>(context, listen: false).contactList.getContact(conversationId)?.messageCache;
|
||||||
ById(Provider.of<AppState>(context, listen: false).selectedIndex!)
|
ById(Provider.of<ContactInfoState>(context, listen: false).messageDraft.getQuotedMessage()!.index)
|
||||||
.get(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(context, listen: false).selectedProfile!, conversationId, cache!)
|
.get(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(context, listen: false).selectedProfile!, conversationId, cache!)
|
||||||
.then((MessageInfo? data) {
|
.then((MessageInfo? data) {
|
||||||
try {
|
try {
|
||||||
|
@ -335,7 +336,7 @@ class _MessageViewState extends State<MessageView> {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
EnvironmentConfig.debugLog("Exception: reply to message could not be found: " + e.toString());
|
EnvironmentConfig.debugLog("Exception: reply to message could not be found: " + e.toString());
|
||||||
}
|
}
|
||||||
Provider.of<AppState>(context, listen: false).selectedIndex = null;
|
Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearQuotedReference();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text);
|
ChatMessage cm = new ChatMessage(o: TextMessageOverlay, d: ctrlrCompose.value.text);
|
||||||
|
@ -370,7 +371,7 @@ class _MessageViewState extends State<MessageView> {
|
||||||
|
|
||||||
// At this point we have decided to send the text to the backend, failure is still possible
|
// At this point we have decided to send the text to the backend, failure is still possible
|
||||||
// but it will show as an error-ed message, as such the draft can be purged.
|
// but it will show as an error-ed message, as such the draft can be purged.
|
||||||
Provider.of<ContactInfoState>(context, listen: false).messageDraft = null;
|
Provider.of<ContactInfoState>(context, listen: false).messageDraft = MessageDraft.empty();
|
||||||
ctrlrCompose.clear();
|
ctrlrCompose.clear();
|
||||||
|
|
||||||
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
|
var profileOnion = Provider.of<ContactInfoState>(context, listen: false).profileOnion;
|
||||||
|
@ -456,7 +457,7 @@ class _MessageViewState extends State<MessageView> {
|
||||||
color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [composeBox]));
|
color: Provider.of<Settings>(context).theme.backgroundMainColor, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [composeBox]));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildComposeBox() {
|
Widget _buildComposeBox(BuildContext context) {
|
||||||
bool isOffline = Provider.of<ContactInfoState>(context).isOnline() == false;
|
bool isOffline = Provider.of<ContactInfoState>(context).isOnline() == false;
|
||||||
bool isGroup = Provider.of<ContactInfoState>(context).isGroup;
|
bool isGroup = Provider.of<ContactInfoState>(context).isGroup;
|
||||||
var showToolbar = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
|
var showToolbar = Provider.of<Settings>(context).isExperimentEnabled(FormattingExperiment);
|
||||||
|
@ -589,7 +590,7 @@ class _MessageViewState extends State<MessageView> {
|
||||||
enabled: true, // always allow editing...
|
enabled: true, // always allow editing...
|
||||||
|
|
||||||
onChanged: (String x) {
|
onChanged: (String x) {
|
||||||
Provider.of<ContactInfoState>(context, listen: false).messageDraft = x;
|
Provider.of<ContactInfoState>(context, listen: false).messageDraft.messageText = x;
|
||||||
setState(() {
|
setState(() {
|
||||||
// we need to force a rerender here to update the max length count
|
// we need to force a rerender here to update the max length count
|
||||||
});
|
});
|
||||||
|
@ -625,9 +626,10 @@ class _MessageViewState extends State<MessageView> {
|
||||||
Container(color: Provider.of<Settings>(context).theme.backgroundMainColor, padding: EdgeInsets.all(2), margin: EdgeInsets.all(2), height: 164, child: Column(children: textEditChildren));
|
Container(color: Provider.of<Settings>(context).theme.backgroundMainColor, padding: EdgeInsets.all(2), margin: EdgeInsets.all(2), height: 164, child: Column(children: textEditChildren));
|
||||||
|
|
||||||
var children;
|
var children;
|
||||||
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<AppState>(context).selectedIndex != null) {
|
if (Provider.of<AppState>(context).selectedConversation != null && Provider.of<ContactInfoState>(context).messageDraft.getQuotedMessage() != null) {
|
||||||
var quoted = FutureBuilder(
|
var quoted = FutureBuilder(
|
||||||
future: messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!, ById(Provider.of<AppState>(context).selectedIndex!)),
|
future: messageHandler(context, Provider.of<AppState>(context).selectedProfile!, Provider.of<AppState>(context).selectedConversation!,
|
||||||
|
ById(Provider.of<ContactInfoState>(context).messageDraft.getQuotedMessage()!.index)),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
var message = snapshot.data! as Message;
|
var message = snapshot.data! as Message;
|
||||||
|
@ -669,7 +671,8 @@ class _MessageViewState extends State<MessageView> {
|
||||||
splashRadius: Material.defaultSplashRadius / 2,
|
splashRadius: Material.defaultSplashRadius / 2,
|
||||||
tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage,
|
tooltip: AppLocalizations.of(context)!.tooltipRemoveThisQuotedMessage,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Provider.of<AppState>(context, listen: false).selectedIndex = null;
|
Provider.of<ContactInfoState>(context, listen: false).messageDraft.clearQuotedReference();
|
||||||
|
setState(() {});
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -89,7 +89,9 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
||||||
tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage,
|
tooltip: AppLocalizations.of(context)!.tooltipReplyToThisMessage,
|
||||||
splashRadius: Material.defaultSplashRadius / 2,
|
splashRadius: Material.defaultSplashRadius / 2,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
|
Provider.of<ContactInfoState>(context, listen: false).messageDraft.quotedReference = Provider.of<MessageMetadata>(context, listen: false).messageID;
|
||||||
|
Provider.of<ContactInfoState>(context, listen: false).notifyMessageDraftUpdate();
|
||||||
|
setState(() {});
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor)));
|
icon: Icon(Icons.reply, color: Provider.of<Settings>(context).theme.dropShadowColor)));
|
||||||
|
|
||||||
|
@ -243,7 +245,9 @@ class MessageRowState extends State<MessageRow> with SingleTickerProviderStateMi
|
||||||
},
|
},
|
||||||
onPanEnd: (details) {
|
onPanEnd: (details) {
|
||||||
_runAnimation(details.velocity.pixelsPerSecond, size);
|
_runAnimation(details.velocity.pixelsPerSecond, size);
|
||||||
Provider.of<AppState>(context, listen: false).selectedIndex = Provider.of<MessageMetadata>(context, listen: false).messageID;
|
Provider.of<ContactInfoState>(context, listen: false).messageDraft.quotedReference = Provider.of<MessageMetadata>(context, listen: false).messageID;
|
||||||
|
Provider.of<ContactInfoState>(context, listen: false).notifyMessageDraftUpdate();
|
||||||
|
setState(() {});
|
||||||
},
|
},
|
||||||
onLongPress: () async {
|
onLongPress: () async {
|
||||||
modalShowReplies(context, AppLocalizations.of(context)!.headingReplies, AppLocalizations.of(context)!.messageNoReplies, settings, pis, cis, borderColor, cache, messageID);
|
modalShowReplies(context, AppLocalizations.of(context)!.headingReplies, AppLocalizations.of(context)!.messageNoReplies, settings, pis, cis, borderColor, cache, messageID);
|
||||||
|
|
Loading…
Reference in New Issue