Improve message pane interactions and editing
This commit is contained in:
193
src/app.cpp
193
src/app.cpp
@@ -6,6 +6,7 @@
|
|||||||
#include <climits>
|
#include <climits>
|
||||||
#include <cwchar>
|
#include <cwchar>
|
||||||
#include <cwctype>
|
#include <cwctype>
|
||||||
|
#include <set>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
#include <curses.h>
|
#include <curses.h>
|
||||||
@@ -58,6 +59,8 @@ std::optional<int> mapped_layout_hotkey(wchar_t ch) {
|
|||||||
return 'i';
|
return 'i';
|
||||||
case L'к':
|
case L'к':
|
||||||
return 'r';
|
return 'r';
|
||||||
|
case L'щ':
|
||||||
|
return 'o';
|
||||||
default:
|
default:
|
||||||
return std::nullopt;
|
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)];
|
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) {
|
std::string utf8_from_wchar(wchar_t ch) {
|
||||||
char buffer[MB_LEN_MAX] = {};
|
char buffer[MB_LEN_MAX] = {};
|
||||||
std::mbstate_t state = std::mbstate_t{};
|
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 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);
|
attron(attrs);
|
||||||
mvaddnstr(y, x, text.c_str(), count);
|
mvaddnstr(y, x, text.c_str(), static_cast<int>(count));
|
||||||
attroff(attrs);
|
attroff(attrs);
|
||||||
x += count;
|
x += used_width;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string entity_prefix(const json& type) {
|
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_;
|
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 {
|
std::tuple<bool, std::int64_t, std::string> App::parse_compose_command(const std::string& value) const {
|
||||||
if (!starts_with_at(value, 0, ">r ")) {
|
if (!starts_with_at(value, 0, ">r ")) {
|
||||||
return {false, 0, value};
|
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;
|
const ChatInfo& chat = chat_it->second;
|
||||||
std::int64_t message_id = parsed_value;
|
return {true, resolve_message_ref(chat, parsed_value), value.substr(id_end)};
|
||||||
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;
|
|
||||||
|
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 {
|
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();
|
start_reply_to_latest_message();
|
||||||
}
|
}
|
||||||
return;
|
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':
|
case 'i':
|
||||||
if (!authorized_) {
|
if (!authorized_) {
|
||||||
status_line_ = "Finish login first.";
|
status_line_ = "Finish login first.";
|
||||||
@@ -902,16 +990,20 @@ void App::handle_key(int ch) {
|
|||||||
if (focus_ == FocusPane::Chats && selected_chat_index_ > 0) {
|
if (focus_ == FocusPane::Chats && selected_chat_index_ > 0) {
|
||||||
--selected_chat_index_;
|
--selected_chat_index_;
|
||||||
} else if (focus_ == FocusPane::Messages) {
|
} else if (focus_ == FocusPane::Messages) {
|
||||||
|
if (!move_message_attachment_selection(-1)) {
|
||||||
++message_scroll_;
|
++message_scroll_;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
case KEY_DOWN:
|
case KEY_DOWN:
|
||||||
if (focus_ == FocusPane::Chats &&
|
if (focus_ == FocusPane::Chats &&
|
||||||
selected_chat_index_ + 1 < static_cast<int>(sorted_chat_ids_.size())) {
|
selected_chat_index_ + 1 < static_cast<int>(sorted_chat_ids_.size())) {
|
||||||
++selected_chat_index_;
|
++selected_chat_index_;
|
||||||
} else if (focus_ == FocusPane::Messages && message_scroll_ > 0) {
|
} else if (focus_ == FocusPane::Messages) {
|
||||||
|
if (!move_message_attachment_selection(1) && message_scroll_ > 0) {
|
||||||
--message_scroll_;
|
--message_scroll_;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
case KEY_PPAGE:
|
case KEY_PPAGE:
|
||||||
message_scroll_ += 10;
|
message_scroll_ += 10;
|
||||||
@@ -1227,6 +1319,7 @@ void App::draw_help_menu(int height, int width) {
|
|||||||
" Enter Open selected chat",
|
" Enter Open selected chat",
|
||||||
" r Reload chats or current chat history",
|
" r Reload chats or current chat history",
|
||||||
" a Reply to the latest message",
|
" a Reply to the latest message",
|
||||||
|
" o Open the latest attachment in the current chat",
|
||||||
" i Compose message",
|
" i Compose message",
|
||||||
" Tab Switch focus",
|
" Tab Switch focus",
|
||||||
" PgUp/PgDn Scroll messages",
|
" PgUp/PgDn Scroll messages",
|
||||||
@@ -1433,18 +1526,30 @@ void App::draw_message_pane(int top, int height, int left, int width) {
|
|||||||
|
|
||||||
struct RenderLine {
|
struct RenderLine {
|
||||||
bool is_day_separator = false;
|
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 timestamp;
|
||||||
std::string message_id;
|
std::string message_id;
|
||||||
std::string sender;
|
std::string sender;
|
||||||
std::string meta;
|
std::string meta;
|
||||||
std::string reply_to;
|
std::string reply_prefix;
|
||||||
|
std::string reply_ref;
|
||||||
std::string body;
|
std::string body;
|
||||||
|
std::string attachment_hint;
|
||||||
std::string state;
|
std::string state;
|
||||||
bool is_continuation = false;
|
bool is_continuation = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<RenderLine> lines;
|
std::vector<RenderLine> lines;
|
||||||
std::string current_day;
|
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) {
|
for (const auto& message : chat.messages) {
|
||||||
const std::string message_day = format_date(message.date);
|
const std::string message_day = format_date(message.date);
|
||||||
if (message_day != current_day) {
|
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 timestamp = "[" + format_time(message.date) + "]";
|
||||||
const std::string message_id = "[" + std::to_string(&message - &chat.messages[0] + 1) + "]";
|
const std::string message_id = "[" + std::to_string(&message - &chat.messages[0] + 1) + "]";
|
||||||
const std::string reply_to =
|
const std::string reply_prefix = message.reply_to_message_id != 0 ? "reply " : "";
|
||||||
message.reply_to_message_id != 0 ? ("reply " + format_message_ref(chat, message.reply_to_message_id)) : "";
|
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 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;
|
const int reserved_state_width = static_cast<int>(state.size()) + 1;
|
||||||
std::string prefix = timestamp + " " + message_id + " " + message.sender + ": ";
|
std::string prefix = timestamp + " " + message_id + " " + message.sender + ": ";
|
||||||
if (!meta.empty()) {
|
if (!meta.empty()) {
|
||||||
prefix += meta + " ";
|
prefix += meta + " ";
|
||||||
}
|
}
|
||||||
if (!reply_to.empty()) {
|
if (!reply_ref.empty()) {
|
||||||
prefix += reply_to + " ";
|
prefix += reply_prefix + reply_ref + " ";
|
||||||
}
|
}
|
||||||
const int wrap_width = std::max(10, width - 3 - reserved_state_width);
|
const int wrap_width = std::max(10, width - 3 - reserved_state_width);
|
||||||
std::vector<std::string> wrapped =
|
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()) {
|
if (wrapped.empty()) {
|
||||||
RenderLine line;
|
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.timestamp = timestamp;
|
||||||
line.message_id = message_id;
|
line.message_id = message_id;
|
||||||
line.sender = message.sender;
|
line.sender = message.sender;
|
||||||
line.meta = meta;
|
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;
|
line.state = state;
|
||||||
lines.push_back(line);
|
lines.push_back(line);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderLine first_line;
|
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.timestamp = timestamp;
|
||||||
first_line.message_id = message_id;
|
first_line.message_id = message_id;
|
||||||
first_line.sender = message.sender;
|
first_line.sender = message.sender;
|
||||||
first_line.meta = meta;
|
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.body = wrapped.front();
|
||||||
|
first_line.attachment_hint = attachment_hint;
|
||||||
first_line.state = state;
|
first_line.state = state;
|
||||||
lines.push_back(first_line);
|
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 available_rows = std::max(1, height - 2);
|
||||||
const int max_scroll = std::max(0, static_cast<int>(lines.size()) - available_rows);
|
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) {
|
if (message_scroll_ > max_scroll) {
|
||||||
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) {
|
for (int row = 0; row < available_rows; ++row) {
|
||||||
const int line_index = first_line + row;
|
const int line_index = first_line + row;
|
||||||
if (line_index >= static_cast<int>(lines.size())) {
|
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 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 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, line.timestamp, COLOR_PAIR(kColorPairTimestamp));
|
||||||
draw_colored_span(y, x, content_max_x, " ", A_NORMAL);
|
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, " ", 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, line.sender, COLOR_PAIR(sender_color_pair(line.sender)) | A_BOLD);
|
||||||
draw_colored_span(y, x, content_max_x, ": ", A_NORMAL);
|
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, line.meta, COLOR_PAIR(kColorPairMarkdown) | A_DIM);
|
||||||
draw_colored_span(y, x, content_max_x, " ", A_NORMAL);
|
draw_colored_span(y, x, content_max_x, " ", A_NORMAL);
|
||||||
}
|
}
|
||||||
if (!line.reply_to.empty()) {
|
if (!line.reply_ref.empty()) {
|
||||||
draw_colored_span(y, x, content_max_x, line.reply_to, COLOR_PAIR(kColorPairMarkdown) | A_DIM);
|
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);
|
draw_colored_span(y, x, content_max_x, " ", A_NORMAL);
|
||||||
}
|
}
|
||||||
int state_draw_x = state_x;
|
int state_draw_x = state_x;
|
||||||
draw_colored_span(y, state_draw_x, max_x, line.state, state_attrs);
|
draw_colored_span(y, state_draw_x, max_x, line.state, state_attrs);
|
||||||
draw_message_body(y, x, content_max_x, line.body);
|
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 {
|
} else {
|
||||||
draw_colored_span(y, x, max_x, " ", A_NORMAL);
|
|
||||||
draw_message_body(y, x, max_x, line.body);
|
draw_message_body(y, x, max_x, line.body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class App {
|
|||||||
void clear_input();
|
void clear_input();
|
||||||
void submit_input();
|
void submit_input();
|
||||||
void send_message(std::int64_t chat_id, const std::string& text, std::optional<std::int64_t> reply_to_message_id = std::nullopt);
|
void send_message(std::int64_t chat_id, const std::string& text, std::optional<std::int64_t> reply_to_message_id = std::nullopt);
|
||||||
|
void edit_message(std::int64_t chat_id, std::int64_t message_id, const std::string& text);
|
||||||
void forward_message(
|
void forward_message(
|
||||||
std::int64_t source_chat_id,
|
std::int64_t source_chat_id,
|
||||||
const std::vector<std::int64_t>& message_ids,
|
const std::vector<std::int64_t>& message_ids,
|
||||||
@@ -82,11 +83,16 @@ class App {
|
|||||||
void render_attachment_preview_graphics(int top, int left, int width, int height);
|
void render_attachment_preview_graphics(int top, int left, int width, int height);
|
||||||
void reset_attachment_viewer_send_preview();
|
void reset_attachment_viewer_send_preview();
|
||||||
[[nodiscard]] std::vector<std::int64_t> forward_target_chat_ids() const;
|
[[nodiscard]] std::vector<std::int64_t> forward_target_chat_ids() const;
|
||||||
|
void sync_message_attachment_selection();
|
||||||
|
bool move_message_attachment_selection(int delta);
|
||||||
|
[[nodiscard]] std::optional<AttachmentInfo> selected_message_attachment() const;
|
||||||
[[nodiscard]] std::optional<AttachmentInfo> selected_attachment() const;
|
[[nodiscard]] std::optional<AttachmentInfo> selected_attachment() const;
|
||||||
[[nodiscard]] std::string render_attachment_preview(const AttachmentInfo& attachment, int width, int height) const;
|
[[nodiscard]] std::string render_attachment_preview(const AttachmentInfo& attachment, int width, int height) const;
|
||||||
[[nodiscard]] std::string build_attachment_preview_graphics(const AttachmentInfo& attachment, int width, int height) const;
|
[[nodiscard]] std::string build_attachment_preview_graphics(const AttachmentInfo& attachment, int width, int height) const;
|
||||||
[[nodiscard]] std::tuple<bool, std::int64_t, std::string> parse_compose_command(const std::string& value) const;
|
[[nodiscard]] std::tuple<bool, std::int64_t, std::string> parse_compose_command(const std::string& value) const;
|
||||||
|
[[nodiscard]] std::tuple<bool, std::int64_t, std::string> parse_edit_command(const std::string& value) const;
|
||||||
[[nodiscard]] std::optional<std::vector<std::int64_t>> parse_forward_command(const std::string& value) const;
|
[[nodiscard]] std::optional<std::vector<std::int64_t>> parse_forward_command(const std::string& value) const;
|
||||||
|
[[nodiscard]] std::int64_t resolve_message_ref(const ChatInfo& chat, std::int64_t ref) const;
|
||||||
[[nodiscard]] std::optional<std::size_t> find_message_index(const ChatInfo& chat, std::int64_t message_id) const;
|
[[nodiscard]] std::optional<std::size_t> find_message_index(const ChatInfo& chat, std::int64_t message_id) const;
|
||||||
[[nodiscard]] std::string format_message_ref(const ChatInfo& chat, std::int64_t message_id) const;
|
[[nodiscard]] std::string format_message_ref(const ChatInfo& chat, std::int64_t message_id) const;
|
||||||
[[nodiscard]] std::string sender_label(const json& sender) const;
|
[[nodiscard]] std::string sender_label(const json& sender) const;
|
||||||
@@ -150,6 +156,7 @@ class App {
|
|||||||
int forward_target_index_ = 0;
|
int forward_target_index_ = 0;
|
||||||
std::int64_t open_chat_id_ = 0;
|
std::int64_t open_chat_id_ = 0;
|
||||||
std::int64_t tdlib_open_chat_id_ = 0;
|
std::int64_t tdlib_open_chat_id_ = 0;
|
||||||
|
std::int64_t message_attachment_message_id_ = 0;
|
||||||
std::int64_t my_user_id_ = 0;
|
std::int64_t my_user_id_ = 0;
|
||||||
std::int64_t forward_source_chat_id_ = 0;
|
std::int64_t forward_source_chat_id_ = 0;
|
||||||
std::size_t input_cursor_ = 0;
|
std::size_t input_cursor_ = 0;
|
||||||
|
|||||||
@@ -383,6 +383,99 @@ void App::reset_attachment_viewer_send_preview() {
|
|||||||
attachment_viewer_send_caption_.clear();
|
attachment_viewer_send_caption_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::sync_message_attachment_selection() {
|
||||||
|
const auto chat_id = open_chat_id();
|
||||||
|
if (!chat_id.has_value()) {
|
||||||
|
message_attachment_message_id_ = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto chat_it = chats_.find(*chat_id);
|
||||||
|
if (chat_it == chats_.end()) {
|
||||||
|
message_attachment_message_id_ = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& message : chat_it->second.messages) {
|
||||||
|
if (message.id == message_attachment_message_id_ && message.has_attachment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message_attachment_message_id_ = 0;
|
||||||
|
for (auto it = chat_it->second.messages.rbegin(); it != chat_it->second.messages.rend(); ++it) {
|
||||||
|
if (it->has_attachment) {
|
||||||
|
message_attachment_message_id_ = it->id;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool App::move_message_attachment_selection(int delta) {
|
||||||
|
if (delta == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_message_attachment_selection();
|
||||||
|
const auto chat_id = open_chat_id();
|
||||||
|
if (!chat_id.has_value()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto chat_it = chats_.find(*chat_id);
|
||||||
|
if (chat_it == chats_.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::int64_t> attachment_message_ids;
|
||||||
|
for (const auto& message : chat_it->second.messages) {
|
||||||
|
if (message.has_attachment) {
|
||||||
|
attachment_message_ids.push_back(message.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (attachment_message_ids.empty()) {
|
||||||
|
message_attachment_message_id_ = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto selected = std::find(
|
||||||
|
attachment_message_ids.begin(),
|
||||||
|
attachment_message_ids.end(),
|
||||||
|
message_attachment_message_id_);
|
||||||
|
if (selected == attachment_message_ids.end()) {
|
||||||
|
message_attachment_message_id_ = attachment_message_ids.back();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::ptrdiff_t index = selected - attachment_message_ids.begin();
|
||||||
|
const std::ptrdiff_t next_index = index + (delta < 0 ? -1 : 1);
|
||||||
|
if (next_index < 0 || next_index >= static_cast<std::ptrdiff_t>(attachment_message_ids.size())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
message_attachment_message_id_ = attachment_message_ids[static_cast<std::size_t>(next_index)];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AttachmentInfo> App::selected_message_attachment() const {
|
||||||
|
const auto chat_id = open_chat_id();
|
||||||
|
if (!chat_id.has_value()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto chat_it = chats_.find(*chat_id);
|
||||||
|
if (chat_it == chats_.end()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& message : chat_it->second.messages) {
|
||||||
|
if (message.id == message_attachment_message_id_ && message.has_attachment) {
|
||||||
|
return message.attachment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<AttachmentInfo> App::selected_attachment() const {
|
std::optional<AttachmentInfo> App::selected_attachment() const {
|
||||||
const auto chat_id = open_chat_id();
|
const auto chat_id = open_chat_id();
|
||||||
if (!chat_id.has_value()) {
|
if (!chat_id.has_value()) {
|
||||||
|
|||||||
@@ -356,6 +356,19 @@ void App::submit_input() {
|
|||||||
open_forward_target_menu(*chat_id, *message_ids);
|
open_forward_target_menu(*chat_id, *message_ids);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const auto [is_edit, edit_message_id, edit_text] = parse_edit_command(value);
|
||||||
|
if (is_edit) {
|
||||||
|
if (edit_message_id == 0) {
|
||||||
|
status_line_ = "Edit command needs a valid message ref.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (edit_text.empty()) {
|
||||||
|
status_line_ = "Edit text cannot be empty.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
edit_message(*chat_id, edit_message_id, edit_text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const auto [is_reply, reply_to_message_id, text] = parse_compose_command(value);
|
const auto [is_reply, reply_to_message_id, text] = parse_compose_command(value);
|
||||||
if (const auto clipboard_caption = parse_clipboard_compose_command(text); clipboard_caption.has_value()) {
|
if (const auto clipboard_caption = parse_clipboard_compose_command(text); clipboard_caption.has_value()) {
|
||||||
preview_clipboard_photo_message(
|
preview_clipboard_photo_message(
|
||||||
@@ -452,6 +465,22 @@ void App::send_message(
|
|||||||
status_line_ = "Message queued.";
|
status_line_ = "Message queued.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::edit_message(std::int64_t chat_id, std::int64_t message_id, const std::string& text) {
|
||||||
|
td_.send({
|
||||||
|
{"@type", "editMessageText"},
|
||||||
|
{"chat_id", chat_id},
|
||||||
|
{"message_id", message_id},
|
||||||
|
{"reply_markup", nullptr},
|
||||||
|
{"input_message_content",
|
||||||
|
{
|
||||||
|
{"@type", "inputMessageText"},
|
||||||
|
{"text", markdown_formatted_text(td_, text)},
|
||||||
|
{"clear_draft", true},
|
||||||
|
}},
|
||||||
|
});
|
||||||
|
status_line_ = "Editing message...";
|
||||||
|
}
|
||||||
|
|
||||||
void App::forward_message(
|
void App::forward_message(
|
||||||
std::int64_t source_chat_id,
|
std::int64_t source_chat_id,
|
||||||
const std::vector<std::int64_t>& message_ids,
|
const std::vector<std::int64_t>& message_ids,
|
||||||
|
|||||||
@@ -341,6 +341,9 @@ void App::set_open_chat(std::int64_t chat_id) {
|
|||||||
|
|
||||||
open_chat_id_ = chat_id;
|
open_chat_id_ = chat_id;
|
||||||
message_scroll_ = 0;
|
message_scroll_ = 0;
|
||||||
|
if (changed_chat) {
|
||||||
|
message_attachment_message_id_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (tdlib_open_chat_id_ != chat_id) {
|
if (tdlib_open_chat_id_ != chat_id) {
|
||||||
td_.send({
|
td_.send({
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ std::vector<std::string> wrap_text(const std::string& text, int width) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (static_cast<int>(current.size() + 1 + word.size()) > width) {
|
if (utf8_display_width(current) + 1 + utf8_display_width(word) > width) {
|
||||||
lines.push_back(current);
|
lines.push_back(current);
|
||||||
current = word;
|
current = word;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user