Skip to content

Commit a7bb0b3

Browse files
MrLeninclaude
andcommitted
redact: fix render state, support late updates, queue on pending
Five bugs in the message-redaction path: - xtext entry_set_text didn't invalidate sublines, so render drew only str_len-many bytes of the new placeholder and burned the line budget, hiding entries below. Also reconcile num_lines via the display_lines delta and update_weight234 so index234_by_weight stays in sync. - xtext set_redaction_info no-op'd on a second REDACT, so the stored info diverged from the displayed placeholder and the click cycle flipped between operators/reasons. Always refresh redactor info; snapshot original_content only on the first call. - fe_redact_message's REDACTED-only guard missed PROMPT and REVEALED. New gtk_xtext_entry_redaction_matches helper skips true duplicates while letting a corrected reason fall through. - REDACT against an unknown msgid only inserted an in-memory notice. Persist via scrollback_db_save with NULL msgid so it survives a restart without colliding with later chathistory results. - Redact-X on a PENDING entry sent the "pending:<label>" placeholder msgid; server replied UNKNOWN_MSGID. Queue via a new pending_redact bit (strike-through visual), fire from fe_confirm_entry once the echo binds the real msgid. Wired into both the hover X and the right-click "Delete Message" menu. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f217b5e commit a7bb0b3

6 files changed

Lines changed: 184 additions & 17 deletions

File tree

src/common/text.c

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,11 @@ scrollback_confirm_pending (session *sess, const char *label, const char *real_m
157157
g_free (pending_key);
158158
}
159159

