Improve message-list interactions: empty-state guidance, jump-to-latest, per-message actions (#1359)

* Improve message-list interactions: empty-state guidance, jump-to-latest, per-message actions

Three usability gaps in the message list, all presentation-layer:

- Empty timeline was a blank screen. It now narrates itself in dim,
  terminal-styled lines: what the channel is, that it's waiting for
  peers, and where the channel switcher and help live. Disappears with
  the first message.

- Scrolled up in a busy channel, nothing signalled that new messages
  arrived and there was no way back. A small "jump to latest" pill now
  appears while scrolled up, counting messages that arrived below, and
  taps back to the newest via the existing scroll helper. The unseen
  count re-baselines on channel switch so a cross-channel count delta is
  never shown as "new".

- A single tap anywhere on a message overwrote the composer draft with
  "@sender " and force-focused the field — casual taps while reading
  destroyed drafts. That whole-row tap is removed; mention/DM/hug/slap/
  block now live in the per-message context menu (reusing the handlers
  the existing action sheet already calls), and mention appends to the
  draft rather than replacing it. A failed own private message gets a
  resend item. The triple-tap-to-clear gesture gains a confirmation.

New strings are added source-language (en) only.

* Remove the failed original when resending a private message

Resend re-submitted the content but left the red failed bubble in
place, so every tap stacked another copy under it. Route resend
through ConversationUIModel, which drops the failed original from the
conversation store (removePrivateMessage) before sending the new copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Count only rendered human messages in the jump-to-latest pill

The unseen count was a raw delta of the messages array, so system
lines (join/leave narration) and whitespace-only messages that never
render as rows inflated the "N new" pill. Baseline the counters
against the number of messages that render as human message rows,
using the same predicates the row builder applies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Hide mention/DM context-menu actions inside 1:1 conversations

In a private conversation, mentioning the only other participant is
noise and the DM action just reopens the already-open conversation
(toggling the sidebar). Gate both behind privatePeer == nil so the
public-timeline context menu is unchanged; hug/slap/block/copy/resend
remain in DMs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: jack <212554440+jackjackbits@users.noreply.github.com>
Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hot Pixel Group
2026-07-06 10:09:04 +02:00
committed by GitHub
co-authored by jack jack Claude Fable 5
parent 0d251ad20c
commit 7a0c821807
3 changed files with 353 additions and 22 deletions
+108
View File
@@ -9144,6 +9144,18 @@
}
}
},
"content.accessibility.jump_to_latest" : {
"comment" : "Accessibility label for the jump to latest messages button",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "jump to latest messages"
}
}
}
},
"content.accessibility.location_channels" : {
"extractionState" : "manual",
"localizations" : {
@@ -12571,6 +12583,18 @@
}
}
},
"content.actions.resend" : {
"comment" : "Context menu action that resends a failed private message",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "resend"
}
}
}
},
"content.actions.slap" : {
"extractionState" : "manual",
"localizations" : {
@@ -14182,6 +14206,30 @@
}
}
},
"content.clear.confirm_action" : {
"comment" : "Destructive confirmation button that clears the current chat",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "clear chat"
}
}
}
},
"content.clear.confirm_title" : {
"comment" : "Title of the confirmation dialog shown before clearing the current chat",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "clear this chat?"
}
}
}
},
"content.commands.block" : {
"extractionState" : "manual",
"localizations" : {
@@ -17488,6 +17536,54 @@
}
}
},
"content.empty.location_intro" : {
"comment" : "First line of an empty geohash timeline naming the channel",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "you're in #%@ — a public location channel over the internet"
}
}
}
},
"content.empty.mesh_intro" : {
"comment" : "First line of the empty mesh timeline explaining what the mesh channel is",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "you're on #mesh — reaches people within bluetooth range"
}
}
}
},
"content.empty.mesh_waiting" : {
"comment" : "Second line of the empty mesh timeline saying no peers are in range yet",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "nobody in range yet... messages appear here"
}
}
}
},
"content.empty.switch_hint" : {
"comment" : "Empty timeline hint pointing at the channel switcher and the help screen",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "tap the channel name above to switch · tap bitchat/ for help"
}
}
}
},
"content.header.people" : {
"extractionState" : "manual",
"localizations" : {
@@ -18085,6 +18181,18 @@
}
}
},
"content.jump.new_count" : {
"comment" : "Count of messages that arrived while scrolled up, shown in the jump-to-latest pill",
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld new"
}
}
}
},
"content.location.enable" : {
"extractionState" : "manual",
"localizations" : {