Refactor app modules and add clang-format
This commit is contained in:
14
.clang-format
Normal file
14
.clang-format
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
BasedOnStyle: LLVM
|
||||||
|
UseTab: ForIndentation
|
||||||
|
IndentWidth: 8
|
||||||
|
TabWidth: 8
|
||||||
|
ContinuationIndentWidth: 8
|
||||||
|
ColumnLimit: 100
|
||||||
|
SortIncludes: false
|
||||||
|
AlignConsecutiveAssignments: false
|
||||||
|
AlignConsecutiveDeclarations: false
|
||||||
|
AlignOperands: Align
|
||||||
|
AllowShortFunctionsOnASingleLine: Empty
|
||||||
|
BreakBeforeBraces: Attach
|
||||||
|
IndentCaseLabels: false
|
||||||
|
PointerAlignment: Right
|
||||||
@@ -6,6 +6,8 @@ set(CMAKE_CXX_STANDARD 17)
|
|||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
find_program(CLANG_FORMAT_BIN clang-format)
|
||||||
|
|
||||||
include(FetchContent)
|
include(FetchContent)
|
||||||
|
|
||||||
option(TELEGRAM_TUI_USE_SYSTEM_TDLIB "Use an installed TDLib package instead of fetching it." OFF)
|
option(TELEGRAM_TUI_USE_SYSTEM_TDLIB "Use an installed TDLib package instead of fetching it." OFF)
|
||||||
@@ -36,8 +38,13 @@ add_executable(
|
|||||||
shinoa
|
shinoa
|
||||||
src/app_attachments.cpp
|
src/app_attachments.cpp
|
||||||
src/app_chats.cpp
|
src/app_chats.cpp
|
||||||
|
src/app_commands.cpp
|
||||||
src/app.cpp
|
src/app.cpp
|
||||||
src/app_auth.cpp
|
src/app_auth.cpp
|
||||||
|
src/app_help.cpp
|
||||||
|
src/app_input.cpp
|
||||||
|
src/app_messages.cpp
|
||||||
|
src/app_shell.cpp
|
||||||
src/app_state.cpp
|
src/app_state.cpp
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/models.cpp
|
src/models.cpp
|
||||||
@@ -45,6 +52,19 @@ add_executable(
|
|||||||
src/util.cpp
|
src/util.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(CLANG_FORMAT_BIN)
|
||||||
|
file(GLOB_RECURSE SHINOA_FORMAT_SOURCES CONFIGURE_DEPENDS
|
||||||
|
src/*.cpp
|
||||||
|
src/*.h
|
||||||
|
)
|
||||||
|
add_custom_target(
|
||||||
|
format
|
||||||
|
COMMAND ${CLANG_FORMAT_BIN} -i ${SHINOA_FORMAT_SOURCES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Formatting C++ sources with clang-format"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_include_directories(shinoa PRIVATE ${CURSES_INCLUDE_DIRS})
|
target_include_directories(shinoa PRIVATE ${CURSES_INCLUDE_DIRS})
|
||||||
target_link_libraries(shinoa PRIVATE ${CURSES_LIBRARIES})
|
target_link_libraries(shinoa PRIVATE ${CURSES_LIBRARIES})
|
||||||
|
|
||||||
|
|||||||
1661
src/app.cpp
1661
src/app.cpp
File diff suppressed because it is too large
Load Diff
123
src/app.h
123
src/app.h
@@ -21,64 +21,83 @@ class App {
|
|||||||
int run();
|
int run();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
enum class ComposeCommandKind {
|
||||||
|
None,
|
||||||
|
Reply,
|
||||||
|
Edit,
|
||||||
|
Delete,
|
||||||
|
Forward,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ComposeCommand {
|
||||||
|
ComposeCommandKind kind = ComposeCommandKind::None;
|
||||||
|
std::int64_t message_id = 0;
|
||||||
|
std::vector<std::int64_t> message_ids;
|
||||||
|
std::string text;
|
||||||
|
};
|
||||||
|
|
||||||
void init_curses();
|
void init_curses();
|
||||||
void shutdown_curses();
|
void shutdown_curses();
|
||||||
bool process_updates();
|
bool process_updates();
|
||||||
void handle_td_object(const json& object);
|
void handle_td_object(const json &object);
|
||||||
void handle_authorization_state();
|
void handle_authorization_state();
|
||||||
void send_tdlib_parameters();
|
void send_tdlib_parameters();
|
||||||
void send_check_phone_number();
|
void send_check_phone_number();
|
||||||
void persist_config();
|
void persist_config();
|
||||||
void request_chat_details(std::int64_t chat_id);
|
void request_chat_details(std::int64_t chat_id);
|
||||||
void set_open_chat(std::int64_t chat_id);
|
void set_open_chat(std::int64_t chat_id);
|
||||||
void open_forward_target_menu(std::int64_t source_chat_id, std::vector<std::int64_t> message_ids);
|
void open_forward_target_menu(std::int64_t source_chat_id,
|
||||||
|
std::vector<std::int64_t> message_ids);
|
||||||
void start_input(InputMode mode, std::string prompt, bool hidden);
|
void start_input(InputMode mode, std::string prompt, bool hidden);
|
||||||
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 prepare_reply_input(std::int64_t chat_id, std::int64_t message_id,
|
||||||
void edit_message(std::int64_t chat_id, std::int64_t message_id, const std::string& text);
|
std::string initial_text = {});
|
||||||
void forward_message(
|
void send_message(std::int64_t chat_id, const std::string &text,
|
||||||
std::int64_t source_chat_id,
|
std::optional<std::int64_t> reply_to_message_id = std::nullopt);
|
||||||
const std::vector<std::int64_t>& message_ids,
|
void edit_message(std::int64_t chat_id, std::int64_t message_id, const std::string &text);
|
||||||
|
void delete_message(std::int64_t chat_id, const std::vector<std::int64_t> &message_ids);
|
||||||
|
void forward_message(std::int64_t source_chat_id,
|
||||||
|
const std::vector<std::int64_t> &message_ids,
|
||||||
std::int64_t target_chat_id);
|
std::int64_t target_chat_id);
|
||||||
void send_photo_message(
|
void send_photo_message(std::int64_t chat_id, const std::string &photo_path,
|
||||||
std::int64_t chat_id,
|
const std::string &caption,
|
||||||
const std::string& photo_path,
|
|
||||||
const std::string& caption,
|
|
||||||
std::optional<std::int64_t> reply_to_message_id = std::nullopt);
|
std::optional<std::int64_t> reply_to_message_id = std::nullopt);
|
||||||
bool preview_clipboard_photo_message(
|
bool preview_clipboard_photo_message(
|
||||||
std::int64_t chat_id,
|
std::int64_t chat_id, const std::string &caption,
|
||||||
const std::string& caption,
|
|
||||||
std::optional<std::int64_t> reply_to_message_id = std::nullopt);
|
std::optional<std::int64_t> reply_to_message_id = std::nullopt);
|
||||||
void request_more_chats();
|
void request_more_chats();
|
||||||
bool request_chat_history(std::int64_t chat_id, bool force);
|
bool request_chat_history(std::int64_t chat_id, bool force);
|
||||||
bool request_open_chat_history(bool force);
|
bool request_open_chat_history(bool force);
|
||||||
void sync_chat_ids_from_response(const json& response);
|
void sync_chat_ids_from_response(const json &response);
|
||||||
void mark_chat_messages_as_read(std::int64_t chat_id);
|
void mark_chat_messages_as_read(std::int64_t chat_id);
|
||||||
void mark_message_as_read(std::int64_t chat_id, std::int64_t message_id);
|
void mark_message_as_read(std::int64_t chat_id, std::int64_t message_id);
|
||||||
void update_user_status(std::int64_t user_id, const json& status);
|
void update_user_status(std::int64_t user_id, const json &status);
|
||||||
void upsert_user(const json& user);
|
void upsert_user(const json &user);
|
||||||
void upsert_basic_group(const json& basic_group);
|
void upsert_basic_group(const json &basic_group);
|
||||||
void upsert_supergroup(const json& supergroup);
|
void upsert_supergroup(const json &supergroup);
|
||||||
void upsert_chat(const json& chat_object);
|
void upsert_chat(const json &chat_object);
|
||||||
void apply_chat_position(ChatInfo& chat, const json& position);
|
void apply_chat_position(ChatInfo &chat, const json &position);
|
||||||
void resort_chats();
|
void resort_chats();
|
||||||
void append_message(std::int64_t chat_id, MessageInfo message);
|
void append_message(std::int64_t chat_id, MessageInfo message);
|
||||||
void remove_message(std::int64_t chat_id, std::int64_t message_id);
|
void remove_message(std::int64_t chat_id, std::int64_t message_id);
|
||||||
void merge_history(std::int64_t chat_id, const json& messages);
|
void merge_history(std::int64_t chat_id, const json &messages);
|
||||||
void update_attachment_file(const json& file);
|
void update_attachment_file(const json &file);
|
||||||
void start_reply_to_latest_message();
|
void start_reply_to_latest_message();
|
||||||
void request_attachment_download(const AttachmentInfo& attachment, bool open_after_download);
|
void request_attachment_download(const AttachmentInfo &attachment,
|
||||||
void open_attachment(const AttachmentInfo& attachment);
|
bool open_after_download);
|
||||||
void download_attachment(const AttachmentInfo& attachment);
|
void open_attachment(const AttachmentInfo &attachment);
|
||||||
void delete_attachment(const AttachmentInfo& attachment);
|
void download_attachment(const AttachmentInfo &attachment);
|
||||||
bool export_attachment_to_downloads(const AttachmentInfo& attachment);
|
void delete_attachment(const AttachmentInfo &attachment);
|
||||||
bool play_video_attachment(const AttachmentInfo& attachment);
|
bool export_attachment_to_downloads(const AttachmentInfo &attachment);
|
||||||
void refresh_attachment_viewer_content(const AttachmentInfo& attachment);
|
bool play_video_attachment(const AttachmentInfo &attachment);
|
||||||
[[nodiscard]] std::vector<std::string> attachment_animation_frames(const AttachmentInfo& attachment) const;
|
void refresh_attachment_viewer_content(const AttachmentInfo &attachment);
|
||||||
|
[[nodiscard]] std::vector<std::string>
|
||||||
|
attachment_animation_frames(const AttachmentInfo &attachment) const;
|
||||||
bool advance_attachment_animation();
|
bool advance_attachment_animation();
|
||||||
[[nodiscard]] std::string attachment_preview_path(const AttachmentInfo& attachment) const;
|
[[nodiscard]] std::string attachment_preview_path(const AttachmentInfo &attachment) const;
|
||||||
[[nodiscard]] bool has_inline_attachment_preview(const AttachmentInfo& attachment, int width, int height) const;
|
[[nodiscard]] bool has_inline_attachment_preview(const AttachmentInfo &attachment,
|
||||||
|
int width, int height) const;
|
||||||
void clear_attachment_preview_graphics();
|
void clear_attachment_preview_graphics();
|
||||||
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();
|
||||||
@@ -87,23 +106,31 @@ class App {
|
|||||||
bool move_message_attachment_selection(int delta);
|
bool move_message_attachment_selection(int delta);
|
||||||
[[nodiscard]] std::optional<AttachmentInfo> selected_message_attachment() const;
|
[[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,
|
||||||
[[nodiscard]] std::string build_attachment_preview_graphics(const AttachmentInfo& attachment, int width, int height) const;
|
int width, int height) const;
|
||||||
[[nodiscard]] std::tuple<bool, std::int64_t, std::string> parse_compose_command(const std::string& value) const;
|
[[nodiscard]] std::string
|
||||||
[[nodiscard]] std::tuple<bool, std::int64_t, std::string> parse_edit_command(const std::string& value) const;
|
build_attachment_preview_graphics(const AttachmentInfo &attachment, int width,
|
||||||
[[nodiscard]] std::optional<std::vector<std::int64_t>> parse_forward_command(const std::string& value) const;
|
int height) const;
|
||||||
[[nodiscard]] std::int64_t resolve_message_ref(const ChatInfo& chat, std::int64_t ref) const;
|
[[nodiscard]] ComposeCommand parse_compose_command(const std::string &value) const;
|
||||||
[[nodiscard]] std::optional<std::size_t> find_message_index(const ChatInfo& chat, std::int64_t message_id) const;
|
[[nodiscard]] std::tuple<bool, std::int64_t, std::string>
|
||||||
[[nodiscard]] std::string format_message_ref(const ChatInfo& chat, std::int64_t message_id) const;
|
parse_single_message_command(const std::string &value, const char *prefix) const;
|
||||||
[[nodiscard]] std::string sender_label(const json& sender) const;
|
[[nodiscard]] std::optional<std::vector<std::int64_t>>
|
||||||
|
parse_message_list_command(const std::string &value, const char *prefix) 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::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 user_status_label(std::int64_t user_id) const;
|
[[nodiscard]] std::string user_status_label(std::int64_t user_id) const;
|
||||||
[[nodiscard]] std::string forward_origin_label(const json& forward_info) const;
|
[[nodiscard]] std::string forward_origin_label(const json &forward_info) const;
|
||||||
[[nodiscard]] std::string via_bot_label(std::int64_t via_bot_user_id) const;
|
[[nodiscard]] std::string via_bot_label(std::int64_t via_bot_user_id) const;
|
||||||
[[nodiscard]] std::string preview_message(const json& message) const;
|
[[nodiscard]] std::string preview_message(const json &message) const;
|
||||||
[[nodiscard]] std::optional<AttachmentInfo> parse_attachment(const json& content) const;
|
[[nodiscard]] std::optional<AttachmentInfo> parse_attachment(const json &content) const;
|
||||||
[[nodiscard]] std::string content_to_text(const json& content, bool decorate) const;
|
[[nodiscard]] std::string content_to_text(const json &content, bool decorate) const;
|
||||||
[[nodiscard]] MessageInfo parse_message(const json& message) const;
|
[[nodiscard]] MessageInfo parse_message(const json &message) const;
|
||||||
[[nodiscard]] std::string format_open_chat_header(const ChatInfo& chat) const;
|
[[nodiscard]] std::string format_open_chat_header(const ChatInfo &chat) const;
|
||||||
void handle_key(int ch);
|
void handle_key(int ch);
|
||||||
void handle_wide_char(wint_t ch);
|
void handle_wide_char(wint_t ch);
|
||||||
void handle_input_key(int ch);
|
void handle_input_key(int ch);
|
||||||
@@ -154,6 +181,7 @@ class App {
|
|||||||
int attachment_action_index_ = 0;
|
int attachment_action_index_ = 0;
|
||||||
int attachment_viewer_scroll_ = 0;
|
int attachment_viewer_scroll_ = 0;
|
||||||
int forward_target_index_ = 0;
|
int forward_target_index_ = 0;
|
||||||
|
int help_menu_page_ = 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 message_attachment_message_id_ = 0;
|
||||||
@@ -178,6 +206,7 @@ class App {
|
|||||||
std::optional<AttachmentInfo> pending_attachment_open_;
|
std::optional<AttachmentInfo> pending_attachment_open_;
|
||||||
std::optional<AttachmentInfo> pending_attachment_download_;
|
std::optional<AttachmentInfo> pending_attachment_download_;
|
||||||
std::optional<AttachmentInfo> attachment_viewer_attachment_;
|
std::optional<AttachmentInfo> attachment_viewer_attachment_;
|
||||||
|
std::optional<std::int64_t> compose_reply_to_message_id_;
|
||||||
std::optional<std::int64_t> attachment_viewer_send_reply_to_message_id_;
|
std::optional<std::int64_t> attachment_viewer_send_reply_to_message_id_;
|
||||||
std::chrono::steady_clock::time_point attachment_viewer_next_frame_at_{};
|
std::chrono::steady_clock::time_point attachment_viewer_next_frame_at_{};
|
||||||
std::int64_t attachment_viewer_send_chat_id_ = 0;
|
std::int64_t attachment_viewer_send_chat_id_ = 0;
|
||||||
|
|||||||
269
src/app_auth.cpp
269
src/app_auth.cpp
@@ -16,8 +16,8 @@ namespace telegram_tui {
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct ClipboardImageType {
|
struct ClipboardImageType {
|
||||||
const char* mime = "";
|
const char *mime = "";
|
||||||
const char* extension = "";
|
const char *extension = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr std::array<ClipboardImageType, 5> kClipboardImageTypes = {{
|
constexpr std::array<ClipboardImageType, 5> kClipboardImageTypes = {{
|
||||||
@@ -28,7 +28,7 @@ constexpr std::array<ClipboardImageType, 5> kClipboardImageTypes = {{
|
|||||||
{"image/tiff", ".tiff"},
|
{"image/tiff", ".tiff"},
|
||||||
}};
|
}};
|
||||||
|
|
||||||
std::string shell_quote(const std::string& value) {
|
std::string shell_quote(const std::string &value) {
|
||||||
std::string quoted = "'";
|
std::string quoted = "'";
|
||||||
for (char ch : value) {
|
for (char ch : value) {
|
||||||
if (ch == '\'') {
|
if (ch == '\'') {
|
||||||
@@ -41,8 +41,8 @@ std::string shell_quote(const std::string& value) {
|
|||||||
return quoted;
|
return quoted;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string run_command_capture(const std::string& command) {
|
std::string run_command_capture(const std::string &command) {
|
||||||
FILE* pipe = popen(command.c_str(), "r");
|
FILE *pipe = popen(command.c_str(), "r");
|
||||||
if (pipe == nullptr) {
|
if (pipe == nullptr) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -58,12 +58,14 @@ std::string run_command_capture(const std::string& command) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool command_exists(const char* command) {
|
bool command_exists(const char *command) {
|
||||||
const std::string resolved = run_command_capture("command -v " + std::string(command) + " 2>/dev/null");
|
const std::string resolved =
|
||||||
|
run_command_capture("command -v " + std::string(command) + " 2>/dev/null");
|
||||||
return !trim_copy(resolved).empty();
|
return !trim_copy(resolved).empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string clipboard_capture_command(const std::string& mime_type, const std::string& destination_path) {
|
std::string clipboard_capture_command(const std::string &mime_type,
|
||||||
|
const std::string &destination_path) {
|
||||||
if (command_exists("wl-paste")) {
|
if (command_exists("wl-paste")) {
|
||||||
return "wl-paste --no-newline --type " + shell_quote(mime_type) + " > " +
|
return "wl-paste --no-newline --type " + shell_quote(mime_type) + " > " +
|
||||||
shell_quote(destination_path) + " 2>/dev/null";
|
shell_quote(destination_path) + " 2>/dev/null";
|
||||||
@@ -80,8 +82,9 @@ std::string clipboard_capture_command(const std::string& mime_type, const std::s
|
|||||||
|
|
||||||
std::optional<ClipboardImageType> detect_clipboard_image_type() {
|
std::optional<ClipboardImageType> detect_clipboard_image_type() {
|
||||||
if (command_exists("wl-paste")) {
|
if (command_exists("wl-paste")) {
|
||||||
const std::string types_output = run_command_capture("wl-paste --list-types 2>/dev/null");
|
const std::string types_output =
|
||||||
for (const auto& type : kClipboardImageTypes) {
|
run_command_capture("wl-paste --list-types 2>/dev/null");
|
||||||
|
for (const auto &type : kClipboardImageTypes) {
|
||||||
if (types_output.find(type.mime) != std::string::npos) {
|
if (types_output.find(type.mime) != std::string::npos) {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
@@ -90,7 +93,7 @@ std::optional<ClipboardImageType> detect_clipboard_image_type() {
|
|||||||
if (command_exists("xclip")) {
|
if (command_exists("xclip")) {
|
||||||
const std::string targets_output =
|
const std::string targets_output =
|
||||||
run_command_capture("xclip -selection clipboard -t TARGETS -o 2>/dev/null");
|
run_command_capture("xclip -selection clipboard -t TARGETS -o 2>/dev/null");
|
||||||
for (const auto& type : kClipboardImageTypes) {
|
for (const auto &type : kClipboardImageTypes) {
|
||||||
if (targets_output.find(type.mime) != std::string::npos) {
|
if (targets_output.find(type.mime) != std::string::npos) {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
@@ -102,9 +105,9 @@ std::optional<ClipboardImageType> detect_clipboard_image_type() {
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> parse_clipboard_compose_command(const std::string& value) {
|
std::optional<std::string> parse_clipboard_compose_command(const std::string &value) {
|
||||||
static constexpr std::array<const char*, 3> kPrefixes = {">paste", ">clip", ">screenshot"};
|
static constexpr std::array<const char *, 3> kPrefixes = {">paste", ">clip", ">screenshot"};
|
||||||
for (const char* prefix : kPrefixes) {
|
for (const char *prefix : kPrefixes) {
|
||||||
if (value == prefix) {
|
if (value == prefix) {
|
||||||
return std::string();
|
return std::string();
|
||||||
}
|
}
|
||||||
@@ -116,21 +119,7 @@ std::optional<std::string> parse_clipboard_compose_command(const std::string& va
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::string> strip_forward_prefix(const std::string& value) {
|
json markdown_formatted_text(TdClient &td, const std::string &text) {
|
||||||
static constexpr std::array<const char*, 3> kPrefixes = {">f", ">fw", ">forward"};
|
|
||||||
for (const char* prefix : kPrefixes) {
|
|
||||||
if (value == prefix) {
|
|
||||||
return std::string();
|
|
||||||
}
|
|
||||||
const std::string with_space = std::string(prefix) + " ";
|
|
||||||
if (value.rfind(with_space, 0) == 0) {
|
|
||||||
return trim_copy(value.substr(with_space.size()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
json markdown_formatted_text(TdClient& td, const std::string& text) {
|
|
||||||
json formatted_text = {
|
json formatted_text = {
|
||||||
{"@type", "formattedText"},
|
{"@type", "formattedText"},
|
||||||
{"text", text},
|
{"text", text},
|
||||||
@@ -280,7 +269,7 @@ void App::request_chat_details(std::int64_t chat_id) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatInfo& chat = chat_it->second;
|
ChatInfo &chat = chat_it->second;
|
||||||
if (chat.details_requested) {
|
if (chat.details_requested) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -329,6 +318,7 @@ void App::clear_input() {
|
|||||||
input_buffer_.clear();
|
input_buffer_.clear();
|
||||||
input_cursor_ = 0;
|
input_cursor_ = 0;
|
||||||
input_hidden_ = false;
|
input_hidden_ = false;
|
||||||
|
compose_reply_to_message_id_.reset();
|
||||||
curs_set(0);
|
curs_set(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,6 +334,7 @@ void App::submit_input() {
|
|||||||
status_line_ = "Telegram API ID must be numeric.";
|
status_line_ = "Telegram API ID must be numeric.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const auto pending_reply_to_message_id = compose_reply_to_message_id_;
|
||||||
clear_input();
|
clear_input();
|
||||||
|
|
||||||
if (mode == InputMode::Compose) {
|
if (mode == InputMode::Compose) {
|
||||||
@@ -352,36 +343,72 @@ void App::submit_input() {
|
|||||||
status_line_ = "Open chat first.";
|
status_line_ = "Open chat first.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (const auto message_ids = parse_forward_command(value); message_ids.has_value()) {
|
const ComposeCommand command = parse_compose_command(value);
|
||||||
open_forward_target_menu(*chat_id, *message_ids);
|
if (command.kind == ComposeCommandKind::Forward) {
|
||||||
|
if (command.message_ids.empty()) {
|
||||||
|
status_line_ =
|
||||||
|
"Forward command needs one or more valid message refs.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto [is_edit, edit_message_id, edit_text] = parse_edit_command(value);
|
open_forward_target_menu(*chat_id, command.message_ids);
|
||||||
if (is_edit) {
|
return;
|
||||||
if (edit_message_id == 0) {
|
}
|
||||||
|
if (command.kind == ComposeCommandKind::Delete) {
|
||||||
|
if (command.message_ids.empty()) {
|
||||||
|
status_line_ = "Delete command needs a valid message ref.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto chat_it = chats_.find(*chat_id);
|
||||||
|
if (chat_it != chats_.end()) {
|
||||||
|
for (const std::int64_t delete_message_id : command.message_ids) {
|
||||||
|
const auto message_it = std::find_if(
|
||||||
|
chat_it->second.messages.begin(),
|
||||||
|
chat_it->second.messages.end(),
|
||||||
|
[&](const MessageInfo &message) {
|
||||||
|
return message.id == delete_message_id;
|
||||||
|
});
|
||||||
|
if (message_it != chat_it->second.messages.end() &&
|
||||||
|
!message_it->is_outgoing) {
|
||||||
|
status_line_ = "Can delete only your own messages.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete_message(*chat_id, command.message_ids);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command.kind == ComposeCommandKind::Edit) {
|
||||||
|
if (command.message_id == 0) {
|
||||||
status_line_ = "Edit command needs a valid message ref.";
|
status_line_ = "Edit command needs a valid message ref.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (edit_text.empty()) {
|
if (command.text.empty()) {
|
||||||
status_line_ = "Edit text cannot be empty.";
|
status_line_ = "Edit text cannot be empty.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
edit_message(*chat_id, edit_message_id, edit_text);
|
edit_message(*chat_id, command.message_id, command.text);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto [is_reply, reply_to_message_id, text] = parse_compose_command(value);
|
std::string text = value;
|
||||||
if (const auto clipboard_caption = parse_clipboard_compose_command(text); clipboard_caption.has_value()) {
|
if (command.kind == ComposeCommandKind::Reply) {
|
||||||
preview_clipboard_photo_message(
|
if (command.message_id == 0) {
|
||||||
*chat_id,
|
status_line_ = "Reply command needs a valid message ref.";
|
||||||
*clipboard_caption,
|
return;
|
||||||
is_reply ? std::optional<std::int64_t>(reply_to_message_id) : std::nullopt);
|
}
|
||||||
|
prepare_reply_input(*chat_id, command.message_id, command.text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (const auto clipboard_caption = parse_clipboard_compose_command(text);
|
||||||
|
clipboard_caption.has_value()) {
|
||||||
|
preview_clipboard_photo_message(*chat_id, *clipboard_caption,
|
||||||
|
pending_reply_to_message_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text.empty()) {
|
if (text.empty()) {
|
||||||
status_line_ = "Reply text cannot be empty.";
|
status_line_ = "Message text cannot be empty.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
send_message(*chat_id, text, is_reply ? std::optional<std::int64_t>(reply_to_message_id) : std::nullopt);
|
send_message(*chat_id, text, pending_reply_to_message_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mode == InputMode::ApiId) {
|
if (mode == InputMode::ApiId) {
|
||||||
@@ -439,9 +466,22 @@ void App::submit_input() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::send_message(
|
void App::prepare_reply_input(std::int64_t chat_id, std::int64_t message_id,
|
||||||
std::int64_t chat_id,
|
std::string initial_text) {
|
||||||
const std::string& text,
|
const auto chat_it = chats_.find(chat_id);
|
||||||
|
if (chat_it == chats_.end()) {
|
||||||
|
status_line_ = "Chat unavailable.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
compose_reply_to_message_id_ = message_id;
|
||||||
|
input_buffer_ = std::move(initial_text);
|
||||||
|
start_input(InputMode::Compose, "Reply " + format_message_ref(chat_it->second, message_id),
|
||||||
|
false);
|
||||||
|
status_line_ = "Reply prepared.";
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::send_message(std::int64_t chat_id, const std::string &text,
|
||||||
std::optional<std::int64_t> reply_to_message_id) {
|
std::optional<std::int64_t> reply_to_message_id) {
|
||||||
json formatted_text = markdown_formatted_text(td_, text);
|
json formatted_text = markdown_formatted_text(td_, text);
|
||||||
|
|
||||||
@@ -465,7 +505,7 @@ 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) {
|
void App::edit_message(std::int64_t chat_id, std::int64_t message_id, const std::string &text) {
|
||||||
td_.send({
|
td_.send({
|
||||||
{"@type", "editMessageText"},
|
{"@type", "editMessageText"},
|
||||||
{"chat_id", chat_id},
|
{"chat_id", chat_id},
|
||||||
@@ -481,9 +521,17 @@ void App::edit_message(std::int64_t chat_id, std::int64_t message_id, const std:
|
|||||||
status_line_ = "Editing message...";
|
status_line_ = "Editing message...";
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::forward_message(
|
void App::delete_message(std::int64_t chat_id, const std::vector<std::int64_t> &message_ids) {
|
||||||
std::int64_t source_chat_id,
|
td_.send({
|
||||||
const std::vector<std::int64_t>& message_ids,
|
{"@type", "deleteMessages"},
|
||||||
|
{"chat_id", chat_id},
|
||||||
|
{"message_ids", message_ids},
|
||||||
|
{"revoke", true},
|
||||||
|
});
|
||||||
|
status_line_ = message_ids.size() == 1 ? "Deleting message..." : "Deleting messages...";
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::forward_message(std::int64_t source_chat_id, const std::vector<std::int64_t> &message_ids,
|
||||||
std::int64_t target_chat_id) {
|
std::int64_t target_chat_id) {
|
||||||
td_.send({
|
td_.send({
|
||||||
{"@type", "forwardMessages"},
|
{"@type", "forwardMessages"},
|
||||||
@@ -496,10 +544,8 @@ void App::forward_message(
|
|||||||
status_line_ = message_ids.size() == 1 ? "Forwarding message..." : "Forwarding messages...";
|
status_line_ = message_ids.size() == 1 ? "Forwarding message..." : "Forwarding messages...";
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::send_photo_message(
|
void App::send_photo_message(std::int64_t chat_id, const std::string &photo_path,
|
||||||
std::int64_t chat_id,
|
const std::string &caption,
|
||||||
const std::string& photo_path,
|
|
||||||
const std::string& caption,
|
|
||||||
std::optional<std::int64_t> reply_to_message_id) {
|
std::optional<std::int64_t> reply_to_message_id) {
|
||||||
json request = {
|
json request = {
|
||||||
{"@type", "sendMessage"},
|
{"@type", "sendMessage"},
|
||||||
@@ -529,20 +575,19 @@ void App::send_photo_message(
|
|||||||
status_line_ = "Photo queued.";
|
status_line_ = "Photo queued.";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool App::preview_clipboard_photo_message(
|
bool App::preview_clipboard_photo_message(std::int64_t chat_id, const std::string &caption,
|
||||||
std::int64_t chat_id,
|
|
||||||
const std::string& caption,
|
|
||||||
std::optional<std::int64_t> reply_to_message_id) {
|
std::optional<std::int64_t> reply_to_message_id) {
|
||||||
const auto clipboard_type = detect_clipboard_image_type();
|
const auto clipboard_type = detect_clipboard_image_type();
|
||||||
if (!clipboard_type.has_value()) {
|
if (!clipboard_type.has_value()) {
|
||||||
status_line_ = "Clipboard doesn't contain an image, or no clipboard tool is available.";
|
status_line_ =
|
||||||
|
"Clipboard doesn't contain an image, or no clipboard tool is available.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path clipboard_dir = files_dir_ / "clipboard";
|
const std::filesystem::path clipboard_dir = files_dir_ / "clipboard";
|
||||||
try {
|
try {
|
||||||
std::filesystem::create_directories(clipboard_dir);
|
std::filesystem::create_directories(clipboard_dir);
|
||||||
} catch (const std::exception& error) {
|
} catch (const std::exception &error) {
|
||||||
status_line_ = std::string("Failed to prepare clipboard cache: ") + error.what();
|
status_line_ = std::string("Failed to prepare clipboard cache: ") + error.what();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -550,8 +595,8 @@ bool App::preview_clipboard_photo_message(
|
|||||||
std::filesystem::path image_path;
|
std::filesystem::path image_path;
|
||||||
for (std::uint64_t index = 0; index < 1024; ++index) {
|
for (std::uint64_t index = 0; index < 1024; ++index) {
|
||||||
const std::filesystem::path candidate =
|
const std::filesystem::path candidate =
|
||||||
clipboard_dir / ("clipboard-" + std::to_string(std::time(nullptr)) + "-" + std::to_string(index) +
|
clipboard_dir / ("clipboard-" + std::to_string(std::time(nullptr)) + "-" +
|
||||||
clipboard_type->extension);
|
std::to_string(index) + clipboard_type->extension);
|
||||||
if (!std::filesystem::exists(candidate)) {
|
if (!std::filesystem::exists(candidate)) {
|
||||||
image_path = candidate;
|
image_path = candidate;
|
||||||
break;
|
break;
|
||||||
@@ -562,8 +607,10 @@ bool App::preview_clipboard_photo_message(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string command = clipboard_capture_command(clipboard_type->mime, image_path.string());
|
const std::string command =
|
||||||
if (command.empty() || std::system(command.c_str()) != 0 || !std::filesystem::exists(image_path)) {
|
clipboard_capture_command(clipboard_type->mime, image_path.string());
|
||||||
|
if (command.empty() || std::system(command.c_str()) != 0 ||
|
||||||
|
!std::filesystem::exists(image_path)) {
|
||||||
status_line_ = "Failed to read image data from the clipboard.";
|
status_line_ = "Failed to read image data from the clipboard.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -574,7 +621,7 @@ bool App::preview_clipboard_photo_message(
|
|||||||
status_line_ = "Clipboard image is empty.";
|
status_line_ = "Clipboard image is empty.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (const std::exception&) {
|
} catch (const std::exception &) {
|
||||||
status_line_ = "Clipboard image couldn't be validated.";
|
status_line_ = "Clipboard image couldn't be validated.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -605,94 +652,4 @@ bool App::preview_clipboard_photo_message(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::vector<std::int64_t>> App::parse_forward_command(const std::string& value) const {
|
|
||||||
const auto remainder = strip_forward_prefix(value);
|
|
||||||
if (!remainder.has_value()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto chat_id = open_chat_id();
|
|
||||||
if (!chat_id.has_value()) {
|
|
||||||
return std::vector<std::int64_t>{};
|
|
||||||
}
|
|
||||||
const auto chat_it = chats_.find(*chat_id);
|
|
||||||
if (chat_it == chats_.end()) {
|
|
||||||
return std::vector<std::int64_t>{};
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChatInfo& chat = chat_it->second;
|
|
||||||
if (remainder->empty()) {
|
|
||||||
return std::vector<std::int64_t>{};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto resolve_message_ref = [&](std::int64_t ref) -> std::int64_t {
|
|
||||||
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::string normalized = *remainder;
|
|
||||||
for (char& ch : normalized) {
|
|
||||||
if (ch == ',') {
|
|
||||||
ch = ' ';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<std::int64_t> unique_message_ids;
|
|
||||||
std::stringstream stream(normalized);
|
|
||||||
std::string token;
|
|
||||||
while (stream >> token) {
|
|
||||||
const auto dash = token.find('-');
|
|
||||||
if (dash != std::string::npos) {
|
|
||||||
const std::string start_text = token.substr(0, dash);
|
|
||||||
const std::string end_text = token.substr(dash + 1);
|
|
||||||
if (!is_decimal_number(start_text) || !is_decimal_number(end_text)) {
|
|
||||||
return std::vector<std::int64_t>{};
|
|
||||||
}
|
|
||||||
std::int64_t start_value = 0;
|
|
||||||
std::int64_t end_value = 0;
|
|
||||||
try {
|
|
||||||
start_value = std::stoll(start_text);
|
|
||||||
end_value = std::stoll(end_text);
|
|
||||||
} catch (...) {
|
|
||||||
return std::vector<std::int64_t>{};
|
|
||||||
}
|
|
||||||
if (start_value > end_value) {
|
|
||||||
std::swap(start_value, end_value);
|
|
||||||
}
|
|
||||||
for (std::int64_t ref = start_value; ref <= end_value; ++ref) {
|
|
||||||
const std::int64_t message_id = resolve_message_ref(ref);
|
|
||||||
if (message_id == 0) {
|
|
||||||
return std::vector<std::int64_t>{};
|
|
||||||
}
|
|
||||||
unique_message_ids.insert(message_id);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!is_decimal_number(token)) {
|
|
||||||
return std::vector<std::int64_t>{};
|
|
||||||
}
|
|
||||||
std::int64_t parsed_value = 0;
|
|
||||||
try {
|
|
||||||
parsed_value = std::stoll(token);
|
|
||||||
} catch (...) {
|
|
||||||
return std::vector<std::int64_t>{};
|
|
||||||
}
|
|
||||||
const std::int64_t message_id = resolve_message_ref(parsed_value);
|
|
||||||
if (message_id == 0) {
|
|
||||||
return std::vector<std::int64_t>{};
|
|
||||||
}
|
|
||||||
unique_message_ids.insert(message_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::vector<std::int64_t>(unique_message_ids.begin(), unique_message_ids.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace telegram_tui
|
} // namespace telegram_tui
|
||||||
|
|||||||
@@ -2,15 +2,19 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <curses.h>
|
||||||
|
|
||||||
|
#include "app_ui.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
namespace telegram_tui {
|
namespace telegram_tui {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
std::string join_with_separator_local(const std::vector<std::string>& parts, const char* separator) {
|
std::string join_with_separator_local(const std::vector<std::string> &parts,
|
||||||
|
const char *separator) {
|
||||||
std::string joined;
|
std::string joined;
|
||||||
for (const auto& part : parts) {
|
for (const auto &part : parts) {
|
||||||
if (part.empty()) {
|
if (part.empty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -22,7 +26,7 @@ std::string join_with_separator_local(const std::vector<std::string>& parts, con
|
|||||||
return joined;
|
return joined;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string format_user_status(const json& status) {
|
std::string format_user_status(const json &status) {
|
||||||
const std::string type = safe_string(status, "@type");
|
const std::string type = safe_string(status, "@type");
|
||||||
if (type == "userStatusOnline") {
|
if (type == "userStatusOnline") {
|
||||||
return "online";
|
return "online";
|
||||||
@@ -43,11 +47,11 @@ std::string format_user_status(const json& status) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string primary_username(const json& object) {
|
std::string primary_username(const json &object) {
|
||||||
const json usernames = object.value("usernames", json::object());
|
const json usernames = object.value("usernames", json::object());
|
||||||
const json active_usernames = usernames.value("active_usernames", json::array());
|
const json active_usernames = usernames.value("active_usernames", json::array());
|
||||||
if (active_usernames.is_array()) {
|
if (active_usernames.is_array()) {
|
||||||
for (const auto& username : active_usernames) {
|
for (const auto &username : active_usernames) {
|
||||||
if (username.is_string()) {
|
if (username.is_string()) {
|
||||||
const std::string value = username.get<std::string>();
|
const std::string value = username.get<std::string>();
|
||||||
if (!value.empty()) {
|
if (!value.empty()) {
|
||||||
@@ -61,17 +65,17 @@ std::string primary_username(const json& object) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void App::update_user_status(std::int64_t user_id, const json& status) {
|
void App::update_user_status(std::int64_t user_id, const json &status) {
|
||||||
UserInfo& info = users_[user_id];
|
UserInfo &info = users_[user_id];
|
||||||
info.id = user_id;
|
info.id = user_id;
|
||||||
info.status = format_user_status(status);
|
info.status = format_user_status(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::upsert_user(const json& user) {
|
void App::upsert_user(const json &user) {
|
||||||
if (user.is_null()) {
|
if (user.is_null()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UserInfo& info = users_[safe_i64(user, "id")];
|
UserInfo &info = users_[safe_i64(user, "id")];
|
||||||
info.id = safe_i64(user, "id");
|
info.id = safe_i64(user, "id");
|
||||||
info.first_name = safe_string(user, "first_name");
|
info.first_name = safe_string(user, "first_name");
|
||||||
info.last_name = safe_string(user, "last_name");
|
info.last_name = safe_string(user, "last_name");
|
||||||
@@ -79,13 +83,13 @@ void App::upsert_user(const json& user) {
|
|||||||
info.status = format_user_status(user.value("status", json::object()));
|
info.status = format_user_status(user.value("status", json::object()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::upsert_basic_group(const json& basic_group) {
|
void App::upsert_basic_group(const json &basic_group) {
|
||||||
if (basic_group.is_null()) {
|
if (basic_group.is_null()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const std::int64_t basic_group_id = safe_i64(basic_group, "id");
|
const std::int64_t basic_group_id = safe_i64(basic_group, "id");
|
||||||
const std::int32_t member_count = safe_i32(basic_group, "member_count");
|
const std::int32_t member_count = safe_i32(basic_group, "member_count");
|
||||||
for (auto& [chat_id, chat] : chats_) {
|
for (auto &[chat_id, chat] : chats_) {
|
||||||
if (chat.basic_group_id != basic_group_id) {
|
if (chat.basic_group_id != basic_group_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -96,7 +100,7 @@ void App::upsert_basic_group(const json& basic_group) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::upsert_supergroup(const json& supergroup) {
|
void App::upsert_supergroup(const json &supergroup) {
|
||||||
if (supergroup.is_null()) {
|
if (supergroup.is_null()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -104,7 +108,7 @@ void App::upsert_supergroup(const json& supergroup) {
|
|||||||
const std::int32_t member_count = safe_i32(supergroup, "member_count");
|
const std::int32_t member_count = safe_i32(supergroup, "member_count");
|
||||||
const bool is_channel = supergroup.value("is_channel", false);
|
const bool is_channel = supergroup.value("is_channel", false);
|
||||||
const std::string username = primary_username(supergroup);
|
const std::string username = primary_username(supergroup);
|
||||||
for (auto& [chat_id, chat] : chats_) {
|
for (auto &[chat_id, chat] : chats_) {
|
||||||
if (chat.supergroup_id != supergroup_id) {
|
if (chat.supergroup_id != supergroup_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -116,13 +120,13 @@ void App::upsert_supergroup(const json& supergroup) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::upsert_chat(const json& chat_object) {
|
void App::upsert_chat(const json &chat_object) {
|
||||||
if (chat_object.is_null()) {
|
if (chat_object.is_null()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::int64_t chat_id = safe_i64(chat_object, "id");
|
const std::int64_t chat_id = safe_i64(chat_object, "id");
|
||||||
ChatInfo& chat = chats_[chat_id];
|
ChatInfo &chat = chats_[chat_id];
|
||||||
chat.id = chat_id;
|
chat.id = chat_id;
|
||||||
if (chat_object.contains("title")) {
|
if (chat_object.contains("title")) {
|
||||||
chat.title = safe_string(chat_object, "title");
|
chat.title = safe_string(chat_object, "title");
|
||||||
@@ -169,7 +173,7 @@ void App::upsert_chat(const json& chat_object) {
|
|||||||
if (chat_object.contains("positions") && chat_object.at("positions").is_array()) {
|
if (chat_object.contains("positions") && chat_object.at("positions").is_array()) {
|
||||||
chat.in_main_list = false;
|
chat.in_main_list = false;
|
||||||
chat.main_order = 0;
|
chat.main_order = 0;
|
||||||
for (const auto& position : chat_object.at("positions")) {
|
for (const auto &position : chat_object.at("positions")) {
|
||||||
apply_chat_position(chat, position);
|
apply_chat_position(chat, position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +183,7 @@ void App::upsert_chat(const json& chat_object) {
|
|||||||
resort_chats();
|
resort_chats();
|
||||||
}
|
}
|
||||||
|
|
||||||
void App::apply_chat_position(ChatInfo& chat, const json& position) {
|
void App::apply_chat_position(ChatInfo &chat, const json &position) {
|
||||||
if (!position.is_object()) {
|
if (!position.is_object()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -194,17 +198,18 @@ void App::apply_chat_position(ChatInfo& chat, const json& position) {
|
|||||||
|
|
||||||
void App::resort_chats() {
|
void App::resort_chats() {
|
||||||
if (sorted_chat_ids_.empty()) {
|
if (sorted_chat_ids_.empty()) {
|
||||||
for (const auto& [chat_id, chat] : chats_) {
|
for (const auto &[chat_id, chat] : chats_) {
|
||||||
if (chat.in_main_list) {
|
if (chat.in_main_list) {
|
||||||
sorted_chat_ids_.push_back(chat_id);
|
sorted_chat_ids_.push_back(chat_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const auto& [chat_id, chat] : chats_) {
|
for (const auto &[chat_id, chat] : chats_) {
|
||||||
if (!chat.in_main_list) {
|
if (!chat.in_main_list) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (std::find(sorted_chat_ids_.begin(), sorted_chat_ids_.end(), chat_id) == sorted_chat_ids_.end()) {
|
if (std::find(sorted_chat_ids_.begin(), sorted_chat_ids_.end(), chat_id) ==
|
||||||
|
sorted_chat_ids_.end()) {
|
||||||
sorted_chat_ids_.push_back(chat_id);
|
sorted_chat_ids_.push_back(chat_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,13 +219,19 @@ void App::resort_chats() {
|
|||||||
[&](std::int64_t lhs, std::int64_t rhs) {
|
[&](std::int64_t lhs, std::int64_t rhs) {
|
||||||
const auto left_it = chats_.find(lhs);
|
const auto left_it = chats_.find(lhs);
|
||||||
const auto right_it = chats_.find(rhs);
|
const auto right_it = chats_.find(rhs);
|
||||||
const bool left_has_order = left_it != chats_.end() && left_it->second.in_main_list && left_it->second.main_order > 0;
|
const bool left_has_order = left_it != chats_.end() &&
|
||||||
const bool right_has_order = right_it != chats_.end() && right_it->second.in_main_list && right_it->second.main_order > 0;
|
left_it->second.in_main_list &&
|
||||||
|
left_it->second.main_order > 0;
|
||||||
|
const bool right_has_order = right_it != chats_.end() &&
|
||||||
|
right_it->second.in_main_list &&
|
||||||
|
right_it->second.main_order > 0;
|
||||||
if (left_has_order != right_has_order) {
|
if (left_has_order != right_has_order) {
|
||||||
return left_has_order;
|
return left_has_order;
|
||||||
}
|
}
|
||||||
if (left_has_order && right_has_order && left_it->second.main_order != right_it->second.main_order) {
|
if (left_has_order && right_has_order &&
|
||||||
return left_it->second.main_order > right_it->second.main_order;
|
left_it->second.main_order != right_it->second.main_order) {
|
||||||
|
return left_it->second.main_order >
|
||||||
|
right_it->second.main_order;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@@ -230,7 +241,15 @@ void App::resort_chats() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string App::format_open_chat_header(const ChatInfo& chat) const {
|
std::string App::user_status_label(std::int64_t user_id) const {
|
||||||
|
const auto user_it = users_.find(user_id);
|
||||||
|
if (user_it == users_.end()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return user_it->second.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string App::format_open_chat_header(const ChatInfo &chat) const {
|
||||||
std::vector<std::string> parts;
|
std::vector<std::string> parts;
|
||||||
if (!chat.title.empty()) {
|
if (!chat.title.empty()) {
|
||||||
parts.push_back(chat.title);
|
parts.push_back(chat.title);
|
||||||
@@ -256,8 +275,8 @@ std::string App::format_open_chat_header(const ChatInfo& chat) const {
|
|||||||
parts.push_back("@" + chat.username);
|
parts.push_back("@" + chat.username);
|
||||||
}
|
}
|
||||||
if (chat.has_member_count) {
|
if (chat.has_member_count) {
|
||||||
parts.push_back(
|
parts.push_back(std::to_string(chat.member_count) + " " +
|
||||||
std::to_string(chat.member_count) + " " + (chat.is_channel ? "subscribers" : "members"));
|
(chat.is_channel ? "subscribers" : "members"));
|
||||||
}
|
}
|
||||||
if (!chat.is_channel && chat.has_online_member_count) {
|
if (!chat.is_channel && chat.has_online_member_count) {
|
||||||
parts.push_back(std::to_string(chat.online_member_count) + " online");
|
parts.push_back(std::to_string(chat.online_member_count) + " online");
|
||||||
@@ -266,4 +285,60 @@ std::string App::format_open_chat_header(const ChatInfo& chat) const {
|
|||||||
return join_with_separator_local(parts, " | ");
|
return join_with_separator_local(parts, " | ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void App::draw_chat_pane(int top, int height, int width) {
|
||||||
|
const int visible_rows = std::max(1, height);
|
||||||
|
int first_index = 0;
|
||||||
|
if (selected_chat_index_ >= visible_rows) {
|
||||||
|
first_index = selected_chat_index_ - visible_rows + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < visible_rows; ++row) {
|
||||||
|
const int index = first_index + row;
|
||||||
|
const int y = top + row;
|
||||||
|
mvhline(y, 0, ' ', width);
|
||||||
|
if (index >= static_cast<int>(sorted_chat_ids_.size())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto chat_id = sorted_chat_ids_[index];
|
||||||
|
const auto chat_it = chats_.find(chat_id);
|
||||||
|
const bool selected = index == selected_chat_index_;
|
||||||
|
const bool is_open = open_chat_id_ == chat_id;
|
||||||
|
if (selected) {
|
||||||
|
attron(A_BOLD | (focus_ == FocusPane::Chats ? A_REVERSE : A_NORMAL));
|
||||||
|
}
|
||||||
|
if (is_open) {
|
||||||
|
attron(COLOR_PAIR(kColorPairOpenChat));
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool has_chat = chat_it != chats_.end();
|
||||||
|
const ChatInfo *chat = has_chat ? &chat_it->second : nullptr;
|
||||||
|
std::string line = (chat != nullptr && chat->unread_count > 0) ? "[U] " : "[ ] ";
|
||||||
|
line += is_open ? ">" : " ";
|
||||||
|
if (chat != nullptr) {
|
||||||
|
line += chat->title;
|
||||||
|
} else {
|
||||||
|
line += "Chat " + std::to_string(chat_id);
|
||||||
|
}
|
||||||
|
if (chat != nullptr && chat->unread_count > 0) {
|
||||||
|
line += " [" + std::to_string(chat->unread_count) + "]";
|
||||||
|
}
|
||||||
|
if (chat != nullptr && !chat->last_message_preview.empty()) {
|
||||||
|
line += " - " + chat->last_message_preview;
|
||||||
|
}
|
||||||
|
if (static_cast<int>(line.size()) > width - 2) {
|
||||||
|
line.resize(static_cast<std::size_t>(width - 5));
|
||||||
|
line += "...";
|
||||||
|
}
|
||||||
|
mvprintw(y, 1, "%s", line.c_str());
|
||||||
|
|
||||||
|
if (is_open) {
|
||||||
|
attroff(COLOR_PAIR(kColorPairOpenChat));
|
||||||
|
}
|
||||||
|
if (selected) {
|
||||||
|
attroff(A_BOLD | (focus_ == FocusPane::Chats ? A_REVERSE : A_NORMAL));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace telegram_tui
|
} // namespace telegram_tui
|
||||||
|
|||||||
180
src/app_commands.cpp
Normal file
180
src/app_commands.cpp
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#include "app.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
namespace telegram_tui {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool starts_with_at(const std::string &text, std::size_t index, const char *prefix) {
|
||||||
|
const std::size_t prefix_size = std::char_traits<char>::length(prefix);
|
||||||
|
return text.compare(index, prefix_size, prefix) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
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_single_message_command(const std::string &value, const char *prefix) const {
|
||||||
|
if (!starts_with_at(value, 0, prefix)) {
|
||||||
|
return {false, 0, value};
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t prefix_size = std::char_traits<char>::length(prefix);
|
||||||
|
std::size_t id_start = prefix_size;
|
||||||
|
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)};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatInfo &chat = chat_it->second;
|
||||||
|
return {true, resolve_message_ref(chat, parsed_value), value.substr(id_end)};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::vector<std::int64_t>> App::parse_message_list_command(const std::string &value,
|
||||||
|
const char *prefix) const {
|
||||||
|
if (!starts_with_at(value, 0, prefix)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t prefix_size = std::char_traits<char>::length(prefix);
|
||||||
|
std::string remainder = value.substr(prefix_size);
|
||||||
|
remainder = trim_copy(std::move(remainder));
|
||||||
|
if (remainder.empty()) {
|
||||||
|
return std::vector<std::int64_t>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto chat_id = open_chat_id();
|
||||||
|
if (!chat_id.has_value()) {
|
||||||
|
return std::vector<std::int64_t>{};
|
||||||
|
}
|
||||||
|
const auto chat_it = chats_.find(*chat_id);
|
||||||
|
if (chat_it == chats_.end()) {
|
||||||
|
return std::vector<std::int64_t>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatInfo &chat = chat_it->second;
|
||||||
|
for (char &ch : remainder) {
|
||||||
|
if (ch == ',') {
|
||||||
|
ch = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::int64_t> unique_message_ids;
|
||||||
|
std::stringstream stream(remainder);
|
||||||
|
std::string token;
|
||||||
|
while (stream >> token) {
|
||||||
|
const auto dash = token.find('-');
|
||||||
|
if (dash != std::string::npos) {
|
||||||
|
const std::string start_text = token.substr(0, dash);
|
||||||
|
const std::string end_text = token.substr(dash + 1);
|
||||||
|
if (!is_decimal_number(start_text) || !is_decimal_number(end_text)) {
|
||||||
|
return std::vector<std::int64_t>{};
|
||||||
|
}
|
||||||
|
std::int64_t start_value = 0;
|
||||||
|
std::int64_t end_value = 0;
|
||||||
|
try {
|
||||||
|
start_value = std::stoll(start_text);
|
||||||
|
end_value = std::stoll(end_text);
|
||||||
|
} catch (...) {
|
||||||
|
return std::vector<std::int64_t>{};
|
||||||
|
}
|
||||||
|
if (start_value > end_value) {
|
||||||
|
std::swap(start_value, end_value);
|
||||||
|
}
|
||||||
|
for (std::int64_t ref = start_value; ref <= end_value; ++ref) {
|
||||||
|
const std::int64_t message_id = resolve_message_ref(chat, ref);
|
||||||
|
if (message_id == 0) {
|
||||||
|
return std::vector<std::int64_t>{};
|
||||||
|
}
|
||||||
|
unique_message_ids.insert(message_id);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_decimal_number(token)) {
|
||||||
|
return std::vector<std::int64_t>{};
|
||||||
|
}
|
||||||
|
std::int64_t parsed_value = 0;
|
||||||
|
try {
|
||||||
|
parsed_value = std::stoll(token);
|
||||||
|
} catch (...) {
|
||||||
|
return std::vector<std::int64_t>{};
|
||||||
|
}
|
||||||
|
const std::int64_t message_id = resolve_message_ref(chat, parsed_value);
|
||||||
|
if (message_id == 0) {
|
||||||
|
return std::vector<std::int64_t>{};
|
||||||
|
}
|
||||||
|
unique_message_ids.insert(message_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::vector<std::int64_t>(unique_message_ids.begin(), unique_message_ids.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
App::ComposeCommand App::parse_compose_command(const std::string &value) const {
|
||||||
|
if (const auto message_ids = parse_message_list_command(value, ">f ");
|
||||||
|
message_ids.has_value()) {
|
||||||
|
return ComposeCommand{ComposeCommandKind::Forward, 0, *message_ids, {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto message_ids = parse_message_list_command(value, ">d ");
|
||||||
|
message_ids.has_value()) {
|
||||||
|
return ComposeCommand{ComposeCommandKind::Delete, 0, *message_ids, {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto [matched, message_id, text] = parse_single_message_command(value, ">e ");
|
||||||
|
matched) {
|
||||||
|
return ComposeCommand{ComposeCommandKind::Edit, message_id, {}, text};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto [matched, message_id, text] = parse_single_message_command(value, ">r ");
|
||||||
|
matched) {
|
||||||
|
return ComposeCommand{ComposeCommandKind::Reply, message_id, {}, text};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ComposeCommand{};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace telegram_tui
|
||||||
174
src/app_help.cpp
Normal file
174
src/app_help.cpp
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
#include "app.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <curses.h>
|
||||||
|
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
namespace telegram_tui {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const std::vector<std::string> kShinoaBanner = {
|
||||||
|
" ____ _ _ ", " / ___|| |__ (_)_ __ ___ __ _ ",
|
||||||
|
" \\___ \\| '_ \\| | '_ \\ / _ \\ / _` |", " ___) | | | | | | | | (_) | (_| |",
|
||||||
|
" |____/|_| |_|_|_| |_|\\___/ \\__,_|",
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void App::handle_help_menu_key(int ch) {
|
||||||
|
switch (ch) {
|
||||||
|
case 27:
|
||||||
|
case 'h':
|
||||||
|
case '?':
|
||||||
|
case KEY_F(1):
|
||||||
|
help_menu_open_ = false;
|
||||||
|
status_line_ = "Closed help.";
|
||||||
|
return;
|
||||||
|
case '1':
|
||||||
|
help_menu_page_ = 0;
|
||||||
|
status_line_ = "Help.";
|
||||||
|
return;
|
||||||
|
case '2':
|
||||||
|
help_menu_page_ = 1;
|
||||||
|
status_line_ = "Settings.";
|
||||||
|
return;
|
||||||
|
case '3':
|
||||||
|
help_menu_page_ = 2;
|
||||||
|
status_line_ = "About.";
|
||||||
|
return;
|
||||||
|
case KEY_LEFT:
|
||||||
|
help_menu_page_ = (help_menu_page_ + 2) % 3;
|
||||||
|
return;
|
||||||
|
case KEY_RIGHT:
|
||||||
|
help_menu_page_ = (help_menu_page_ + 1) % 3;
|
||||||
|
return;
|
||||||
|
case 't':
|
||||||
|
if (help_menu_page_ != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto_reload_chat_history_ = !auto_reload_chat_history_;
|
||||||
|
persist_config();
|
||||||
|
status_line_ = auto_reload_chat_history_ ? "Auto-reload history enabled."
|
||||||
|
: "Auto-reload history disabled.";
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::draw_help_menu(int height, int width) {
|
||||||
|
const int menu_width = std::min(width - 4, 92);
|
||||||
|
const int menu_height = std::min(height - 4, 24);
|
||||||
|
const int top = std::max(1, (height - menu_height) / 2);
|
||||||
|
const int left = std::max(1, (width - menu_width) / 2);
|
||||||
|
|
||||||
|
WINDOW *window = newwin(menu_height, menu_width, top, left);
|
||||||
|
if (window == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
box(window, 0, 0);
|
||||||
|
const char *title =
|
||||||
|
help_menu_page_ == 0 ? " Help " : (help_menu_page_ == 1 ? " Settings " : " About ");
|
||||||
|
mvwprintw(window, 0, 2, "%s", title);
|
||||||
|
const std::string page_tabs =
|
||||||
|
help_menu_page_ == 0 ? "[1] Help 2 Settings 3 About"
|
||||||
|
: (help_menu_page_ == 1 ? "1 Help [2] Settings 3 About"
|
||||||
|
: "1 Help 2 Settings [3] About");
|
||||||
|
mvwaddnstr(window, 1, 2, page_tabs.c_str(), menu_width - 4);
|
||||||
|
mvwhline(window, 2, 1, ACS_HLINE, menu_width - 2);
|
||||||
|
const auto help_item = [](const std::string &key, const std::string &description) {
|
||||||
|
std::string line = " " + key;
|
||||||
|
const int key_width = std::max(16, utf8_display_width(key));
|
||||||
|
const int padding = std::max(2, key_width - utf8_display_width(key) + 2);
|
||||||
|
line.append(static_cast<std::size_t>(padding), ' ');
|
||||||
|
line += description;
|
||||||
|
return line;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
if (help_menu_page_ == 0) {
|
||||||
|
lines = {
|
||||||
|
"Navigation",
|
||||||
|
help_item("? / h", "Open or close help"),
|
||||||
|
help_item("Tab", "Switch focus"),
|
||||||
|
help_item("Enter", "Open selected chat"),
|
||||||
|
help_item("Up / Down", "Move chats or attachment selection"),
|
||||||
|
help_item("PgUp / PgDn", "Scroll messages"),
|
||||||
|
"",
|
||||||
|
"Compose",
|
||||||
|
help_item("i", "Compose a message"),
|
||||||
|
help_item("a", "Prepare reply to latest"),
|
||||||
|
help_item(">r <msg> [text]", "Prepare a reply"),
|
||||||
|
help_item(">paste [caption]", "Send clipboard image"),
|
||||||
|
"",
|
||||||
|
"Message Actions",
|
||||||
|
help_item(">f <msg...>", "Forward messages"),
|
||||||
|
help_item(">e <msg> <text>", "Edit a message"),
|
||||||
|
help_item(">d <msg...>", "Delete your messages"),
|
||||||
|
help_item("m / o", "Attachments menu / open selected"),
|
||||||
|
};
|
||||||
|
} else if (help_menu_page_ == 1) {
|
||||||
|
lines = {
|
||||||
|
"Settings",
|
||||||
|
std::string(" [") + (auto_reload_chat_history_ ? 'x' : ' ') +
|
||||||
|
"] Auto-reload open chat history [t]",
|
||||||
|
"",
|
||||||
|
"Behavior",
|
||||||
|
" On: Enter in the message pane reloads history.",
|
||||||
|
" Off: Press r to reload the current chat manually.",
|
||||||
|
"",
|
||||||
|
"Menu",
|
||||||
|
help_item("1 / 2 / 3", "Switch help pages"),
|
||||||
|
help_item("Left / Right", "Cycle pages"),
|
||||||
|
help_item("Esc", "Close the help overlay"),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
lines = {
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const int content_top = 3;
|
||||||
|
const int content_height = menu_height - 6;
|
||||||
|
if (help_menu_page_ == 2) {
|
||||||
|
int row = content_top;
|
||||||
|
for (const auto &line : kShinoaBanner) {
|
||||||
|
if (row >= content_top + content_height) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const int x = std::max(2, (menu_width - static_cast<int>(line.size())) / 2);
|
||||||
|
mvwaddnstr(window, row++, x, line.c_str(), menu_width - x - 1);
|
||||||
|
}
|
||||||
|
const std::vector<std::string> about_lines = {
|
||||||
|
"",
|
||||||
|
"Telegram TUI with message refs, reply prep, attachment navigation,",
|
||||||
|
"and keyboard-first chat workflows.",
|
||||||
|
};
|
||||||
|
for (const auto &line : about_lines) {
|
||||||
|
if (row >= content_top + content_height) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const int x = std::max(2, (menu_width - static_cast<int>(line.size())) / 2);
|
||||||
|
mvwaddnstr(window, row++, x, line.c_str(), menu_width - x - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (std::size_t i = 0; i < lines.size() && static_cast<int>(i) < content_height;
|
||||||
|
++i) {
|
||||||
|
mvwaddnstr(window, static_cast<int>(i) + content_top, 2, lines[i].c_str(),
|
||||||
|
menu_width - 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mvwhline(window, menu_height - 3, 1, ACS_HLINE, menu_width - 2);
|
||||||
|
mvwaddnstr(window, menu_height - 2, 2, "Esc close", menu_width - 4);
|
||||||
|
wrefresh(window);
|
||||||
|
delwin(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace telegram_tui
|
||||||
396
src/app_input.cpp
Normal file
396
src/app_input.cpp
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
#include "app.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <climits>
|
||||||
|
#include <cwchar>
|
||||||
|
#include <cwctype>
|
||||||
|
|
||||||
|
#include <curses.h>
|
||||||
|
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
namespace telegram_tui {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::optional<int> mapped_layout_hotkey(wchar_t ch) {
|
||||||
|
switch (std::towlower(ch)) {
|
||||||
|
case L'й':
|
||||||
|
return 'q';
|
||||||
|
case L'р':
|
||||||
|
return 'h';
|
||||||
|
case L'ш':
|
||||||
|
return 'i';
|
||||||
|
case L'к':
|
||||||
|
return 'r';
|
||||||
|
case L'щ':
|
||||||
|
return 'o';
|
||||||
|
default:
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string utf8_from_wchar(wchar_t ch) {
|
||||||
|
char buffer[MB_LEN_MAX] = {};
|
||||||
|
std::mbstate_t state = std::mbstate_t{};
|
||||||
|
const std::size_t size = std::wcrtomb(buffer, ch, &state);
|
||||||
|
if (size == static_cast<std::size_t>(-1)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return std::string(buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void App::start_reply_to_latest_message() {
|
||||||
|
const auto chat_id = open_chat_id();
|
||||||
|
if (!chat_id.has_value()) {
|
||||||
|
status_line_ = "Open chat first.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto chat_it = chats_.find(*chat_id);
|
||||||
|
if (chat_it == chats_.end() || chat_it->second.messages.empty()) {
|
||||||
|
status_line_ = "No message available to reply to.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_reply_input(*chat_id, chat_it->second.messages.back().id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::open_forward_target_menu(std::int64_t source_chat_id,
|
||||||
|
std::vector<std::int64_t> message_ids) {
|
||||||
|
if (source_chat_id == 0 || message_ids.empty()) {
|
||||||
|
status_line_ = "Forward command needs one or more valid message refs.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const std::vector<std::int64_t> target_chat_ids = forward_target_chat_ids();
|
||||||
|
if (target_chat_ids.empty()) {
|
||||||
|
status_line_ = "No non-channel chats available to forward to.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
forward_source_chat_id_ = source_chat_id;
|
||||||
|
forward_message_ids_ = std::move(message_ids);
|
||||||
|
forward_target_index_ = selected_chat_index_;
|
||||||
|
if (forward_target_index_ < 0 ||
|
||||||
|
forward_target_index_ >= static_cast<int>(target_chat_ids.size())) {
|
||||||
|
forward_target_index_ = 0;
|
||||||
|
}
|
||||||
|
if (target_chat_ids.size() > 1 &&
|
||||||
|
target_chat_ids[static_cast<std::size_t>(forward_target_index_)] == source_chat_id) {
|
||||||
|
if (forward_target_index_ + 1 < static_cast<int>(target_chat_ids.size())) {
|
||||||
|
++forward_target_index_;
|
||||||
|
} else {
|
||||||
|
--forward_target_index_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
forward_target_menu_open_ = true;
|
||||||
|
status_line_ = forward_message_ids_.size() == 1
|
||||||
|
? "Select a chat to forward the message to."
|
||||||
|
: "Select a chat to forward the messages to.";
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::handle_key(int ch) {
|
||||||
|
if (input_mode_ != InputMode::None) {
|
||||||
|
handle_input_key(ch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (forward_target_menu_open_) {
|
||||||
|
handle_forward_target_menu_key(ch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attachment_viewer_open_) {
|
||||||
|
handle_attachment_viewer_key(ch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attachment_action_menu_open_) {
|
||||||
|
handle_attachment_action_menu_key(ch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attachments_menu_open_) {
|
||||||
|
handle_attachments_menu_key(ch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (help_menu_open_) {
|
||||||
|
handle_help_menu_key(ch);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ch) {
|
||||||
|
case 'q':
|
||||||
|
running_ = false;
|
||||||
|
return;
|
||||||
|
case 'h':
|
||||||
|
case '?':
|
||||||
|
case KEY_F(1):
|
||||||
|
help_menu_open_ = true;
|
||||||
|
help_menu_page_ = 0;
|
||||||
|
status_line_ = "Help.";
|
||||||
|
return;
|
||||||
|
case 'm':
|
||||||
|
if (!open_chat_id().has_value()) {
|
||||||
|
status_line_ = "Open chat first.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attachments_menu_open_ = true;
|
||||||
|
attachment_category_index_ = 0;
|
||||||
|
attachment_selection_index_ = 0;
|
||||||
|
status_line_ = "Attachments.";
|
||||||
|
return;
|
||||||
|
case '\t':
|
||||||
|
focus_ = focus_ == FocusPane::Chats ? FocusPane::Messages : FocusPane::Chats;
|
||||||
|
return;
|
||||||
|
case 'r':
|
||||||
|
if (focus_ == FocusPane::Chats) {
|
||||||
|
request_more_chats();
|
||||||
|
} else {
|
||||||
|
request_open_chat_history(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case 'a':
|
||||||
|
if (focus_ == FocusPane::Messages) {
|
||||||
|
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.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!open_chat_id().has_value()) {
|
||||||
|
status_line_ = "Open chat first.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
start_input(InputMode::Compose, "Message", false);
|
||||||
|
status_line_ = "Compose mode.";
|
||||||
|
return;
|
||||||
|
case KEY_UP:
|
||||||
|
if (focus_ == FocusPane::Chats && selected_chat_index_ > 0) {
|
||||||
|
--selected_chat_index_;
|
||||||
|
} else if (focus_ == FocusPane::Messages) {
|
||||||
|
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) {
|
||||||
|
if (!move_message_attachment_selection(1) && message_scroll_ > 0) {
|
||||||
|
--message_scroll_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case KEY_PPAGE:
|
||||||
|
message_scroll_ += 10;
|
||||||
|
return;
|
||||||
|
case KEY_NPAGE:
|
||||||
|
message_scroll_ = std::max(0, message_scroll_ - 10);
|
||||||
|
return;
|
||||||
|
case '\n':
|
||||||
|
case KEY_ENTER:
|
||||||
|
if (focus_ == FocusPane::Chats) {
|
||||||
|
const auto chat_id = highlighted_chat_id();
|
||||||
|
if (chat_id.has_value()) {
|
||||||
|
const bool is_same_chat = open_chat_id_ == *chat_id;
|
||||||
|
set_open_chat(*chat_id);
|
||||||
|
if (is_same_chat) {
|
||||||
|
const auto chat_it = chats_.find(*chat_id);
|
||||||
|
const bool should_request_history =
|
||||||
|
auto_reload_chat_history_ ||
|
||||||
|
(chat_it != chats_.end() &&
|
||||||
|
(!chat_it->second.history_loaded ||
|
||||||
|
chat_it->second.messages.empty()));
|
||||||
|
const bool requested = should_request_history &&
|
||||||
|
request_open_chat_history(true);
|
||||||
|
if (!requested) {
|
||||||
|
status_line_ = "Opened chat.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (auto_reload_chat_history_) {
|
||||||
|
const bool requested = request_open_chat_history(true);
|
||||||
|
if (!requested) {
|
||||||
|
status_line_ = "Opened chat.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
status_line_ = "History reload is manual. Press r to reload.";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case KEY_RESIZE:
|
||||||
|
default:
|
||||||
|
if (focus_ == FocusPane::Messages && authorized_ && open_chat_id().has_value() &&
|
||||||
|
(ch >= 32 && ch <= 126)) {
|
||||||
|
input_buffer_.clear();
|
||||||
|
input_buffer_.push_back(static_cast<char>(ch));
|
||||||
|
input_cursor_ = input_buffer_.size();
|
||||||
|
start_input(InputMode::Compose, "Message", false);
|
||||||
|
status_line_ = "Compose mode.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::handle_wide_char(wint_t ch) {
|
||||||
|
const std::string encoded = utf8_from_wchar(static_cast<wchar_t>(ch));
|
||||||
|
if (encoded.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input_mode_ != InputMode::None) {
|
||||||
|
input_buffer_.insert(input_cursor_, encoded);
|
||||||
|
input_cursor_ += encoded.size();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachment_viewer_open_ || attachment_action_menu_open_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (const auto hotkey = mapped_layout_hotkey(static_cast<wchar_t>(ch));
|
||||||
|
hotkey.has_value()) {
|
||||||
|
handle_key(*hotkey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachments_menu_open_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (forward_target_menu_open_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (help_menu_open_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (focus_ == FocusPane::Messages && authorized_ && open_chat_id().has_value()) {
|
||||||
|
input_buffer_ = encoded;
|
||||||
|
input_cursor_ = input_buffer_.size();
|
||||||
|
start_input(InputMode::Compose, "Message", false);
|
||||||
|
status_line_ = "Compose mode.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::handle_input_key(int ch) {
|
||||||
|
switch (ch) {
|
||||||
|
case 27:
|
||||||
|
if (input_mode_ == InputMode::Compose) {
|
||||||
|
clear_input();
|
||||||
|
status_line_ = "Compose cancelled.";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case '\n':
|
||||||
|
case KEY_ENTER:
|
||||||
|
submit_input();
|
||||||
|
return;
|
||||||
|
case KEY_BACKSPACE:
|
||||||
|
case 127:
|
||||||
|
case 8:
|
||||||
|
if (input_cursor_ > 0) {
|
||||||
|
const std::size_t previous = utf8_prev_index(input_buffer_, input_cursor_);
|
||||||
|
input_buffer_.erase(previous, input_cursor_ - previous);
|
||||||
|
input_cursor_ = previous;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case KEY_DC:
|
||||||
|
if (input_cursor_ < input_buffer_.size()) {
|
||||||
|
const std::size_t next = utf8_next_index(input_buffer_, input_cursor_);
|
||||||
|
input_buffer_.erase(input_cursor_, next - input_cursor_);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case KEY_LEFT:
|
||||||
|
input_cursor_ = utf8_prev_index(input_buffer_, input_cursor_);
|
||||||
|
return;
|
||||||
|
case KEY_RIGHT:
|
||||||
|
input_cursor_ = utf8_next_index(input_buffer_, input_cursor_);
|
||||||
|
return;
|
||||||
|
case KEY_HOME:
|
||||||
|
input_cursor_ = 0;
|
||||||
|
return;
|
||||||
|
case KEY_END:
|
||||||
|
input_cursor_ = input_buffer_.size();
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
if (ch >= 32 && ch <= 126) {
|
||||||
|
input_buffer_.insert(input_cursor_, 1, static_cast<char>(ch));
|
||||||
|
++input_cursor_;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::handle_forward_target_menu_key(int ch) {
|
||||||
|
const std::vector<std::int64_t> target_chat_ids = forward_target_chat_ids();
|
||||||
|
if (target_chat_ids.empty()) {
|
||||||
|
forward_target_menu_open_ = false;
|
||||||
|
status_line_ = "No non-channel chats available to forward to.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ch) {
|
||||||
|
case 27:
|
||||||
|
case 'q':
|
||||||
|
forward_target_menu_open_ = false;
|
||||||
|
forward_message_ids_.clear();
|
||||||
|
status_line_ = "Cancelled forwarding.";
|
||||||
|
return;
|
||||||
|
case KEY_UP:
|
||||||
|
if (forward_target_index_ > 0) {
|
||||||
|
--forward_target_index_;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case KEY_DOWN:
|
||||||
|
if (forward_target_index_ + 1 < static_cast<int>(target_chat_ids.size())) {
|
||||||
|
++forward_target_index_;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case KEY_PPAGE:
|
||||||
|
forward_target_index_ = std::max(0, forward_target_index_ - 10);
|
||||||
|
return;
|
||||||
|
case KEY_NPAGE:
|
||||||
|
forward_target_index_ = std::min(static_cast<int>(target_chat_ids.size()) - 1,
|
||||||
|
forward_target_index_ + 10);
|
||||||
|
return;
|
||||||
|
case '\n':
|
||||||
|
case KEY_ENTER:
|
||||||
|
case ' ': {
|
||||||
|
const std::int64_t target_chat_id =
|
||||||
|
target_chat_ids[static_cast<std::size_t>(forward_target_index_)];
|
||||||
|
forward_target_menu_open_ = false;
|
||||||
|
forward_message(forward_source_chat_id_, forward_message_ids_, target_chat_id);
|
||||||
|
forward_message_ids_.clear();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::int64_t> App::forward_target_chat_ids() const {
|
||||||
|
std::vector<std::int64_t> target_chat_ids;
|
||||||
|
target_chat_ids.reserve(sorted_chat_ids_.size());
|
||||||
|
for (const auto chat_id : sorted_chat_ids_) {
|
||||||
|
const auto chat_it = chats_.find(chat_id);
|
||||||
|
if (chat_it != chats_.end() && chat_it->second.is_channel) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
target_chat_ids.push_back(chat_id);
|
||||||
|
}
|
||||||
|
return target_chat_ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace telegram_tui
|
||||||
869
src/app_messages.cpp
Normal file
869
src/app_messages.cpp
Normal file
@@ -0,0 +1,869 @@
|
|||||||
|
#include "app.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <curses.h>
|
||||||
|
|
||||||
|
#include "app_ui.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
namespace telegram_tui {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct TextInsertion {
|
||||||
|
std::size_t offset = 0;
|
||||||
|
int order = 0;
|
||||||
|
std::size_t span_length = 0;
|
||||||
|
std::string text;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
int body_indent = 0;
|
||||||
|
std::string timestamp;
|
||||||
|
std::string message_id;
|
||||||
|
std::string sender;
|
||||||
|
std::string forward_info;
|
||||||
|
std::string via_bot;
|
||||||
|
std::string reply_prefix;
|
||||||
|
std::string reply_ref;
|
||||||
|
std::string body;
|
||||||
|
std::string attachment_hint;
|
||||||
|
std::string state;
|
||||||
|
bool is_continuation = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool is_word_char(unsigned char ch) {
|
||||||
|
return std::isalnum(ch) || ch == '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool starts_with_at(const std::string &text, std::size_t index, const char *prefix) {
|
||||||
|
const std::size_t prefix_size = std::char_traits<char>::length(prefix);
|
||||||
|
return text.compare(index, prefix_size, prefix) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
short sender_color_pair(const std::string &sender) {
|
||||||
|
static constexpr short sender_pairs[] = {
|
||||||
|
kColorPairSenderBlue, kColorPairSenderCyan, kColorPairSenderGreen,
|
||||||
|
kColorPairSenderYellow, kColorPairSenderMagenta, kColorPairSenderRed,
|
||||||
|
};
|
||||||
|
|
||||||
|
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::int64_t file_size_from_object(const json &file) {
|
||||||
|
return std::max(safe_i64(file, "size"), safe_i64(file, "expected_size"));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int32_t file_id_from_object(const json &file) {
|
||||||
|
return safe_i32(file, "id");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string local_path_from_file(const json &file) {
|
||||||
|
return safe_string(file.value("local", json::object()), "path");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool file_can_be_downloaded(const json &file) {
|
||||||
|
return file.value("local", json::object()).value("can_be_downloaded", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool file_can_be_deleted(const json &file) {
|
||||||
|
return file.value("local", json::object()).value("can_be_deleted", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int64_t file_downloaded_size(const json &file) {
|
||||||
|
return safe_i64(file.value("local", json::object()), "downloaded_size");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool file_is_downloading_active(const json &file) {
|
||||||
|
return file.value("local", json::object()).value("is_downloading_active", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool file_is_downloaded(const json &file) {
|
||||||
|
return file.value("local", json::object()).value("is_downloading_completed", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
AttachmentInfo attachment_from_file(AttachmentType type, std::string name, const json &file,
|
||||||
|
std::int64_t size_bytes) {
|
||||||
|
AttachmentInfo attachment;
|
||||||
|
attachment.type = type;
|
||||||
|
attachment.name = std::move(name);
|
||||||
|
attachment.size_bytes = size_bytes;
|
||||||
|
attachment.downloaded_size = file_downloaded_size(file);
|
||||||
|
attachment.file_id = file_id_from_object(file);
|
||||||
|
attachment.local_path = local_path_from_file(file);
|
||||||
|
attachment.is_downloading_active = file_is_downloading_active(file);
|
||||||
|
attachment.can_be_downloaded = file_can_be_downloaded(file);
|
||||||
|
attachment.can_be_deleted = file_can_be_deleted(file);
|
||||||
|
attachment.is_downloaded = file_is_downloaded(file);
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
|
||||||
|
json largest_photo_file(const json &photo) {
|
||||||
|
json best_file = json::object();
|
||||||
|
std::int64_t best_size = -1;
|
||||||
|
const auto &sizes = photo.value("sizes", json::array());
|
||||||
|
if (!sizes.is_array()) {
|
||||||
|
return best_file;
|
||||||
|
}
|
||||||
|
for (const auto &item : sizes) {
|
||||||
|
const json file = item.value("photo", json::object());
|
||||||
|
const std::int64_t size = file_size_from_object(file);
|
||||||
|
if (size > best_size) {
|
||||||
|
best_size = size;
|
||||||
|
best_file = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int64_t photo_size_from_object(const json &photo) {
|
||||||
|
std::int64_t size = 0;
|
||||||
|
const auto &sizes = photo.value("sizes", json::array());
|
||||||
|
if (!sizes.is_array()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
for (const auto &item : sizes) {
|
||||||
|
size = std::max(size, file_size_from_object(item.value("photo", json::object())));
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_colored_span(int y, int &x, int max_x, const std::string &text, chtype attrs) {
|
||||||
|
if (x >= max_x || text.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int remaining = max_x - x;
|
||||||
|
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(), static_cast<int>(count));
|
||||||
|
attroff(attrs);
|
||||||
|
x += used_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string entity_prefix(const json &type) {
|
||||||
|
const std::string type_name = safe_string(type, "@type");
|
||||||
|
if (type_name == "textEntityTypeBold") {
|
||||||
|
return "*";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeItalic") {
|
||||||
|
return "_";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeCode" || type_name == "textEntityTypePre" ||
|
||||||
|
type_name == "textEntityTypePreCode") {
|
||||||
|
return "`";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeStrikethrough") {
|
||||||
|
return "~";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeUnderline") {
|
||||||
|
return "__";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeSpoiler") {
|
||||||
|
return "||";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeTextUrl") {
|
||||||
|
return "[";
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string entity_suffix(const json &type) {
|
||||||
|
const std::string type_name = safe_string(type, "@type");
|
||||||
|
if (type_name == "textEntityTypeBold") {
|
||||||
|
return "*";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeItalic") {
|
||||||
|
return "_";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeCode" || type_name == "textEntityTypePre" ||
|
||||||
|
type_name == "textEntityTypePreCode") {
|
||||||
|
return "`";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeStrikethrough") {
|
||||||
|
return "~";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeUnderline") {
|
||||||
|
return "__";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeSpoiler") {
|
||||||
|
return "||";
|
||||||
|
}
|
||||||
|
if (type_name == "textEntityTypeTextUrl") {
|
||||||
|
const std::string url = safe_string(type, "url");
|
||||||
|
return url.empty() ? "]" : "](" + url + ")";
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_message_body(int y, int &x, int max_x, const std::string &text) {
|
||||||
|
std::size_t i = 0;
|
||||||
|
while (i < text.size() && x < max_x) {
|
||||||
|
if (starts_with_at(text, i, "http://") || starts_with_at(text, i, "https://") ||
|
||||||
|
starts_with_at(text, i, "t.me/")) {
|
||||||
|
std::size_t j = i;
|
||||||
|
while (j < text.size() &&
|
||||||
|
!std::isspace(static_cast<unsigned char>(text[j]))) {
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
draw_colored_span(y, x, max_x, text.substr(i, j - i),
|
||||||
|
COLOR_PAIR(kColorPairLink) | A_UNDERLINE);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text[i] == '@') {
|
||||||
|
std::size_t j = i + 1;
|
||||||
|
while (j < text.size() &&
|
||||||
|
is_word_char(static_cast<unsigned char>(text[j]))) {
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
if (j > i + 1) {
|
||||||
|
draw_colored_span(y, x, max_x, text.substr(i, j - i),
|
||||||
|
COLOR_PAIR(kColorPairLink) | A_BOLD);
|
||||||
|
i = j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text[i] == '*' || text[i] == '_' || text[i] == '`' || text[i] == '~' ||
|
||||||
|
text[i] == '[' || text[i] == ']' || text[i] == '(' || text[i] == ')') {
|
||||||
|
draw_colored_span(y, x, max_x, text.substr(i, 1),
|
||||||
|
COLOR_PAIR(kColorPairMarkdown) | A_BOLD);
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t j = i;
|
||||||
|
while (j < text.size()) {
|
||||||
|
if (starts_with_at(text, j, "http://") ||
|
||||||
|
starts_with_at(text, j, "https://") ||
|
||||||
|
starts_with_at(text, j, "t.me/") || text[j] == '@' || text[j] == '*' ||
|
||||||
|
text[j] == '_' || text[j] == '`' || text[j] == '~' || text[j] == '[' ||
|
||||||
|
text[j] == ']' || text[j] == '(' || text[j] == ')') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++j;
|
||||||
|
}
|
||||||
|
draw_colored_span(y, x, max_x, text.substr(i, j - i), A_NORMAL);
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string truncate_to_width(std::string text, int max_width) {
|
||||||
|
if (max_width <= 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (static_cast<int>(text.size()) <= max_width) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
if (max_width <= 3) {
|
||||||
|
return text.substr(0, static_cast<std::size_t>(max_width));
|
||||||
|
}
|
||||||
|
text.resize(static_cast<std::size_t>(max_width - 3));
|
||||||
|
text += "...";
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string App::sender_label(const json &sender) const {
|
||||||
|
const std::string type = safe_string(sender, "@type");
|
||||||
|
if (type == "messageSenderUser") {
|
||||||
|
const auto user_id = safe_i64(sender, "user_id");
|
||||||
|
auto it = users_.find(user_id);
|
||||||
|
if (it != users_.end()) {
|
||||||
|
return it->second.display_name();
|
||||||
|
}
|
||||||
|
if (user_id == my_user_id_) {
|
||||||
|
return "You";
|
||||||
|
}
|
||||||
|
return "User " + std::to_string(user_id);
|
||||||
|
}
|
||||||
|
if (type == "messageSenderChat") {
|
||||||
|
const auto chat_id = safe_i64(sender, "chat_id");
|
||||||
|
auto it = chats_.find(chat_id);
|
||||||
|
if (it != chats_.end() && !it->second.title.empty()) {
|
||||||
|
return it->second.title;
|
||||||
|
}
|
||||||
|
return "Chat " + std::to_string(chat_id);
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string App::forward_origin_label(const json &forward_info) const {
|
||||||
|
if (!forward_info.is_object()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const json origin = forward_info.value("origin", json::object());
|
||||||
|
const std::string origin_type = safe_string(origin, "@type");
|
||||||
|
if (origin_type.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (origin_type == "messageOriginUser") {
|
||||||
|
const auto user_id = safe_i64(origin, "sender_user_id");
|
||||||
|
const auto user_it = users_.find(user_id);
|
||||||
|
if (user_it != users_.end()) {
|
||||||
|
return "fwd " + user_it->second.display_name();
|
||||||
|
}
|
||||||
|
return "fwd User " + std::to_string(user_id);
|
||||||
|
}
|
||||||
|
if (origin_type == "messageOriginHiddenUser") {
|
||||||
|
const std::string sender_name = safe_string(origin, "sender_name");
|
||||||
|
return sender_name.empty() ? "fwd" : "fwd " + sender_name;
|
||||||
|
}
|
||||||
|
if (origin_type == "messageOriginChat") {
|
||||||
|
const auto sender_chat_id = safe_i64(origin, "sender_chat_id");
|
||||||
|
std::string label;
|
||||||
|
const auto chat_it = chats_.find(sender_chat_id);
|
||||||
|
if (chat_it != chats_.end() && !chat_it->second.title.empty()) {
|
||||||
|
label = chat_it->second.title;
|
||||||
|
} else {
|
||||||
|
label = "Chat " + std::to_string(sender_chat_id);
|
||||||
|
}
|
||||||
|
const std::string signature = safe_string(origin, "author_signature");
|
||||||
|
if (!signature.empty()) {
|
||||||
|
label += " (" + signature + ")";
|
||||||
|
}
|
||||||
|
return "fwd " + label;
|
||||||
|
}
|
||||||
|
if (origin_type == "messageOriginChannel") {
|
||||||
|
const auto channel_chat_id = safe_i64(origin, "chat_id");
|
||||||
|
std::string label;
|
||||||
|
const auto chat_it = chats_.find(channel_chat_id);
|
||||||
|
if (chat_it != chats_.end() && !chat_it->second.title.empty()) {
|
||||||
|
label = chat_it->second.title;
|
||||||
|
} else {
|
||||||
|
label = "Channel " + std::to_string(channel_chat_id);
|
||||||
|
}
|
||||||
|
const std::string signature = safe_string(origin, "author_signature");
|
||||||
|
if (!signature.empty()) {
|
||||||
|
label += " (" + signature + ")";
|
||||||
|
}
|
||||||
|
return "fwd " + label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "fwd";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string App::via_bot_label(std::int64_t via_bot_user_id) const {
|
||||||
|
if (via_bot_user_id == 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto user_it = users_.find(via_bot_user_id);
|
||||||
|
if (user_it != users_.end()) {
|
||||||
|
if (!user_it->second.username.empty()) {
|
||||||
|
return "@" + user_it->second.username;
|
||||||
|
}
|
||||||
|
return user_it->second.display_name();
|
||||||
|
}
|
||||||
|
return "via";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string App::preview_message(const json &message) const {
|
||||||
|
if (!message.is_object()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return single_line(content_to_text(message.value("content", json::object()), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AttachmentInfo> App::parse_attachment(const json &content) const {
|
||||||
|
const std::string type = safe_string(content, "@type");
|
||||||
|
if (type == "messagePhoto") {
|
||||||
|
const json photo_file = largest_photo_file(content.value("photo", json::object()));
|
||||||
|
return attachment_from_file(
|
||||||
|
AttachmentType::Photo, "photo", photo_file,
|
||||||
|
photo_size_from_object(content.value("photo", json::object())));
|
||||||
|
}
|
||||||
|
if (type == "messageVideo") {
|
||||||
|
const json video = content.value("video", json::object());
|
||||||
|
std::string name = safe_string(video, "file_name");
|
||||||
|
if (name.empty()) {
|
||||||
|
name = "video";
|
||||||
|
}
|
||||||
|
const json file = video.value("video", json::object());
|
||||||
|
return attachment_from_file(AttachmentType::Video, name, file,
|
||||||
|
file_size_from_object(file));
|
||||||
|
}
|
||||||
|
if (type == "messageVideoNote") {
|
||||||
|
const json file =
|
||||||
|
content.value("video_note", json::object()).value("video", json::object());
|
||||||
|
return attachment_from_file(AttachmentType::Video, "video note", file,
|
||||||
|
file_size_from_object(file));
|
||||||
|
}
|
||||||
|
if (type == "messageDocument") {
|
||||||
|
const json document = content.value("document", json::object());
|
||||||
|
std::string name = safe_string(document, "file_name");
|
||||||
|
if (name.empty()) {
|
||||||
|
name = "document";
|
||||||
|
}
|
||||||
|
const json file = document.value("document", json::object());
|
||||||
|
return attachment_from_file(AttachmentType::Document, name, file,
|
||||||
|
file_size_from_object(file));
|
||||||
|
}
|
||||||
|
if (type == "messageAudio") {
|
||||||
|
const json audio = content.value("audio", json::object());
|
||||||
|
std::string name = safe_string(audio, "title");
|
||||||
|
if (name.empty()) {
|
||||||
|
name = safe_string(audio, "file_name");
|
||||||
|
}
|
||||||
|
if (name.empty()) {
|
||||||
|
name = "audio";
|
||||||
|
}
|
||||||
|
const json file = audio.value("audio", json::object());
|
||||||
|
return attachment_from_file(AttachmentType::Audio, name, file,
|
||||||
|
file_size_from_object(file));
|
||||||
|
}
|
||||||
|
if (type == "messageVoiceNote") {
|
||||||
|
const json file =
|
||||||
|
content.value("voice_note", json::object()).value("voice", json::object());
|
||||||
|
return attachment_from_file(AttachmentType::Voice, "voice note", file,
|
||||||
|
file_size_from_object(file));
|
||||||
|
}
|
||||||
|
if (type == "messageAnimation") {
|
||||||
|
const json animation = content.value("animation", json::object());
|
||||||
|
std::string name = safe_string(animation, "file_name");
|
||||||
|
if (name.empty()) {
|
||||||
|
name = "animation";
|
||||||
|
}
|
||||||
|
const json file = animation.value("animation", json::object());
|
||||||
|
return attachment_from_file(AttachmentType::Animation, name, file,
|
||||||
|
file_size_from_object(file));
|
||||||
|
}
|
||||||
|
if (type == "messageSticker") {
|
||||||
|
const json sticker = content.value("sticker", json::object());
|
||||||
|
std::string name = safe_string(sticker, "emoji");
|
||||||
|
if (name.empty()) {
|
||||||
|
name = "sticker";
|
||||||
|
}
|
||||||
|
const json file = sticker.value("sticker", json::object());
|
||||||
|
return attachment_from_file(AttachmentType::Sticker, name, file,
|
||||||
|
file_size_from_object(file));
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string App::content_to_text(const json &content, bool decorate) const {
|
||||||
|
auto format_text = [&](const json &formatted) {
|
||||||
|
const std::string base = safe_string(formatted, "text");
|
||||||
|
if (!decorate || !formatted.contains("entities") ||
|
||||||
|
!formatted.at("entities").is_array()) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<TextInsertion> insertions;
|
||||||
|
for (const auto &entity : formatted.at("entities")) {
|
||||||
|
if (!entity.is_object()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t utf16_offset =
|
||||||
|
static_cast<std::size_t>(safe_i32(entity, "offset"));
|
||||||
|
const std::size_t utf16_length =
|
||||||
|
static_cast<std::size_t>(safe_i32(entity, "length"));
|
||||||
|
if (utf16_length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const json type = entity.value("type", json::object());
|
||||||
|
const std::string prefix = entity_prefix(type);
|
||||||
|
const std::string suffix = entity_suffix(type);
|
||||||
|
if (prefix.empty() && suffix.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t start =
|
||||||
|
utf8_byte_index_from_utf16_offset(base, utf16_offset);
|
||||||
|
const std::size_t end = utf8_byte_index_from_utf16_offset(
|
||||||
|
base, utf16_offset + utf16_length);
|
||||||
|
if (start > end || end > base.size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!suffix.empty()) {
|
||||||
|
insertions.push_back({end, 0, utf16_length, suffix});
|
||||||
|
}
|
||||||
|
if (!prefix.empty()) {
|
||||||
|
insertions.push_back({start, 1, utf16_length, prefix});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(insertions.begin(), insertions.end(),
|
||||||
|
[](const TextInsertion &lhs, const TextInsertion &rhs) {
|
||||||
|
if (lhs.offset != rhs.offset) {
|
||||||
|
return lhs.offset > rhs.offset;
|
||||||
|
}
|
||||||
|
if (lhs.order != rhs.order) {
|
||||||
|
return lhs.order < rhs.order;
|
||||||
|
}
|
||||||
|
return lhs.span_length < rhs.span_length;
|
||||||
|
});
|
||||||
|
|
||||||
|
std::string decorated = base;
|
||||||
|
for (const auto &insertion : insertions) {
|
||||||
|
decorated.insert(insertion.offset, insertion.text);
|
||||||
|
}
|
||||||
|
return decorated;
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::string type = safe_string(content, "@type");
|
||||||
|
if (type == "messageText") {
|
||||||
|
return format_text(content.value("text", json::object()));
|
||||||
|
}
|
||||||
|
if (type == "messagePhoto") {
|
||||||
|
const std::string caption = format_text(content.value("caption", json::object()));
|
||||||
|
return caption.empty() ? "[photo]" : "[photo] " + caption;
|
||||||
|
}
|
||||||
|
if (type == "messageVideo") {
|
||||||
|
const std::string caption = format_text(content.value("caption", json::object()));
|
||||||
|
return caption.empty() ? "[video]" : "[video] " + caption;
|
||||||
|
}
|
||||||
|
if (type == "messageDocument") {
|
||||||
|
const std::string file_name =
|
||||||
|
safe_string(content.value("document", json::object()), "file_name");
|
||||||
|
const std::string caption = format_text(content.value("caption", json::object()));
|
||||||
|
if (!caption.empty()) {
|
||||||
|
return file_name.empty() ? "[document] " + caption
|
||||||
|
: "[document] " + file_name + " " + caption;
|
||||||
|
}
|
||||||
|
return file_name.empty() ? "[document]" : "[document] " + file_name;
|
||||||
|
}
|
||||||
|
if (type == "messageSticker") {
|
||||||
|
const std::string emoji =
|
||||||
|
safe_string(content.value("sticker", json::object()), "emoji");
|
||||||
|
return emoji.empty() ? "[sticker]" : "[sticker] " + emoji;
|
||||||
|
}
|
||||||
|
if (type == "messageAnimation") {
|
||||||
|
const std::string caption = format_text(content.value("caption", json::object()));
|
||||||
|
return caption.empty() ? "[animation]" : "[animation] " + caption;
|
||||||
|
}
|
||||||
|
if (type == "messageVoiceNote") {
|
||||||
|
return "[voice message]";
|
||||||
|
}
|
||||||
|
if (type == "messageAudio") {
|
||||||
|
const std::string caption = format_text(content.value("caption", json::object()));
|
||||||
|
return caption.empty() ? "[audio]" : "[audio] " + caption;
|
||||||
|
}
|
||||||
|
if (type == "messageCall") {
|
||||||
|
return "[call]";
|
||||||
|
}
|
||||||
|
if (type == "messagePoll") {
|
||||||
|
return "[poll] " + safe_string(content.value("poll", json::object()), "question");
|
||||||
|
}
|
||||||
|
if (type == "messageUnsupported") {
|
||||||
|
return "[unsupported message]";
|
||||||
|
}
|
||||||
|
return "[" + type + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageInfo App::parse_message(const json &message) const {
|
||||||
|
MessageInfo parsed;
|
||||||
|
parsed.id = safe_i64(message, "id");
|
||||||
|
parsed.date = safe_i32(message, "date");
|
||||||
|
parsed.is_outgoing = message.value("is_outgoing", false);
|
||||||
|
const json reply_to = message.value("reply_to", json::object());
|
||||||
|
if (safe_string(reply_to, "@type") == "messageReplyToMessage") {
|
||||||
|
parsed.reply_to_message_id = safe_i64(reply_to, "message_id");
|
||||||
|
}
|
||||||
|
parsed.forward_info = forward_origin_label(message.value("forward_info", json::object()));
|
||||||
|
parsed.sender = parsed.is_outgoing
|
||||||
|
? "You"
|
||||||
|
: sender_label(message.value("sender_id", json::object()));
|
||||||
|
const json content = message.value("content", json::object());
|
||||||
|
if (const auto attachment = parse_attachment(content); attachment.has_value()) {
|
||||||
|
parsed.has_attachment = true;
|
||||||
|
parsed.attachment = *attachment;
|
||||||
|
}
|
||||||
|
parsed.text = content_to_text(content, true);
|
||||||
|
parsed.via_bot = via_bot_label(safe_i64(message, "via_bot_user_id"));
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::draw_message_pane(int top, int height, int left, int width) {
|
||||||
|
for (int row = 0; row < height; ++row) {
|
||||||
|
mvhline(top + row, left, ' ', width);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto chat_id = open_chat_id();
|
||||||
|
if (!chat_id.has_value()) {
|
||||||
|
mvprintw(top, left + 1, "%s",
|
||||||
|
authorized_ ? "Open a chat with Enter." : "Waiting for authorization.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto chat_it = chats_.find(*chat_id);
|
||||||
|
if (chat_it == chats_.end()) {
|
||||||
|
mvprintw(top, left + 1, "Chat %lld unavailable.", static_cast<long long>(*chat_id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ChatInfo &chat = chat_it->second;
|
||||||
|
const std::string header =
|
||||||
|
truncate_to_width(format_open_chat_header(chat), std::max(1, width - 2));
|
||||||
|
attron(COLOR_PAIR(kColorPairOpenChat) | A_BOLD);
|
||||||
|
mvprintw(top, left + 1, "%s", header.c_str());
|
||||||
|
attroff(COLOR_PAIR(kColorPairOpenChat) | A_BOLD);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
current_day = message_day;
|
||||||
|
RenderLine separator;
|
||||||
|
separator.is_day_separator = true;
|
||||||
|
separator.body = message_day;
|
||||||
|
lines.push_back(separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string state = "[in]";
|
||||||
|
if (message.is_outgoing) {
|
||||||
|
state = (chat.last_read_outbox_message_id != 0 &&
|
||||||
|
message.id <= chat.last_read_outbox_message_id)
|
||||||
|
? "[read]"
|
||||||
|
: "[sent]";
|
||||||
|
} else if (chat.last_read_inbox_message_id != 0 &&
|
||||||
|
message.id > chat.last_read_inbox_message_id) {
|
||||||
|
state = "[new]";
|
||||||
|
}
|
||||||
|
|
||||||
|
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_prefix = message.reply_to_message_id != 0 ? "re " : "";
|
||||||
|
const std::string reply_ref =
|
||||||
|
message.reply_to_message_id != 0
|
||||||
|
? format_message_ref(chat, message.reply_to_message_id)
|
||||||
|
: "";
|
||||||
|
const std::string forward_info = message.forward_info;
|
||||||
|
const std::string via_bot = 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 (!forward_info.empty()) {
|
||||||
|
prefix += forward_info + " ";
|
||||||
|
}
|
||||||
|
if (!via_bot.empty()) {
|
||||||
|
prefix += via_bot + " ";
|
||||||
|
}
|
||||||
|
if (!reply_ref.empty()) {
|
||||||
|
prefix += reply_prefix + reply_ref + " ";
|
||||||
|
}
|
||||||
|
const int wrap_width = std::max(10, width - 3 - reserved_state_width);
|
||||||
|
const int prefix_width = utf8_display_width(prefix);
|
||||||
|
const int body_indent =
|
||||||
|
prefix_width < std::max(8, wrap_width - 12) ? prefix_width : 0;
|
||||||
|
std::vector<std::string> wrapped = wrap_text(
|
||||||
|
message.text, std::max(10, wrap_width - prefix_width -
|
||||||
|
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.body_indent = body_indent;
|
||||||
|
line.timestamp = timestamp;
|
||||||
|
line.message_id = message_id;
|
||||||
|
line.sender = message.sender;
|
||||||
|
line.forward_info = forward_info;
|
||||||
|
line.via_bot = via_bot;
|
||||||
|
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.body_indent = body_indent;
|
||||||
|
first_line.timestamp = timestamp;
|
||||||
|
first_line.message_id = message_id;
|
||||||
|
first_line.sender = message.sender;
|
||||||
|
first_line.forward_info = forward_info;
|
||||||
|
first_line.via_bot = via_bot;
|
||||||
|
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);
|
||||||
|
|
||||||
|
for (std::size_t i = 1; i < wrapped.size(); ++i) {
|
||||||
|
RenderLine continuation;
|
||||||
|
continuation.body_indent = body_indent;
|
||||||
|
continuation.body = wrapped[i];
|
||||||
|
continuation.is_continuation = true;
|
||||||
|
lines.push_back(continuation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines.empty()) {
|
||||||
|
mvprintw(top + 1, left + 1, "%s", "No messages loaded.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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())) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &line = lines[line_index];
|
||||||
|
const int y = top + 1 + row;
|
||||||
|
if (line.is_day_separator) {
|
||||||
|
const int separator_width = std::max(0, width - 2);
|
||||||
|
std::string label = " " + line.body + " ";
|
||||||
|
if (static_cast<int>(label.size()) > separator_width) {
|
||||||
|
label.resize(static_cast<std::size_t>(separator_width));
|
||||||
|
}
|
||||||
|
attron(COLOR_PAIR(kColorPairTimestamp) | A_DIM);
|
||||||
|
mvhline(y, left + 1, ACS_HLINE, separator_width);
|
||||||
|
mvaddnstr(y,
|
||||||
|
left + 1 +
|
||||||
|
std::max(0, (separator_width -
|
||||||
|
static_cast<int>(label.size())) /
|
||||||
|
2),
|
||||||
|
label.c_str(), separator_width);
|
||||||
|
attroff(COLOR_PAIR(kColorPairTimestamp) | A_DIM);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x = left + 1;
|
||||||
|
const int max_x = left + width;
|
||||||
|
if (!line.is_continuation) {
|
||||||
|
chtype state_attrs = A_DIM;
|
||||||
|
if (line.state == "[new]") {
|
||||||
|
state_attrs = COLOR_PAIR(kColorPairMarkdown) | A_BOLD;
|
||||||
|
} else if (line.state == "[read]") {
|
||||||
|
state_attrs = COLOR_PAIR(kColorPairOpenChat) | A_BOLD;
|
||||||
|
} else if (line.state == "[sent]") {
|
||||||
|
state_attrs = COLOR_PAIR(kColorPairTimestamp) | A_DIM;
|
||||||
|
}
|
||||||
|
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, 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);
|
||||||
|
if (!line.forward_info.empty()) {
|
||||||
|
draw_colored_span(y, x, content_max_x, line.forward_info,
|
||||||
|
COLOR_PAIR(kColorPairMarkdown) | A_DIM);
|
||||||
|
draw_colored_span(y, x, content_max_x, " ", A_NORMAL);
|
||||||
|
}
|
||||||
|
if (!line.via_bot.empty()) {
|
||||||
|
draw_colored_span(y, x, content_max_x, line.via_bot,
|
||||||
|
COLOR_PAIR(kColorPairMarkdown) | A_DIM);
|
||||||
|
draw_colored_span(y, x, content_max_x, " ", A_NORMAL);
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
x = std::min(max_x, left + 1 + line.body_indent);
|
||||||
|
draw_message_body(y, x, max_x, line.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace telegram_tui
|
||||||
262
src/app_shell.cpp
Normal file
262
src/app_shell.cpp
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
#include "app.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <clocale>
|
||||||
|
|
||||||
|
#include <curses.h>
|
||||||
|
|
||||||
|
#include "app_ui.h"
|
||||||
|
#include "util.h"
|
||||||
|
|
||||||
|
namespace telegram_tui {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string truncate_to_width(std::string text, int max_width) {
|
||||||
|
if (max_width <= 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (static_cast<int>(text.size()) <= max_width) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
if (max_width <= 3) {
|
||||||
|
return text.substr(0, static_cast<std::size_t>(max_width));
|
||||||
|
}
|
||||||
|
text.resize(static_cast<std::size_t>(max_width - 3));
|
||||||
|
text += "...";
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void App::init_curses() {
|
||||||
|
std::setlocale(LC_ALL, "");
|
||||||
|
initscr();
|
||||||
|
init_colors();
|
||||||
|
cbreak();
|
||||||
|
noecho();
|
||||||
|
keypad(stdscr, TRUE);
|
||||||
|
#ifdef NCURSES_VERSION
|
||||||
|
set_escdelay(25);
|
||||||
|
#endif
|
||||||
|
timeout(kPollTimeoutMs);
|
||||||
|
curs_set(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::init_colors() {
|
||||||
|
if (!has_colors()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
start_color();
|
||||||
|
use_default_colors();
|
||||||
|
init_pair(kColorPairSenderBlue, COLOR_BLUE, -1);
|
||||||
|
init_pair(kColorPairSenderCyan, COLOR_CYAN, -1);
|
||||||
|
init_pair(kColorPairSenderGreen, COLOR_GREEN, -1);
|
||||||
|
init_pair(kColorPairSenderYellow, COLOR_YELLOW, -1);
|
||||||
|
init_pair(kColorPairSenderMagenta, COLOR_MAGENTA, -1);
|
||||||
|
init_pair(kColorPairSenderRed, COLOR_RED, -1);
|
||||||
|
init_pair(kColorPairLink, COLOR_CYAN, -1);
|
||||||
|
init_pair(kColorPairMarkdown, COLOR_MAGENTA, -1);
|
||||||
|
init_pair(kColorPairTimestamp, COLOR_YELLOW, -1);
|
||||||
|
init_pair(kColorPairOpenChat, COLOR_GREEN, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::shutdown_curses() {
|
||||||
|
endwin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::draw() {
|
||||||
|
erase();
|
||||||
|
|
||||||
|
int height = 0;
|
||||||
|
int width = 0;
|
||||||
|
getmaxyx(stdscr, height, width);
|
||||||
|
if (height < 8 || width < 40) {
|
||||||
|
mvprintw(0, 0, "Terminal too small.");
|
||||||
|
refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int header_y = 0;
|
||||||
|
const int content_top = 1;
|
||||||
|
const int footer_y = height - 2;
|
||||||
|
const int input_y = height - 1;
|
||||||
|
const int content_height = footer_y - content_top;
|
||||||
|
const int chat_width = std::max(24, width / 3);
|
||||||
|
const int message_width = width - chat_width - 1;
|
||||||
|
|
||||||
|
attron(A_REVERSE);
|
||||||
|
mvhline(header_y, 0, ' ', width);
|
||||||
|
const std::string header_label = use_test_dc_ ? "shinoa [TEST DC]" : "shinoa";
|
||||||
|
mvprintw(header_y, 1, "%s", header_label.c_str());
|
||||||
|
const std::string auth_label = authorized_ ? "ready" : current_auth_label();
|
||||||
|
const int auth_x = std::max(1, width - static_cast<int>(auth_label.size()) - 2);
|
||||||
|
mvprintw(header_y, auth_x, "%s", auth_label.c_str());
|
||||||
|
attroff(A_REVERSE);
|
||||||
|
|
||||||
|
mvvline(content_top, chat_width, ACS_VLINE, content_height);
|
||||||
|
draw_chat_pane(content_top, content_height, chat_width);
|
||||||
|
draw_message_pane(content_top, content_height, chat_width + 1, message_width);
|
||||||
|
|
||||||
|
attron(A_REVERSE);
|
||||||
|
mvhline(footer_y, 0, ' ', width);
|
||||||
|
const std::string footer_status = use_test_dc_ ? "[TEST DC] " + status_line_ : status_line_;
|
||||||
|
mvprintw(footer_y, 1, "%s", footer_status.c_str());
|
||||||
|
const std::string footer_hint = "? for help";
|
||||||
|
const int footer_hint_x = std::max(1, width - static_cast<int>(footer_hint.size()) - 2);
|
||||||
|
if (footer_hint_x > 1 + static_cast<int>(footer_status.size())) {
|
||||||
|
attron(A_DIM);
|
||||||
|
mvprintw(footer_y, footer_hint_x, "%s", footer_hint.c_str());
|
||||||
|
attroff(A_DIM);
|
||||||
|
}
|
||||||
|
attroff(A_REVERSE);
|
||||||
|
|
||||||
|
const std::string help =
|
||||||
|
input_mode_ == InputMode::None
|
||||||
|
? "? for help"
|
||||||
|
: input_prompt_ + ": " +
|
||||||
|
(input_hidden_ ? std::string(input_buffer_.size(), '*')
|
||||||
|
: input_buffer_);
|
||||||
|
mvhline(input_y, 0, ' ', width);
|
||||||
|
mvprintw(input_y, 0, "%s", help.c_str());
|
||||||
|
if (input_mode_ != InputMode::None) {
|
||||||
|
const std::string prefix = input_prompt_ + ": ";
|
||||||
|
const int cursor_x = std::min(
|
||||||
|
width - 1, utf8_display_width(prefix) +
|
||||||
|
utf8_display_width(input_buffer_, input_cursor_));
|
||||||
|
move(input_y, std::max(0, cursor_x));
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
if (attachment_viewer_open_) {
|
||||||
|
draw_attachment_viewer(height, width);
|
||||||
|
} else if (attachment_action_menu_open_) {
|
||||||
|
draw_attachment_action_menu(height, width);
|
||||||
|
} else if (attachments_menu_open_) {
|
||||||
|
draw_attachments_menu(height, width);
|
||||||
|
} else if (forward_target_menu_open_) {
|
||||||
|
draw_forward_target_menu(height, width);
|
||||||
|
} else if (help_menu_open_) {
|
||||||
|
draw_help_menu(height, width);
|
||||||
|
} else {
|
||||||
|
clear_attachment_preview_graphics();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void App::draw_forward_target_menu(int height, int width) {
|
||||||
|
const std::vector<std::int64_t> target_chat_ids = forward_target_chat_ids();
|
||||||
|
if (target_chat_ids.empty()) {
|
||||||
|
forward_target_menu_open_ = false;
|
||||||
|
status_line_ = "No non-channel chats available to forward to.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int menu_width = std::min(width - 4, 72);
|
||||||
|
const int menu_height = std::min(height - 4, 20);
|
||||||
|
const int top = std::max(1, (height - menu_height) / 2);
|
||||||
|
const int left = std::max(1, (width - menu_width) / 2);
|
||||||
|
|
||||||
|
WINDOW *window = newwin(menu_height, menu_width, top, left);
|
||||||
|
if (window == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forward_target_index_ < 0) {
|
||||||
|
forward_target_index_ = 0;
|
||||||
|
}
|
||||||
|
if (forward_target_index_ >= static_cast<int>(target_chat_ids.size())) {
|
||||||
|
forward_target_index_ = static_cast<int>(target_chat_ids.size()) - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
box(window, 0, 0);
|
||||||
|
mvwprintw(window, 0, 2, " Forward To ");
|
||||||
|
|
||||||
|
std::string source_label =
|
||||||
|
forward_message_ids_.size() == 1
|
||||||
|
? "1 message"
|
||||||
|
: (std::to_string(forward_message_ids_.size()) + " messages");
|
||||||
|
const auto source_chat_it = chats_.find(forward_source_chat_id_);
|
||||||
|
if (source_chat_it != chats_.end()) {
|
||||||
|
if (forward_message_ids_.size() == 1) {
|
||||||
|
const auto message_index = find_message_index(source_chat_it->second,
|
||||||
|
forward_message_ids_.front());
|
||||||
|
if (message_index.has_value()) {
|
||||||
|
source_label =
|
||||||
|
"Message [" + std::to_string(*message_index + 1) + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!source_chat_it->second.title.empty()) {
|
||||||
|
source_label += " from " + source_chat_it->second.title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mvwaddnstr(window, 1, 2, truncate_to_width(source_label, menu_width - 4).c_str(),
|
||||||
|
menu_width - 4);
|
||||||
|
mvwhline(window, 2, 1, ACS_HLINE, menu_width - 2);
|
||||||
|
|
||||||
|
const int list_top = 3;
|
||||||
|
const int list_height = std::max(1, menu_height - 5);
|
||||||
|
int first_index = 0;
|
||||||
|
if (forward_target_index_ >= list_height) {
|
||||||
|
first_index = forward_target_index_ - list_height + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int row = 0; row < list_height; ++row) {
|
||||||
|
const int item_index = first_index + row;
|
||||||
|
const int y = list_top + row;
|
||||||
|
mvwhline(window, y, 1, ' ', menu_width - 2);
|
||||||
|
if (item_index >= static_cast<int>(target_chat_ids.size())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::int64_t chat_id = target_chat_ids[static_cast<std::size_t>(item_index)];
|
||||||
|
const auto chat_it = chats_.find(chat_id);
|
||||||
|
std::string label = chat_it != chats_.end() ? chat_it->second.title
|
||||||
|
: ("Chat " + std::to_string(chat_id));
|
||||||
|
if (chat_id == forward_source_chat_id_) {
|
||||||
|
label += " (current)";
|
||||||
|
}
|
||||||
|
if (item_index == forward_target_index_) {
|
||||||
|
wattron(window, A_REVERSE | A_BOLD);
|
||||||
|
}
|
||||||
|
mvwaddnstr(window, y, 2, truncate_to_width(label, menu_width - 4).c_str(),
|
||||||
|
menu_width - 4);
|
||||||
|
if (item_index == forward_target_index_) {
|
||||||
|
wattroff(window, A_REVERSE | A_BOLD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mvwaddnstr(window, menu_height - 1, 2, "Enter forward Esc cancel Up/Down move",
|
||||||
|
menu_width - 4);
|
||||||
|
wrefresh(window);
|
||||||
|
delwin(window);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string App::current_auth_label() const {
|
||||||
|
const std::string type = safe_string(authorization_state_, "@type");
|
||||||
|
if (type == "authorizationStateWaitTdlibParameters") {
|
||||||
|
return "need API";
|
||||||
|
}
|
||||||
|
if (type == "authorizationStateWaitPhoneNumber") {
|
||||||
|
return "need phone";
|
||||||
|
}
|
||||||
|
if (type == "authorizationStateWaitCode") {
|
||||||
|
return "need code";
|
||||||
|
}
|
||||||
|
if (type == "authorizationStateWaitEncryptionKey") {
|
||||||
|
return "unlock db";
|
||||||
|
}
|
||||||
|
if (type == "authorizationStateWaitPassword") {
|
||||||
|
return "need password";
|
||||||
|
}
|
||||||
|
if (type == "authorizationStateReady") {
|
||||||
|
return "ready";
|
||||||
|
}
|
||||||
|
if (type.empty()) {
|
||||||
|
return "starting";
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace telegram_tui
|
||||||
16
src/app_ui.h
Normal file
16
src/app_ui.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace telegram_tui {
|
||||||
|
|
||||||
|
constexpr short kColorPairSenderBlue = 1;
|
||||||
|
constexpr short kColorPairSenderCyan = 2;
|
||||||
|
constexpr short kColorPairSenderGreen = 3;
|
||||||
|
constexpr short kColorPairSenderYellow = 4;
|
||||||
|
constexpr short kColorPairSenderMagenta = 5;
|
||||||
|
constexpr short kColorPairSenderRed = 6;
|
||||||
|
constexpr short kColorPairLink = 7;
|
||||||
|
constexpr short kColorPairMarkdown = 8;
|
||||||
|
constexpr short kColorPairTimestamp = 9;
|
||||||
|
constexpr short kColorPairOpenChat = 10;
|
||||||
|
|
||||||
|
} // namespace telegram_tui
|
||||||
Reference in New Issue
Block a user