160-
/* IRCv3 redaction: mark message as redacted in scrollback */
160+
/* IRCv3 redaction: mark message as redacted in scrollback.
161+
* If the target msgid isn't in scrollback (we never saw the original),
162+
* persist a standalone notice line so the redaction event survives a
163+
* restart — matching the in-memory notice fe_redact_message inserts
164+
* when gtk_xtext_find_by_msgid returns NULL. */
161165
void
162166
scrollback_redact_for_session (session *sess, const char *msgid,
163167
const char *redacted_by, const char *reason,
@@ -169,8 +173,26 @@ scrollback_redact_for_session (session *sess, const char *msgid,
169173
return;
170174

171175
db = get_scrollback_db (sess);
172-
if (db)
173-
scrollback_redact_message (db, msgid, redacted_by, reason, redact_time);
176+
if (!db)
177+
return;
178+
179+
if (scrollback_redact_message (db, msgid, redacted_by, reason, redact_time))
180+
return;
181+
182+
/* Original wasn't in the DB — save a notice keyed by NULL msgid so it
183+
* never collides with a later chathistory-fetched original. */
184+
if (sess->channel[0] && redacted_by && *redacted_by)
185+
{
186+
char *notice;
187+
if (reason && *reason)
188+
notice = g_strdup_printf ("\017[Message redacted by %s: %s]",
189+
redacted_by, reason);
190+
else
191+
notice = g_strdup_printf ("\017[Message redacted by %s]",
192+
redacted_by);
193+
scrollback_db_save (db, sess->channel, redact_time, NULL, notice);
194+
g_free (notice);
195+
}
174196
}
175197

176198
/* IRCv3 reactions: persist to scrollback */

src/fe-gtk/fe-gtk.c

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1418,7 +1418,13 @@ fe_redact_message (session *sess, const char *msgid,
14181418
gtk_xtext_entry_set_reply (buf, notice_ent, msgid, NULL, NULL, 0);
14191419
return;
14201420
}
1421-
if (gtk_xtext_entry_get_state (ent) == XTEXT_STATE_REDACTED)
1421+
1422+
/* Duplicate suppression: skip if existing redaction matches the new
1423+
* one exactly. Without this, a server retransmit (chathistory replay,
1424+
* rejoin) would kick the user out of REVEALED state back to REDACTED.
1425+
* A truly-changed redaction (different reason, different op) falls
1426+
* through and refreshes the placeholder + redaction info. */
1427+
if (gtk_xtext_entry_redaction_matches (ent, redacted_by, reason, redact_time))
14221428
return;
14231429

14241430
/* Preserve original content for accountability */
@@ -1541,9 +1547,24 @@ fe_confirm_entry (session *sess, guint64 entry_id, const char *msgid)
15411547
textentry *ent = gtk_xtext_find_by_id (sess->res->buffer, entry_id);
15421548
if (ent && gtk_xtext_entry_get_state (ent) == XTEXT_STATE_PENDING)
15431549
{
1550+
gboolean wants_redact = gtk_xtext_entry_get_pending_redact (ent);
1551+
15441552
gtk_xtext_entry_set_state (sess->res->buffer, ent, XTEXT_STATE_NORMAL);
15451553
if (msgid)
15461554
gtk_xtext_set_msgid (sess->res->buffer, ent, msgid);
1555+
1556+
/* User clicked the redact button while the entry was still
1557+
* pending; we deferred the REDACT until the real msgid was
1558+
* known. Fire it now. */
1559+
if (wants_redact && msgid && sess->channel[0] &&
1560+
sess->server && sess->server->have_redact &&
1561+
sess->server->connected)
1562+
{
1563+
char *cmd = g_strdup_printf ("REDACT %s %s", sess->channel, msgid);
1564+
handle_command (sess, cmd, FALSE);
1565+
g_free (cmd);
1566+
}
1567+
gtk_xtext_entry_set_pending_redact (ent, FALSE);
15471568
}
15481569
/* Virtual scrollback: if the entry was evicted (find_by_id returns NULL),
15491570
* the DB-side msgid update is already handled by scrollback_confirm_pending

src/fe-gtk/maingui.c

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2846,9 +2846,22 @@ mg_redact_button_cb (GtkXText *xtext, const char *msgid, const char *nick, gpoin
28462846
if (xtext->redact_confirm_ent == xtext->hover_ent &&
28472847
(g_get_monotonic_time () - xtext->redact_confirm_time) < 3 * G_USEC_PER_SEC)
28482848
{
2849-
char *cmd = g_strdup_printf ("REDACT %s %s", sess->channel, msgid);
2850-
handle_command (sess, cmd, FALSE);
2851-
g_free (cmd);
2849+
/* Entry still awaiting echo/labeled-response — msgid is a
2850+
* "pending:<label>" placeholder the server doesn't know. Mark
2851+
* the entry and let fe_confirm_entry fire REDACT once the real
2852+
* msgid binds. */
2853+
if (gtk_xtext_entry_get_state (xtext->hover_ent) == XTEXT_STATE_PENDING)
2854+
{
2855+
gtk_xtext_entry_set_pending_redact (xtext->hover_ent, TRUE);
2856+
fe_toast_show (sess, _("Will delete once the message is confirmed by the server"),
2857+
3000, TOAST_TYPE_INFO, 0);
2858+
}
2859+
else
2860+
{
2861+
char *cmd = g_strdup_printf ("REDACT %s %s", sess->channel, msgid);
2862+
handle_command (sess, cmd, FALSE);
2863+
g_free (cmd);
2864+
}
28522865
xtext->redact_confirm_ent = NULL;
28532866
}
28542867
else

