Improve message pane interactions and editing
This commit is contained in:
197
src/app.cpp
197
src/app.cpp
@@ -6,6 +6,7 @@
|
||||
#include <climits>
|
||||
#include <cwchar>
|
||||
#include <cwctype>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
#include <curses.h>
|
||||
@@ -58,6 +59,8 @@ std::optional<int> mapped_layout_hotkey(wchar_t ch) {
|
||||
return 'i';
|
||||
case L'к':
|
||||
return 'r';
|
||||
case L'щ':
|
||||
return 'o';
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -76,6 +79,19 @@ short sender_color_pair(const std::string& sender) {
|
||||
return sender_pairs[std::hash<std::string>{}(sender) % std::size(sender_pairs)];
|
||||
}
|
||||
|
||||
short message_id_color_pair(std::int64_t message_id) {
|
||||
static constexpr short id_pairs[] = {
|
||||
kColorPairSenderBlue,
|
||||
kColorPairSenderCyan,
|
||||
kColorPairSenderGreen,
|
||||
kColorPairSenderYellow,
|
||||
kColorPairSenderMagenta,
|
||||
kColorPairSenderRed,
|
||||
};
|
||||
|
||||
return id_pairs[std::hash<std::int64_t>{}(message_id) % std::size(id_pairs)];
|
||||
}
|
||||
|
||||
std::string utf8_from_wchar(wchar_t ch) {
|
||||
char buffer[MB_LEN_MAX] = {};
|
||||
std::mbstate_t state = std::mbstate_t{};
|
||||
@@ -183,11 +199,25 @@ void draw_colored_span(int y, int& x, int max_x, const std::string& text, chtype
|
||||
}
|
||||
|
||||
const int remaining = max_x - x;
|
||||
const int count = std::min(remaining, static_cast<int>(text.size()));
|
||||
std::size_t count = 0;
|
||||
int used_width = 0;
|
||||
while (count < text.size() && used_width < remaining) {
|
||||
const std::size_t next = utf8_next_index(text, count);
|
||||
const int next_width =
|
||||
utf8_display_width(text, next) - utf8_display_width(text, count);
|
||||
if (used_width + next_width > remaining) {
|
||||
break;
|
||||
}
|
||||
count = next;
|
||||
used_width += next_width;
|
||||
}
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
attron(attrs);
|
||||
mvaddnstr(y, x, text.c_str(), count);
|
||||
mvaddnstr(y, x, text.c_str(), static_cast<int>(count));
|
||||
attroff(attrs);
|
||||
x += count;
|
||||
x += used_width;
|
||||
}
|
||||
|
||||
std::string entity_prefix(const json& type) {
|
||||
@@ -408,6 +438,18 @@ std::optional<std::int64_t> App::open_chat_id() const {
|
||||
return open_chat_id_;
|
||||
}
|
||||
|
||||
std::int64_t App::resolve_message_ref(const ChatInfo& chat, std::int64_t ref) const {
|
||||
if (ref > 0 && ref <= static_cast<std::int64_t>(chat.messages.size())) {
|
||||
return chat.messages[static_cast<std::size_t>(ref - 1)].id;
|
||||
}
|
||||
for (const auto& message : chat.messages) {
|
||||
if (message.id == ref) {
|
||||
return message.id;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::tuple<bool, std::int64_t, std::string> App::parse_compose_command(const std::string& value) const {
|
||||
if (!starts_with_at(value, 0, ">r ")) {
|
||||
return {false, 0, value};
|
||||
@@ -446,12 +488,47 @@ std::tuple<bool, std::int64_t, std::string> App::parse_compose_command(const std
|
||||
}
|
||||
|
||||
const ChatInfo& chat = chat_it->second;
|
||||
std::int64_t message_id = parsed_value;
|
||||
if (parsed_value > 0 && parsed_value <= static_cast<std::int64_t>(chat.messages.size())) {
|
||||
message_id = chat.messages[static_cast<std::size_t>(parsed_value - 1)].id;
|
||||
return {true, resolve_message_ref(chat, parsed_value), value.substr(id_end)};
|
||||
}
|
||||
|
||||
std::tuple<bool, std::int64_t, std::string> App::parse_edit_command(const std::string& value) const {
|
||||
if (!starts_with_at(value, 0, ">e ")) {
|
||||
return {false, 0, value};
|
||||
}
|
||||
|
||||
return {true, message_id, value.substr(id_end)};
|
||||
std::size_t id_start = 3;
|
||||
while (id_start < value.size() && value[id_start] == ' ') {
|
||||
++id_start;
|
||||
}
|
||||
std::size_t id_end = id_start;
|
||||
while (id_end < value.size() && std::isdigit(static_cast<unsigned char>(value[id_end]))) {
|
||||
++id_end;
|
||||
}
|
||||
if (id_end == id_start) {
|
||||
return {true, 0, {}};
|
||||
}
|
||||
|
||||
std::int64_t parsed_value = 0;
|
||||
try {
|
||||
parsed_value = std::stoll(value.substr(id_start, id_end - id_start));
|
||||
} catch (...) {
|
||||
return {true, 0, {}};
|
||||
}
|
||||
|
||||
while (id_end < value.size() && value[id_end] == ' ') {
|
||||
++id_end;
|
||||
}
|
||||
|
||||
const auto chat_id = open_chat_id();
|
||||
if (!chat_id.has_value()) {
|
||||
return {true, 0, value.substr(id_end)};
|
||||
}
|
||||
const auto chat_it = chats_.find(*chat_id);
|
||||
if (chat_it == chats_.end()) {
|
||||
return {true, 0, value.substr(id_end)};
|
||||
}
|
||||
|
||||
return {true, resolve_message_ref(chat_it->second, parsed_value), value.substr(id_end)};
|
||||
}
|
||||
|
||||
std::optional<std::size_t> App::find_message_index(const ChatInfo& chat, std::int64_t message_id) const {
|
||||
@@ -886,6 +963,17 @@ void App::handle_key(int ch) {
|
||||
start_reply_to_latest_message();
|
||||
}
|
||||
return;
|
||||
case 'o':
|
||||
if (focus_ != FocusPane::Messages) {
|
||||
return;
|
||||
}
|
||||
sync_message_attachment_selection();
|
||||
if (const auto attachment = selected_message_attachment(); attachment.has_value()) {
|
||||
open_attachment(*attachment);
|
||||
} else {
|
||||
status_line_ = "No attachment available to open.";
|
||||
}
|
||||
return;
|
||||
case 'i':
|
||||
if (!authorized_) {
|
||||
status_line_ = "Finish login first.";
|
||||
@@ -902,15 +990,19 @@ void App::handle_key(int ch) {
|
||||
if (focus_ == FocusPane::Chats && selected_chat_index_ > 0) {
|
||||
--selected_chat_index_;
|
||||
} else if (focus_ == FocusPane::Messages) {
|
||||
++message_scroll_;
|
||||
if (!move_message_attachment_selection(-1)) {
|
||||
++message_scroll_;
|
||||
}
|
||||
}
|
||||
return;
|
||||
case KEY_DOWN:
|
||||
if (focus_ == FocusPane::Chats &&
|
||||
selected_chat_index_ + 1 < static_cast<int>(sorted_chat_ids_.size())) {
|
||||
++selected_chat_index_;
|
||||
} else if (focus_ == FocusPane::Messages && message_scroll_ > 0) {
|
||||
--message_scroll_;
|
||||
} else if (focus_ == FocusPane::Messages) {
|
||||
if (!move_message_attachment_selection(1) && message_scroll_ > 0) {
|
||||
--message_scroll_;
|
||||
}
|
||||
}
|
||||
return;
|
||||
case KEY_PPAGE:
|
||||
@@ -1227,6 +1319,7 @@ void App::draw_help_menu(int height, int width) {
|
||||
" Enter Open selected chat",
|
||||
" r Reload chats or current chat history",
|
||||
" a Reply to the latest message",
|
||||
" o Open the latest attachment in the current chat",
|
||||
" i Compose message",
|
||||
" Tab Switch focus",
|
||||
" PgUp/PgDn Scroll messages",
|
||||
@@ -1433,18 +1526,30 @@ void App::draw_message_pane(int top, int height, int left, int width) {
|
||||
|
||||
struct RenderLine {
|
||||
bool is_day_separator = false;
|
||||
bool is_selected_attachment = false;
|
||||
std::int64_t message_numeric_id = 0;
|
||||
std::int64_t reply_target_message_id = 0;
|
||||
std::string timestamp;
|
||||
std::string message_id;
|
||||
std::string sender;
|
||||
std::string meta;
|
||||
std::string reply_to;
|
||||
std::string reply_prefix;
|
||||
std::string reply_ref;
|
||||
std::string body;
|
||||
std::string attachment_hint;
|
||||
std::string state;
|
||||
bool is_continuation = false;
|
||||
};
|
||||
|
||||
std::vector<RenderLine> lines;
|
||||
std::string current_day;
|
||||
std::set<std::int64_t> replied_message_ids;
|
||||
sync_message_attachment_selection();
|
||||
for (const auto& message : chat.messages) {
|
||||
if (message.reply_to_message_id != 0) {
|
||||
replied_message_ids.insert(message.reply_to_message_id);
|
||||
}
|
||||
}
|
||||
for (const auto& message : chat.messages) {
|
||||
const std::string message_day = format_date(message.date);
|
||||
if (message_day != current_day) {
|
||||
@@ -1467,39 +1572,54 @@ void App::draw_message_pane(int top, int height, int left, int width) {
|
||||
|
||||
const std::string timestamp = "[" + format_time(message.date) + "]";
|
||||
const std::string message_id = "[" + std::to_string(&message - &chat.messages[0] + 1) + "]";
|
||||
const std::string reply_to =
|
||||
message.reply_to_message_id != 0 ? ("reply " + format_message_ref(chat, message.reply_to_message_id)) : "";
|
||||
const std::string reply_prefix = message.reply_to_message_id != 0 ? "reply " : "";
|
||||
const std::string reply_ref =
|
||||
message.reply_to_message_id != 0 ? format_message_ref(chat, message.reply_to_message_id) : "";
|
||||
const std::string meta = join_with_separator({message.forward_info, message.via_bot}, " ");
|
||||
const std::string attachment_hint =
|
||||
message.id == message_attachment_message_id_ ? " o to open" : "";
|
||||
const int reserved_state_width = static_cast<int>(state.size()) + 1;
|
||||
std::string prefix = timestamp + " " + message_id + " " + message.sender + ": ";
|
||||
if (!meta.empty()) {
|
||||
prefix += meta + " ";
|
||||
}
|
||||
if (!reply_to.empty()) {
|
||||
prefix += reply_to + " ";
|
||||
if (!reply_ref.empty()) {
|
||||
prefix += reply_prefix + reply_ref + " ";
|
||||
}
|
||||
const int wrap_width = std::max(10, width - 3 - reserved_state_width);
|
||||
std::vector<std::string> wrapped =
|
||||
wrap_text(message.text, std::max(10, wrap_width - static_cast<int>(prefix.size())));
|
||||
wrap_text(
|
||||
message.text,
|
||||
std::max(10, wrap_width - utf8_display_width(prefix) - utf8_display_width(attachment_hint)));
|
||||
if (wrapped.empty()) {
|
||||
RenderLine line;
|
||||
line.is_selected_attachment = message.id == message_attachment_message_id_;
|
||||
line.message_numeric_id = message.id;
|
||||
line.reply_target_message_id = message.reply_to_message_id;
|
||||
line.timestamp = timestamp;
|
||||
line.message_id = message_id;
|
||||
line.sender = message.sender;
|
||||
line.meta = meta;
|
||||
line.reply_to = reply_to;
|
||||
line.reply_prefix = reply_prefix;
|
||||
line.reply_ref = reply_ref;
|
||||
line.attachment_hint = attachment_hint;
|
||||
line.state = state;
|
||||
lines.push_back(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderLine first_line;
|
||||
first_line.is_selected_attachment = message.id == message_attachment_message_id_;
|
||||
first_line.message_numeric_id = message.id;
|
||||
first_line.reply_target_message_id = message.reply_to_message_id;
|
||||
first_line.timestamp = timestamp;
|
||||
first_line.message_id = message_id;
|
||||
first_line.sender = message.sender;
|
||||
first_line.meta = meta;
|
||||
first_line.reply_to = reply_to;
|
||||
first_line.reply_prefix = reply_prefix;
|
||||
first_line.reply_ref = reply_ref;
|
||||
first_line.body = wrapped.front();
|
||||
first_line.attachment_hint = attachment_hint;
|
||||
first_line.state = state;
|
||||
lines.push_back(first_line);
|
||||
|
||||
@@ -1518,10 +1638,28 @@ void App::draw_message_pane(int top, int height, int left, int width) {
|
||||
|
||||
const int available_rows = std::max(1, height - 2);
|
||||
const int max_scroll = std::max(0, static_cast<int>(lines.size()) - available_rows);
|
||||
int selected_attachment_line_index = -1;
|
||||
for (int i = 0; i < static_cast<int>(lines.size()); ++i) {
|
||||
if (lines[static_cast<std::size_t>(i)].is_selected_attachment) {
|
||||
selected_attachment_line_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (message_scroll_ > max_scroll) {
|
||||
message_scroll_ = max_scroll;
|
||||
}
|
||||
const int first_line = std::max(0, static_cast<int>(lines.size()) - available_rows - message_scroll_);
|
||||
int first_line = std::max(0, static_cast<int>(lines.size()) - available_rows - message_scroll_);
|
||||
if (selected_attachment_line_index >= 0) {
|
||||
if (selected_attachment_line_index < first_line) {
|
||||
message_scroll_ = std::max(0, static_cast<int>(lines.size()) - available_rows - selected_attachment_line_index);
|
||||
} else if (selected_attachment_line_index >= first_line + available_rows) {
|
||||
message_scroll_ = std::max(0, static_cast<int>(lines.size()) - selected_attachment_line_index - 1);
|
||||
}
|
||||
if (message_scroll_ > max_scroll) {
|
||||
message_scroll_ = max_scroll;
|
||||
}
|
||||
first_line = std::max(0, static_cast<int>(lines.size()) - available_rows - message_scroll_);
|
||||
}
|
||||
for (int row = 0; row < available_rows; ++row) {
|
||||
const int line_index = first_line + row;
|
||||
if (line_index >= static_cast<int>(lines.size())) {
|
||||
@@ -1557,9 +1695,12 @@ void App::draw_message_pane(int top, int height, int left, int width) {
|
||||
}
|
||||
const int state_x = std::max(left + 1, max_x - static_cast<int>(line.state.size()));
|
||||
const int content_max_x = std::max(left + 1, state_x - 1);
|
||||
const chtype message_id_attrs = replied_message_ids.find(line.message_numeric_id) != replied_message_ids.end()
|
||||
? (COLOR_PAIR(message_id_color_pair(line.message_numeric_id)) | A_BOLD)
|
||||
: (COLOR_PAIR(kColorPairMarkdown) | A_BOLD);
|
||||
draw_colored_span(y, x, content_max_x, line.timestamp, COLOR_PAIR(kColorPairTimestamp));
|
||||
draw_colored_span(y, x, content_max_x, " ", A_NORMAL);
|
||||
draw_colored_span(y, x, content_max_x, line.message_id, COLOR_PAIR(kColorPairMarkdown) | A_BOLD);
|
||||
draw_colored_span(y, x, content_max_x, line.message_id, message_id_attrs);
|
||||
draw_colored_span(y, x, content_max_x, " ", A_NORMAL);
|
||||
draw_colored_span(y, x, content_max_x, line.sender, COLOR_PAIR(sender_color_pair(line.sender)) | A_BOLD);
|
||||
draw_colored_span(y, x, content_max_x, ": ", A_NORMAL);
|
||||
@@ -1567,15 +1708,25 @@ void App::draw_message_pane(int top, int height, int left, int width) {
|
||||
draw_colored_span(y, x, content_max_x, line.meta, COLOR_PAIR(kColorPairMarkdown) | A_DIM);
|
||||
draw_colored_span(y, x, content_max_x, " ", A_NORMAL);
|
||||
}
|
||||
if (!line.reply_to.empty()) {
|
||||
draw_colored_span(y, x, content_max_x, line.reply_to, COLOR_PAIR(kColorPairMarkdown) | A_DIM);
|
||||
if (!line.reply_ref.empty()) {
|
||||
draw_colored_span(y, x, content_max_x, line.reply_prefix, COLOR_PAIR(kColorPairMarkdown) | A_DIM);
|
||||
draw_colored_span(
|
||||
y,
|
||||
x,
|
||||
content_max_x,
|
||||
line.reply_ref,
|
||||
replied_message_ids.find(line.reply_target_message_id) != replied_message_ids.end()
|
||||
? (COLOR_PAIR(message_id_color_pair(line.reply_target_message_id)) | A_BOLD)
|
||||
: (COLOR_PAIR(kColorPairMarkdown) | A_BOLD));
|
||||
draw_colored_span(y, x, content_max_x, " ", A_NORMAL);
|
||||
}
|
||||
int state_draw_x = state_x;
|
||||
draw_colored_span(y, state_draw_x, max_x, line.state, state_attrs);
|
||||
draw_message_body(y, x, content_max_x, line.body);
|
||||
if (!line.attachment_hint.empty()) {
|
||||
draw_colored_span(y, x, content_max_x, line.attachment_hint, A_DIM);
|
||||
}
|
||||
} else {
|
||||
draw_colored_span(y, x, max_x, " ", A_NORMAL);
|
||||
draw_message_body(y, x, max_x, line.body);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user