summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md6
-rw-r--r--src/carddelegate.cpp6
-rw-r--r--src/cardlayout.cpp33
-rw-r--r--src/cardlayout.h17
-rw-r--r--src/threadlistmodel.cpp18
-rw-r--r--src/threadlistmodel.h8
-rw-r--r--src/threadlistview.cpp4
-rw-r--r--src/threadlistview.h2
-rw-r--r--src/types.h6
-rw-r--r--tests/test_cardlayout.cpp24
-rw-r--r--tests/test_mainwindow.cpp9
-rw-r--r--tests/test_threadlistmodel.cpp39
-rw-r--r--translations/qtmaildir_it_IT.ts11
13 files changed, 111 insertions, 72 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56d98e4..4bdc536 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -115,6 +115,12 @@ point at which they are stable.
removes the row at once while an automatic one waits until you move on.
Reading the last unread message of a long conversation is the one case that
still waits for the next query or sync.
+- **A conversation's card counts its MESSAGES, not its replies.** The pill
+ that opens the thread read "N replies" against the total the row actually
+ lists: a thread of one message and four replies said "4 replies" over rows
+ for all five messages. It now reads "5 messages", which is what the
+ conversation is. A thread of one message still shows no count at all, since
+ there is nothing for the pill to open.
- **Delete and Restore appear only where they apply.** Delete is hidden on
mail already in the trash, where it reported success and did nothing, and
Restore is hidden on mail that was never deleted.
diff --git a/src/carddelegate.cpp b/src/carddelegate.cpp
index fbdf13f..02f1df0 100644
--- a/src/carddelegate.cpp
+++ b/src/carddelegate.cpp
@@ -40,7 +40,7 @@ CardLayout::Input inputFor(const QModelIndex &index)
CardLayout::Input in;
in.isMessage = index.data(ThreadListModel::IsMessageRole).toBool();
in.depth = index.data(ThreadListModel::MessageDepthRole).toInt();
- in.replyCount = index.data(ThreadListModel::ReplyCountRole).toInt();
+ in.messageCount = index.data(ThreadListModel::MessageCountRole).toInt();
in.dateFormat = index.data(ThreadListModel::DateFormatRole).toString();
// Item 70's marks. The layout reserves a rect for each, so these have to
@@ -305,14 +305,14 @@ void CardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
drawMark(card.receivedForwardRect, Marks::Mark::ReceivedForward);
drawMark(card.repliedRect, Marks::Mark::Replied);
- // The reply count, which is also the expander, drawn as a PILL.
+ // The message count, which is also the expander, drawn as a PILL.
//
// A bare "3" on the card's own background read as an unexplained number
// beside the subject and gave no hint that it could be clicked. The chip
// shape says "this is a control", matching the tag chips on line 3, and the
// word says what the number counts.
if (!card.expanderRect.isEmpty()) {
- const int count = index.data(ThreadListModel::ReplyCountRole).toInt();
+ const int count = index.data(ThreadListModel::MessageCountRole).toInt();
const QString label = CardLayout::expanderLabel(
count, option.state & QStyle::State_Open);
diff --git a/src/cardlayout.cpp b/src/cardlayout.cpp
index ec22635..f89d3ec 100644
--- a/src/cardlayout.cpp
+++ b/src/cardlayout.cpp
@@ -18,6 +18,7 @@
#include "cardlayout.h"
+#include <QCoreApplication>
#include <QFontMetrics>
#include <QLocale>
@@ -52,9 +53,9 @@ int CardLayout::markSide(const QFont &font)
return qMax(8, side);
}
-QString CardLayout::expanderLabel(int replyCount, bool expanded)
+QString CardLayout::expanderLabel(int messageCount, bool expanded)
{
- // "3 replies", not a bare "3". The count alone reads as an unexplained
+ // "5 messages", not a bare "5". The count alone reads as an unexplained
// number beside the subject, and the word is what says the card opens.
//
// The triangle is NO LONGER part of this string. Item 70 made it a drawn
@@ -65,14 +66,20 @@ QString CardLayout::expanderLabel(int replyCount, bool expanded)
// when the card opens, and a caller that stopped passing the state would
// hide that requirement rather than satisfy it.
//
- // Not translated through tr() here because CardLayout is a plain struct
- // rather than a QObject; the delegate is where a translated build would
- // wrap this, and the string is deliberately kept in one place so there is
- // exactly one thing to change.
+ // CardLayout is a plain struct rather than a QObject, so this cannot use
+ // tr(); QCoreApplication::translate() with an explicit context is the
+ // equivalent, and the literal context string is what lupdate extracts
+ // under. The word is translated rather than the whole "%1 %2", because
+ // %n's untranslated fallback on this Qt does not pluralise (measured:
+ // "%n message(s)" stays literally "(s)"), and the two forms read cleanly
+ // in the .ts.
Q_UNUSED(expanded);
- const QString word = replyCount == 1 ? QStringLiteral("reply")
- : QStringLiteral("replies");
- return QStringLiteral("%1 %2").arg(replyCount).arg(word);
+ const QString word = messageCount == 1
+ ? QCoreApplication::translate("CardLayout",
+ "message")
+ : QCoreApplication::translate("CardLayout",
+ "messages");
+ return QStringLiteral("%1 %2").arg(messageCount).arg(word);
}
QString CardLayout::widestDateSample(const QString &format)
@@ -196,18 +203,18 @@ CardLayout CardLayout::compute(const Input &input, const QRect &rect,
- kPaddingX),
metrics.height());
- // The expander is the reply count as a PILL, on line two and on the right.
+ // The expander is the message count as a PILL, on line two and on the right.
//
// Sized from the label actually drawn rather than from a fixed sample, so
// the background and the text inside it cannot disagree. Both states of the
// glyph are measured because the rect must not change width when the card
// is expanded: a pill that resized on click would shift the subject's
// elision under the pointer.
- if (input.replyCount > 0) {
+ if (input.messageCount > 0) {
const int collapsed = smallMetrics.horizontalAdvance(
- expanderLabel(input.replyCount, false));
+ expanderLabel(input.messageCount, false));
const int expanded = smallMetrics.horizontalAdvance(
- expanderLabel(input.replyCount, true));
+ expanderLabel(input.messageCount, true));
// The triangle is a drawn mark since item 70, so the pill has to
// reserve its width explicitly. It came free from the text metrics
// while it was a glyph in the label, which is exactly the kind of
diff --git a/src/cardlayout.h b/src/cardlayout.h
index 57edca7..9620900 100644
--- a/src/cardlayout.h
+++ b/src/cardlayout.h
@@ -35,7 +35,7 @@
/// The card is three lines, always:
///
/// sender ................................ date <- senderRect/dateRect
-/// * subject @ v 3 replies <- subjectRect/expanderRect
+/// * subject @ v 5 messages <- subjectRect/expanderRect
/// [tag] [tag] <- tagRect
struct CardLayout
{
@@ -45,7 +45,7 @@ struct CardLayout
{
bool isMessage = false;
int depth = 0; ///< 0 for a thread root, 1 for a direct reply.
- int replyCount = 0; ///< 0 means no expander.
+ int messageCount = 0; ///< Messages in the conversation; 0 means no expander.
/// A QDateTime::toString() pattern from [general] date_format, or empty
/// for the system's short format.
@@ -105,8 +105,8 @@ struct CardLayout
QRect subjectRect;
QRect tagRect;
- /// The reply count's rect, and the click target that toggles the thread.
- /// Empty when the row has no replies.
+ /// The message count's rect, and the click target that toggles the thread.
+ /// Empty when the row has no expander.
QRect expanderRect;
/// The flagged mark, at the start of line two before the subject. Empty
@@ -197,15 +197,18 @@ struct CardLayout
/// The widest string formatDate() can return, for reserving space.
static QString widestDateSample(const QString &format = QString());
- /// The expander's label: the reply count with its glyph, as drawn.
+ /// The expander's label: the message count with its word, as drawn.
///
/// Shared with the layout for the same reason as formatDate: the rect
/// reserved for the pill and the text put inside it must come from one
/// place, or a count wider than the sample the layout guessed at spills
/// out of its own background.
///
- /// `expanded` chooses which way the triangle points.
- static QString expanderLabel(int replyCount, bool expanded);
+ /// `expanded` chooses which way the triangle points. The word is
+ /// translated: a pill reading a foreign count in English next to an
+ /// otherwise local interface is a tiny broken window, and this is the one
+ /// user-facing string this plain struct owns.
+ static QString expanderLabel(int messageCount, bool expanded);
/// Padding inside the expander pill, matching a tag chip's, so the two read
/// as the same kind of object on the card.
diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp
index 8bb9849..5c44dd4 100644
--- a/src/threadlistmodel.cpp
+++ b/src/threadlistmodel.cpp
@@ -439,7 +439,7 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
m_forwardPrefixes);
case IsRepliedRole:
return node.isReplied();
- case ReplyCountRole:
+ case MessageCountRole:
// A reply never offers an expander: nesting past the first level is
// drawn from depth, not from further parent-child structure.
return 0;
@@ -714,15 +714,19 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
m_forwardPrefixes);
case IsRepliedRole:
return thread.isReplied();
- case ReplyCountRole:
+ case MessageCountRole:
// Zero in a flat list, so the card draws no expander pill. The count
- // and hasChildren() must agree: a card advertising "3 replies" that
- // cannot be opened is the inert-glyph defect this project has already
- // shipped once.
+ // and hasChildren() must agree: a card advertising a count for a thread
+ // that cannot be opened is the inert-glyph defect this project has
+ // already shipped once.
if (m_flatMode)
return 0;
- // totalCount includes the root message, which is the card itself.
- return qMax(0, thread.totalCount - 1);
+ // The number of MESSAGES in the conversation, not the replies alone:
+ // the row stands for the conversation now (item 177), so a thread of
+ // one message and four replies reads "5 messages". A thread of one
+ // shows nothing: its row is the message, there is nothing to expand,
+ // and the pill is the expander.
+ return thread.totalCount > 1 ? thread.totalCount : 0;
case DateFormatRole:
return m_dateFormat;
default:
diff --git a/src/threadlistmodel.h b/src/threadlistmodel.h
index 93474ac..fee4a1f 100644
--- a/src/threadlistmodel.h
+++ b/src/threadlistmodel.h
@@ -83,7 +83,8 @@ public:
MessageIdRole,
/// The message's reply depth, for the view's indentation. 1 for a
- /// direct reply, since depth 0 is the root row itself.
+ /// direct reply; 0 is the thread's first message, which sits under the
+ /// conversation row unindented.
MessageDepthRole,
/// True when the row is a thread that has replies to show.
@@ -138,7 +139,8 @@ public:
/// bool; the message was replied to, from the Maildir "R" flag.
IsRepliedRole,
- ReplyCountRole, ///< int; 0 when a thread has no replies.
+ MessageCountRole, ///< int; messages in the conversation, 0 when the
+ ///< thread has one message and nothing to expand.
/// The [general] date_format pattern, or empty for the system's short
/// format. Same row value for every row.
@@ -207,7 +209,7 @@ public:
/// that is what keeps it from leaking: it is off by default, only the Sent
/// button turns it on, and every other query turns it off again. The
/// expander already comes from hasChildren() and the card's count from
- /// ReplyCountRole, so flat mode is those two answering differently and
+ /// MessageCountRole, so flat mode is those two answering differently and
/// nothing else changes.
///
/// The children are not discarded, only hidden. Leaving flat mode restores
diff --git a/src/threadlistview.cpp b/src/threadlistview.cpp
index 5a3ce77..019d7c9 100644
--- a/src/threadlistview.cpp
+++ b/src/threadlistview.cpp
@@ -27,10 +27,10 @@ void ThreadListView::mousePressEvent(QMouseEvent *event)
{
const QModelIndex index = indexAt(event->pos());
- // The reply count IS the expander. Anything outside its rect selects the
+ // The message count IS the expander. Anything outside its rect selects the
// card and opens it, which is what the rest of the card is for.
if (event->button() == Qt::LeftButton && index.isValid()
- && index.data(ThreadListModel::ReplyCountRole).toInt() > 0) {
+ && index.data(ThreadListModel::MessageCountRole).toInt() > 0) {
QStyleOptionViewItem option;
initViewItemOption(&option);
diff --git a/src/threadlistview.h b/src/threadlistview.h
index 3215153..a509389 100644
--- a/src/threadlistview.h
+++ b/src/threadlistview.h
@@ -40,7 +40,7 @@ public:
using QTreeView::QTreeView;
protected:
- /// Toggles a thread when its reply count is clicked.
+ /// Toggles a thread when its message count is clicked.
///
/// Being VISIBLE and being CLICKABLE are separate properties:
/// setRootIsDecorated(false), needed to stop the style drawing its own
diff --git a/src/types.h b/src/types.h
index 8e24cd1..ee2627f 100644
--- a/src/types.h
+++ b/src/types.h
@@ -190,9 +190,9 @@ struct MessageNode
QStringList tags;
QString filePath;
- /// Reply depth within the thread. 0 is the thread's first message, which
- /// occupies the ROOT row rather than a child row: the user's model is
- /// "N replies", so a thread of 7 shows 1 root and 6 descendants.
+ /// Reply depth within the thread. 0 is the thread's first message: since
+ /// item 177 a conversation lists it as a child row too, but it carries no
+ /// nesting. 1 is a direct reply.
int depth = 0;
bool isUnread() const { return tags.contains(QStringLiteral("unread")); }
diff --git a/tests/test_cardlayout.cpp b/tests/test_cardlayout.cpp
index c81296f..09afc5f 100644
--- a/tests/test_cardlayout.cpp
+++ b/tests/test_cardlayout.cpp
@@ -58,7 +58,7 @@ CardLayout::Input threadInput()
CardLayout::Input in;
in.isMessage = false;
in.depth = 0;
- in.replyCount = 3;
+ in.messageCount = 3;
return in;
}
@@ -67,7 +67,7 @@ CardLayout::Input replyInput(int depth)
CardLayout::Input in;
in.isMessage = true;
in.depth = depth;
- in.replyCount = 0;
+ in.messageCount = 0;
return in;
}
@@ -91,7 +91,7 @@ void TestCardLayout::everyCardIsTheSameHeight()
const CardLayout deepReply =
CardLayout::compute(replyInput(3), QRect(0, 0, 400, thread), font);
CardLayout::Input noRepliesIn = threadInput();
- noRepliesIn.replyCount = 0;
+ noRepliesIn.messageCount = 0;
const CardLayout noReplies =
CardLayout::compute(noRepliesIn, QRect(0, 0, 400, thread), font);
@@ -225,7 +225,7 @@ void TestCardLayout::expanderIsEmptyWithoutReplies()
const QFont font;
const int h = CardLayout::heightFor(font);
CardLayout::Input in = threadInput();
- in.replyCount = 0;
+ in.messageCount = 0;
const CardLayout card = CardLayout::compute(in, QRect(0, 0, 400, h), font);
QVERIFY(card.expanderRect.isEmpty());
@@ -240,12 +240,12 @@ void TestCardLayout::theExpanderReadsAsAPillWithAWord()
// NO triangle in the label since item 70: it is a drawn mark now, and a
// glyph left here would be a second triangle beside the drawn one. The
// label is the words alone, and the state no longer changes it.
- QCOMPARE(CardLayout::expanderLabel(3, false), QStringLiteral("3 replies"));
- QCOMPARE(CardLayout::expanderLabel(3, true), QStringLiteral("3 replies"));
+ QCOMPARE(CardLayout::expanderLabel(3, false), QStringLiteral("3 messages"));
+ QCOMPARE(CardLayout::expanderLabel(3, true), QStringLiteral("3 messages"));
- // Singular, because "1 replies" is the kind of detail that makes an
+ // Singular, because "1 messages" is the kind of detail that makes an
// interface look unfinished.
- QCOMPARE(CardLayout::expanderLabel(1, false), QStringLiteral("1 reply"));
+ QCOMPARE(CardLayout::expanderLabel(1, false), QStringLiteral("1 message"));
// The glyphs are gone from the label entirely. Asserted rather than assumed,
// because a stray one would draw underneath the mark and look like a
@@ -377,7 +377,11 @@ void TestCardLayout::marksDoNotCollideWithEachOtherOrTheExpander()
// densest line two can get. Nothing may overlap anything.
const QFont font;
const int h = CardLayout::heightFor(font);
- const QRect rect(0, 0, 400, h);
+ // 460 rather than 400: the pill reads "3 messages" now, a character wider
+ // than "3 replies" was, and 400 left the elastic subject narrower than the
+ // avatar gutter with all four marks out. The width is a stress value, not
+ // a spec.
+ const QRect rect(0, 0, 460, h);
CardLayout::Input in = threadInput();
in.flagged = true;
@@ -421,7 +425,7 @@ void TestCardLayout::marksDoNotCollideWithEachOtherOrTheExpander()
// not leave the subject narrower than the avatar gutter beside it.
QVERIFY2(card.subjectRect.width() > card.avatarRect.width(),
"four marks left the subject narrower than the avatar gutter on a "
- "400px card");
+ "460px card");
}
void TestCardLayout::dateIsFlushRight()
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index aecefb7..2e7d220 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -1396,10 +1396,11 @@ void TestMainWindow::aThreadWithRepliesDrawsAVisibleExpander()
const QModelIndex first = model->index(0, 0, QModelIndex());
const QModelIndex second = model->index(1, 0, QModelIndex());
- // Guards: the model agrees about which thread has replies, and only that
- // one is offered an expander at all.
- QCOMPARE(model->data(first, ThreadListModel::ReplyCountRole).toInt(), 2);
- QCOMPARE(model->data(second, ThreadListModel::ReplyCountRole).toInt(), 0);
+ // Guards: the model agrees about which thread has an expander, and only
+ // that one is offered one at all. The count is MESSAGES: the three-message
+ // thread reads 3, the lone message reads 0.
+ QCOMPARE(model->data(first, ThreadListModel::MessageCountRole).toInt(), 3);
+ QCOMPARE(model->data(second, ThreadListModel::MessageCountRole).toInt(), 0);
const QFont font = view->font();
const int height = CardLayout::heightFor(font);
diff --git a/tests/test_threadlistmodel.cpp b/tests/test_threadlistmodel.cpp
index fba2d2d..28d3a8b 100644
--- a/tests/test_threadlistmodel.cpp
+++ b/tests/test_threadlistmodel.cpp
@@ -49,7 +49,7 @@ private slots:
void appendingEmptyBatchIsNoOp();
void clearResetsModel();
void reportsSubjectAndAuthors();
- void theReplyCountExcludesTheRootMessage();
+ void thePillCountsTheThreadsMessages();
void unreadThreadsRenderBold();
void readThreadsAreDimmedAndUnreadAreNot();
void flaggedThreadsShowAStar();
@@ -519,7 +519,7 @@ void TestThreadListModel::aFlatViewsAvatarFollowsTheRecipient()
QStringLiteral("me@example.org"));
}
-void TestThreadListModel::theReplyCountExcludesTheRootMessage()
+void TestThreadListModel::thePillCountsTheThreadsMessages()
{
ThreadListModel model;
@@ -530,12 +530,14 @@ void TestThreadListModel::theReplyCountExcludesTheRootMessage()
model.appendBatch({ single, multi });
// The count used to be a "(4)" suffix on the subject. It is the expander
- // on the card's second line now, and it counts REPLIES: totalCount
- // includes the root message, which is the card itself.
+ // on the card's second line now, and it counts MESSAGES: the row stands
+ // for the conversation since item 177, so a thread of one message and
+ // three replies reads "4 messages". A thread of one shows nothing to
+ // expand, so its count stays 0.
QCOMPARE(model.data(model.index(0, 0),
- ThreadListModel::ReplyCountRole).toInt(), 0);
+ ThreadListModel::MessageCountRole).toInt(), 0);
QCOMPARE(model.data(model.index(1, 0),
- ThreadListModel::ReplyCountRole).toInt(), 3);
+ ThreadListModel::MessageCountRole).toInt(), 4);
// And the subject is bare, with no count spliced into it.
QCOMPARE(model.data(model.index(1, 0),
@@ -1483,9 +1485,9 @@ void TestThreadListModel::modelHasOneColumn()
QStringLiteral("alice@example.org"));
QVERIFY(index.data(ThreadListModel::DateRole).toDateTime().isValid());
- // A single-message thread offers no expander: totalCount includes the root
- // message, which is the card itself.
- QCOMPARE(index.data(ThreadListModel::ReplyCountRole).toInt(), 0);
+ // A single-message thread offers no expander: it is its own message and
+ // has nothing to open onto.
+ QCOMPARE(index.data(ThreadListModel::MessageCountRole).toInt(), 0);
}
void TestThreadListModel::aFlatThreadStillListsItsReplies()
@@ -1520,12 +1522,11 @@ void TestThreadListModel::aFlatThreadStillListsItsReplies()
.toString(),
QStringLiteral("m1@example.org"));
- // The pill counts REPLIES while the rows are MESSAGES, so since item 177
- // the rows are one more than the pill: the conversation lists its first
- // message too. They must still move together, or the expander opens onto a
- // number the card never promised.
- QCOMPARE(root.data(ThreadListModel::ReplyCountRole).toInt(),
- model.rowCount(root) - 1);
+ // The pill counts MESSAGES, which is exactly what the rows are: since item
+ // 177 a conversation lists every message under itself, so the number on
+ // the card and the number of rows it opens onto agree.
+ QCOMPARE(root.data(ThreadListModel::MessageCountRole).toInt(),
+ model.rowCount(root));
}
void TestThreadListModel::theRootCardKnowsItsOwnMessage()
@@ -1893,7 +1894,7 @@ void TestThreadListModel::flatModeOffersNoExpanderAndNoReplyCount()
// the replies they received under a view that claims to be their outbox.
//
// Deliberately not a second model or a filtered query. The expander is
- // driven by hasChildren() and the card's count by ReplyCountRole, both
+ // driven by hasChildren() and the card's count by MessageCountRole, both
// already here, so flat mode is those two answering differently.
ThreadListModel model;
model.appendBatch({ makeThread(QStringLiteral("t1"),
@@ -1906,13 +1907,13 @@ void TestThreadListModel::flatModeOffersNoExpanderAndNoReplyCount()
// below: a test whose subject was already flat would pass either way.
QVERIFY2(model.hasChildren(thread),
"the fixture thread is not expandable, so this proves nothing");
- QCOMPARE(model.data(thread, ThreadListModel::ReplyCountRole).toInt(), 1);
+ QCOMPARE(model.data(thread, ThreadListModel::MessageCountRole).toInt(), 2);
model.setFlatMode(true);
QVERIFY2(!model.hasChildren(thread),
"a flat list still offered an expander");
- QCOMPARE(model.data(thread, ThreadListModel::ReplyCountRole).toInt(), 0);
+ QCOMPARE(model.data(thread, ThreadListModel::MessageCountRole).toInt(), 0);
// rowCount has to agree, or the view draws an expander it cannot open, or
// opens onto rows the card said were not there.
@@ -1943,7 +1944,7 @@ void TestThreadListModel::flatModeIsOffByDefaultAndReversible()
model.setFlatMode(false);
QVERIFY2(model.hasChildren(thread),
"leaving flat mode did not restore the tree");
- QCOMPARE(model.data(thread, ThreadListModel::ReplyCountRole).toInt(), 1);
+ QCOMPARE(model.data(thread, ThreadListModel::MessageCountRole).toInt(), 2);
}
void TestThreadListModel::recipientsReplaceTheSenderWhenPresent()
diff --git a/translations/qtmaildir_it_IT.ts b/translations/qtmaildir_it_IT.ts
index d158380..5249149 100644
--- a/translations/qtmaildir_it_IT.ts
+++ b/translations/qtmaildir_it_IT.ts
@@ -2,6 +2,17 @@
<!DOCTYPE TS>
<TS version="2.1" language="it_IT">
<context>
+ <name>CardLayout</name>
+ <message>
+ <source>message</source>
+ <translation>messaggio</translation>
+ </message>
+ <message>
+ <source>messages</source>
+ <translation>messaggi</translation>
+ </message>
+</context>
+<context>
<name>ComposeWindow</name>
<message>
<source>Compose[*]</source>