Initial reaction support #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "pounce/goguma:reacts"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
These commits add IRC support to the model and controller. There is no support yet for viewing or showing reactions. These features will be added in a later PR once consensus on how the features are to be developed has been reached.
a6f60b5460
todfb3f9a182
dfb3f9a182
to767294e17d
767294e17d
to019a21fa9b
019a21fa9b
to144f617dc7
Here's a first round of comments. The commit messages seem to be mixed up as well.
Thanks!
@ -662,8 +662,8 @@ class ClientController {
var typing = msg.tags['+typing'];
if (typing != null && !client.isMyNick(msg.source.name)) {
_bufferList.get(target, network)?.setTyping(msg.source.name, typing == 'active');
break;
Since there is the
client.isMyNick()
check this means we prevent some of the typingTAGMSG
s from reaching_handleChatMessages()
but not all. I think I'd prefer to not have this special case and always pass all of theTAGMSG
s to_handleChatMessages()
.@ -797,3 +795,1 @@
if (buf.messageHistoryLoaded) {
var models = await buildMessageModelList(_db, entries);
buf.addMessages(models, append: !isHistory);
List<IrcMessage> privmsgs = [];
We could directly build a list of
MessageEntry
here to remove the need for theprivmsgs.map()
call below.@ -803,0 +816,4 @@
}
// The empty string is the prefix of all strings, and is ordered first.
String t = '';
Can we move this variable declaration right before we need it, ie. right before the loop on the message entries?
Also I don't think we need to take into account reacts here: reacts don't mark buffers as unread, right?
@ -239,0 +246,4 @@
DateTime? _dateTime;
ReactionEntry({
Could we maybe use the same signature as the
MessageEntry
constructor here for consistency?(Was there a reason for not doing this? For instance, constructing this object requires some tags?)
my rationale is that only messages of a particular format can be a
ReactionEntry
(and we don't really care about the raw message anyway). We already have to do the work of determining whether a message has that format inclient.dart
, and we would need to duplicate it all here (with a lot of assertions) to get the relevant fields out of anIrcMessage
@ -724,6 +793,65 @@ class DB {
});
}
Future<Map<String, List<ReactionEntry>>> fetchReactionSetByNetworkMsgid(int buffer, List<String> msgids) async {
Hm,
msgids
will typically contain 1000 entries. This sounds a bit much... What's the performance impact of this function?This function has almost the same code, and is called in the same context with the same values as
fetchMessageSetByNetworkMsgid
above, so I was expecting similar performance to existing codeas discussed on IRC, this is called with the whole buffer while
fetchMessageSetByNetworkMsgid
is only called with reply parentsI restructured the query to only take two message parameters. This should be a safer way to build the sql query. I'm not sure if it's so much more performant (I didn't check that the query plan is optimized with indices) but it should be better since it's all in the sql engine now.
@ -727,0 +812,4 @@
return reactions;
}
Future<void> storeOrUpdateReactions(List<ReactionEntry> entries) async {
In Goguma "store" usually means "upsert", see for instance
storeBuffer()
andstoreMessage()
. So I think we can dropOrUpdate
here.@ -727,0 +821,4 @@
}
var colNames = <String>[];
var valList = <Object?>[];
entry.toMap().forEach((colName, val) {
Nit: I slightly prefer
for
loops overforEach()
callbacks.this isn't actually
forEach
fromIterable
, sinceMap
doesn't implementIterable
(which is why this function takes two values instead of one). There's a clean forEach version of this which uses dart > v3.0.0 and patterns, which I will change to and we'll see which one is better.@ -727,0 +823,4 @@
var valList = <Object?>[];
entry.toMap().forEach((colName, val) {
colNames.add(colName);
valList.add(val);
This could maybe be simplified with
Map.keys
andMap.values
. But the current logic is fine by me as well.@ -727,0 +836,4 @@
// In particular, we update reactions only if we see a newer reaction
// from the same user.
//
// UPSERT: https://www.sqlite.org/lang_upsert.html
I personally don't think we should explain how SQLite works here. Readers unfamiliar with
ON CONFLICT … DO
can easily read the docs.I left the link to the docs. I agree that everything else is superfluous.
@ -727,0 +841,4 @@
INSERT INTO Reaction (${colNames.join(', ')})
VALUES ($placeholderList)
ON CONFLICT (nick, reply_network_msgid)
DO UPDATE SET text=excluded.text, time=excluded.time
Nit: missing spaces around
=
@ -393,6 +393,7 @@ class BufferModel extends ChangeNotifier {
String? _lastDeliveredTime;
bool _messageHistoryLoaded = false;
List<MessageModel> _messages = [];
final Map<String, MessageModel> _messagesById = {};
Could we maybe name this
_messagesByNetworkMsgid
just to make it clear it's different from database IDs?Or should we use database IDs as keys instead?
@ -492,31 +494,71 @@ class BufferModel extends ChangeNotifier {
notifyListeners();
}
Nit: stray empty line.
@ -495,0 +499,4 @@
// We need to make sure that we have a concrete list of MessageModel before
// adding to our two data structures. This ensures that the same object is
// written to both.
msgs = msgs.toList();
I'm not sure I understand. Is the concern that
msgs
might yield different objects when it's iterated over twice?Yes, this happened in practice and caused some bugs. In particular, some iterables are "consumed" once you iterate over them (with a for loop) while others aren't. If this iterable is consumed in the first call, then it will be empty in the next call.
@ -495,0 +516,4 @@
msgs
.map((msg) => MapEntry(msg.entry.networkMsgid, msg))
.where((entry) => entry.key != null)
.map((entry) => MapEntry(entry.key!, entry.value)));
Would be nice to extract this in a helper function, e.g.
_populateMessagesById
.@ -563,3 +606,2 @@
MessageModel({ required this.entry, this.replyTo }) {
assert(entry.id != null);
MessageModel({ required this.entry, this.replyTo, required Iterable<ReactionEntry> reactions}) :
Nit: missing space before
}
@ -566,0 +610,4 @@
reactions = [...reactions],
assert(entry.id != null);
void addReaction(ReactionEntry newReact) {
This should probably be private, since it doesn't notify any listener.
We might have to use it later to when sending a reaction, but I'm not entirely sure. I can always make it unprivate at that point if need-be.
@ -566,0 +612,4 @@
void addReaction(ReactionEntry newReact) {
var oldReact = reactions.cast<ReactionEntry?>()
.singleWhere((react) => react!.nick == newReact.nick, orElse: () => null);
Do we only allow a single reaction per nick?
Yes, each nick can only react once.
@ -566,0 +615,4 @@
.singleWhere((react) => react!.nick == newReact.nick, orElse: () => null);
if (oldReact != null) {
if(oldReact.dateTime.isAfter(newReact.dateTime)) {
Nit: missing space before
(
144f617dc7
to9d7cdf8894
9d7cdf8894
toc3239abf68
Thanks a lot! Here's a new round of comments, we're getting closer :)
@ -30,6 +30,7 @@
.pub-cache/
.pub/
/build/
devtools_options.yaml
Nit: where is this file coming from?
It think it's autogenerated by the flutter debugger. That or some portion of the dart tooling that I'm using. See https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
Either way since it's mentioned in the official flutter docs, I thought it'd be fine to add to the .gitignore
@ -803,0 +829,4 @@
// now we need to update unread counts and bump lastReadTimes
// The empty string is the prefix of all strings, and is ordered first.
String t = '';
I think the 3 lines above can be reverted to be the same as the previous logic.
@ -1157,1 +1182,3 @@
}
List<String> parentMsgids = entries
.map((e) => e.msg.tags['+draft/reply'])
.whereType<String>()
Hm, not a fan of
whereType()
because it's dangerous wrt. type safety. If this doesn't match what's in the entries then all entries are silently dropped?@ -1161,0 +1188,4 @@
var parentSet = await db.fetchMessageSetByNetworkMsgid(bufferId, parentMsgids);
// We assume here that `entries` is given in time-sorted order.
Map<String, List<ReactionEntry>> reactionSet = {};
Nit: this is a map, not a set (ditto for
parentSet
)@ -1161,2 +1193,4 @@
return entries.map((entry) {
MessageEntry? replyTo;
List<ReactionEntry> reacts = List.empty();
Nit:
[]
can be used instead ofList.empty()
.@ -1167,1 +1201,3 @@
return MessageModel(entry: entry, replyTo: replyTo);
var msgid = entry.networkMsgid;
if (msgid != null) {
reacts = reactionSet[msgid] ?? reacts;
Nit:
?? reacts
is unnecessaryhere
reactionSet[msgid]
could very easily be null, ifmsgid
doesn't have any reactions. So instead we give it an empty list.I've condensed this logic into something much simpler with one null check.
@ -380,0 +424,4 @@
CREATE TABLE Reaction (
id INTEGER PRIMARY KEY,
buffer INTEGER NOT NULL,
nick TEXT NOT NULL,
I'm kind of wondering if we should really store the nickname as a field, or if we should take a more
Message
-like approach where the whole raw IRC message is stored in the DB. The reason is that we may want to access other message tags, e.g.account
. I'm unsure we'll want to dedup by nickname on the long run.@ -727,0 +798,4 @@
SELECT *
FROM Reaction
WHERE buffer = ?
AND reply_network_msgid IN (
Style nit: we do not align SQL keywords (we don't align anything in general)
@ -563,3 +602,2 @@
MessageModel({ required this.entry, this.replyTo }) {
assert(entry.id != null);
MessageModel({ required this.entry, this.replyTo, required Iterable<ReactionEntry> reactions }) :
Nit: maybe
reactions
could be optional likereplyTo
@ -10,3 +10,3 @@
environment:
sdk: '>=2.19.0 <3.0.0'
sdk: '>=3.0.0 <4.0.0'
Hm, what caused this?
c3239abf68
to5246c1b4a0
5246c1b4a0
tocd83964f67
cd83964f67
to04ee3a256d
@ -468,0 +512,4 @@
id INTEGER PRIMARY KEY,
buffer INTEGER NOT NULL,
nick TEXT NOT NULL,
text TEXT NOT NULL,
Seems like this is unused.
04ee3a256d
to5716ed4e55
5716ed4e55
to04e20bb9c2
04e20bb9c2
tof77a909ff5
f77a909ff5
tod8d8afecf6
I've made a few changes to the implementation:
MessageModel.reactions
getter.Thanks!