src/fe-gtk/menu.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1465,12 +1465,30 @@ static void
14651465
middle_action_redact (GSimpleAction *action, GVariant *parameter, gpointer user_data)
14661466
{
14671467
session *sess = middle_menu_sess;
1468+
textentry *ent;
14681469
(void)action; (void)parameter; (void)user_data;
14691470

14701471
if (!sess || !sess->server || !sess->server->have_redact ||
14711472
!sess->server->connected || !middle_menu_clicked_msgid)
14721473
return;
14731474

1475+
/* Same pending-echo gate as the hover-X path (maingui.c): if the
1476+
* entry is still awaiting its real msgid, queue the redact and let
1477+
* fe_confirm_entry fire it once the echo lands. */
1478+
ent = sess->res && sess->res->buffer
1479+
? gtk_xtext_find_by_msgid (sess->res->buffer, middle_menu_clicked_msgid)
1480+
: NULL;
1481+
if (ent && gtk_xtext_entry_get_state (ent) == XTEXT_STATE_PENDING)
1482+
{
1483+
gtk_xtext_entry_set_pending_redact (ent, TRUE);
1484+
fe_toast_show (sess,
1485+
_("Will delete once the message is confirmed by the server"),
1486+
3000, TOAST_TYPE_INFO, 0);
1487+
if (sess->gui && sess->gui->xtext)
1488+
gtk_widget_queue_draw (sess->gui->xtext);
1489+
return;
1490+
}
1491+
14741492
{
14751493
char *cmd = g_strdup_printf ("REDACT %s %s", sess->channel, middle_menu_clicked_msgid);
14761494
handle_command (sess, cmd, FALSE);

src/fe-gtk/xtext.c

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ struct textentry
212212
unsigned int collapsed:1; /* multiline entry is currently collapsed */
213213
unsigned int collapsible:1; /* multiline entry can be collapsed/expanded */
214214
unsigned int has_db_row:1; /* entry was saved to DB (has valid DB rowid) */
215+
unsigned int pending_redact:1; /* user clicked redact while pending; fire on echo */
215216

216217
int display_lines; /* cached display line count (replaces g_slist_length in hot paths) */
217218
int sublines_width; /* window_width when sublines were computed (0 = needs recompute) */
@@ -4526,6 +4527,18 @@ gtk_xtext_render_subline (GtkXText *xtext, int y, textentry *ent,
45264527
pango_attr_list_insert (attrs, fg_alpha);
45274528
}
45284529

4530+
/* Queued-redact strike-through: the user clicked delete while we were
4531+
* still awaiting echo confirmation. Strike the whole subline so the
4532+
* intent is visible until the echo arrives and the real REDACT goes
4533+
* out. Combines with the pending dim above. */
4534+
if (ent->pending_redact)
4535+
{
4536+
PangoAttribute *strike = pango_attr_strikethrough_new (TRUE);
4537+
strike->start_index = 0;
4538+
strike->end_index = sub_len;
4539+
pango_attr_list_insert (attrs, strike);
4540+
}
4541+
45294542
pango_layout_set_text (xtext->layout,
45304543
(char *)(ent->stripped_str + sub_start), sub_len);
45314544
pango_layout_set_attributes (xtext->layout, attrs);
@@ -11178,7 +11191,7 @@ gboolean
1117811191
gtk_xtext_entry_set_text (xtext_buffer *buf, textentry *ent,
1117911192
const unsigned char *new_text, int new_len)
1118011193
{
11181-
int old_sublines, new_sublines;
11194+
int old_dl, delta;
1118211195

1118311196
if (!buf || !ent || !new_text)
1118411197
return FALSE;
@@ -11226,11 +11239,33 @@ gtk_xtext_entry_set_text (xtext_buffer *buf, textentry *ent,
1122611239
ent->raw_to_stripped_map = r2s;
1122711240
}
1122811241

11229-
/* Recalculate derived data */
11242+
/* Recalculate derived data. gtk_xtext_lines_taken mutates
11243+
* ent->display_lines; bracket the call so we can apply the delta to
11244+
* both buf->num_lines AND the order-statistic tree's cached weights.
11245+
* Skipping the tree update leaves index234_by_weight out of sync with
11246+
* display_lines and breaks every code path that resolves a scroll
11247+
* line back to an entry (gtk_xtext_nth, char/click hit-testing). */
1123011248
ent->str_width = gtk_xtext_text_width_ent (buf->xtext, ent);
11231-
old_sublines = g_slist_length (ent->sublines);
11232-
new_sublines = gtk_xtext_lines_taken (buf, ent);
11233-
buf->num_lines += (new_sublines - old_sublines);
11249+
11250+
/* Force lines_taken to rebuild sublines from scratch. Its fast path
11251+
* reuses ent->sublines without checking that the single GINT boundary
11252+
* inside matches the new ent->str_len; on a redaction whose placeholder
11253+
* is longer than the original message, the stale boundary makes the
11254+
* render loop's `str < ent->str + str_len` advance recycle past the
11255+
* end, truncating the placeholder and burning the line budget so the
11256+
* entries below don't render. */
11257+
g_slist_free (ent->sublines);
11258+
ent->sublines = NULL;
11259+
11260+
old_dl = ent->display_lines;
11261+
gtk_xtext_lines_taken (buf, ent);
11262+
delta = ent->display_lines - old_dl;
11263+
if (delta)
11264+
{
11265+
buf->num_lines += delta;
11266+
if (buf->entry_tree)
11267+
update_weight234 (buf->entry_tree, ent, delta);
11268+
}
1123411269

1123511270
/* Invalidate search marks */
1123611271
if (ent->marks)
@@ -11302,17 +11337,60 @@ gtk_xtext_entry_set_redaction_info (xtext_buffer *buf, textentry *ent,
1130211337
const char *redacted_by, const char *reason,
1130311338
time_t redact_time)
1130411339
{
11305-
if (!ent || ent->redaction)
11306-
return; /* already has redaction info — don't overwrite */
11340+
if (!ent)
11341+
return;
11342+
11343+
if (!ent->redaction)
11344+
{
11345+
/* First redaction: snapshot the original content for reveal. */
11346+
ent->redaction = g_new0 (xtext_redaction_info, 1);
11347+
ent->redaction->original_content = g_strndup (original_str, original_len);
11348+
ent->redaction->original_len = original_len;
11349+
}
11350+
/* Re-redaction: keep the first-call original_content (ent->str may be
11351+
* the prompt or placeholder at this point) and only refresh the
11352+
* redactor identity so the latest REDACT wins. */
1130711353

11308-
ent->redaction = g_new0 (xtext_redaction_info, 1);
11309-
ent->redaction->original_content = g_strndup (original_str, original_len);
11310-
ent->redaction->original_len = original_len;
11354+
g_free (ent->redaction->redacted_by);
1131111355
ent->redaction->redacted_by = g_strdup (redacted_by);
11356+
11357+
g_free (ent->redaction->redaction_reason);
1131211358
ent->redaction->redaction_reason = (reason && *reason) ? g_strdup (reason) : NULL;
11359+
1131311360
ent->redaction->redaction_time = redact_time;
1131411361
}
1131511362

11363+
gboolean
11364+
gtk_xtext_entry_redaction_matches (textentry *ent,
11365+
const char *redacted_by,
11366+
const char *reason,
11367+
time_t redact_time)
11368+
{
11369+
const char *normalized;
11370+
11371+
if (!ent || !ent->redaction)
11372+
return FALSE;
11373+
11374+
normalized = (reason && *reason) ? reason : NULL;
11375+
11376+
return g_strcmp0 (ent->redaction->redacted_by, redacted_by) == 0
11377+
&& g_strcmp0 (ent->redaction->redaction_reason, normalized) == 0
11378+
&& ent->redaction->redaction_time == redact_time;
11379+
}
11380+
11381+
void
11382+
gtk_xtext_entry_set_pending_redact (textentry *ent, gboolean pending)
11383+
{
11384+
if (ent)
11385+
ent->pending_redact = pending ? 1 : 0;
11386+
}
11387+
11388+
gboolean
11389+
gtk_xtext_entry_get_pending_redact (textentry *ent)
11390+
{
11391+
return ent ? (gboolean) ent->pending_redact : FALSE;
11392+
}
11393+
1131611394
/* Lookup the textentry at a given y pixel coordinate.
1131711395
* Used by context menus to identify which message was clicked.
1131811396
*/

src/fe-gtk/xtext.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,21 @@ void gtk_xtext_entry_set_redaction_info (xtext_buffer *buf, textentry *ent,
570570
const char *redacted_by, const char *reason,
571571
time_t redact_time);
572572

573+
/* TRUE if ent already carries redaction info that matches the given
574+
* (redacted_by, reason, redact_time). An empty/NULL reason is treated
575+
* equivalently. Used to suppress duplicate REDACT events. */
576+
gboolean gtk_xtext_entry_redaction_matches (textentry *ent,
577+
const char *redacted_by,
578+
const char *reason,
579+
time_t redact_time);
580+
581+
/* Mark ent as "redact-on-confirm": user clicked the redact button while
582+
* the entry was still PENDING. The flag is read and cleared by
583+
* fe_confirm_entry once the echo/labeled-response arrives and a real
584+
* msgid is bound to the entry. */
585+
void gtk_xtext_entry_set_pending_redact (textentry *ent, gboolean pending);
586+
gboolean gtk_xtext_entry_get_pending_redact (textentry *ent);
587+
573588
/* IRCv3 reactions and reply context */
574589
struct xtext_reactions_info;
575590
struct xtext_reply_info;

0 commit comments

Comments
 (0)