Compare commits

..

17 Commits

Author SHA1 Message Date
2ef9395882 build: autoupdate
Some checks failed
CI / Check spelling (pull_request) Successful in 17s
CI / Check coding style (pull_request) Successful in 33s
CI / Linux (ubuntu) (pull_request) Failing after 2m10s
CI / Linux (debian) (pull_request) Successful in 11m55s
CI / Linux (arch) (pull_request) Successful in 14m22s
2025-08-26 09:46:45 +00:00
a3e23c3514 Temporarily disable Fedora tests
All checks were successful
CI / Check spelling (pull_request) Successful in 15s
CI / Check coding style (pull_request) Successful in 28s
CI / Linux (arch) (pull_request) Successful in 11m52s
CI / Linux (ubuntu) (pull_request) Successful in 11m59s
CI / Linux (debian) (pull_request) Successful in 12m2s
CI / Check spelling (push) Successful in 17s
CI / Check coding style (push) Successful in 30s
CI / Linux (ubuntu) (push) Successful in 10m9s
CI / Linux (arch) (push) Successful in 12m4s
CI / Linux (debian) (push) Successful in 13m0s
They are failing on our runners due to issues with dependency resolution.
2025-08-07 03:27:14 +02:00
ad294cdd6d feat(editor): add asynchronous external editor support
Some checks failed
CI / Check spelling (pull_request) Successful in 18s
CI / Check coding style (pull_request) Successful in 31s
CI / Linux (fedora) (pull_request) Failing after 1m14s
CI / Linux (debian) (pull_request) Successful in 10m19s
CI / Linux (ubuntu) (pull_request) Successful in 10m42s
CI / Linux (arch) (pull_request) Successful in 13m2s
The synchronous `get_message_from_editor` blocked the main loop (`prof_run`) while launching an external editor like `vim`, halting network I/O in `session_process_events` and preventing incoming message reception.

Introduced `get_message_from_editor_async` for `cmd_editor` to run the editor asynchronously. It forks and execs the editor in a thread (`editor_thread`), suspending NCurses to free the terminal. The main loop skips `inp_readline` and `ui_update` via a new `background_mode` flag while the editor runs, allowing `session_process_events` to keep the connection alive.

On editor completion, `editor_process` (called per loop iteration) resumes NCurses with `ui_resize`, inserts the result into the readline buffer and clears `background_mode`.

Retained synchronous `get_message_from_editor` for ~20 existing code paths (e.g., `vcard_nickname`) to avoid breaking them.

Tested with `vim` and `nano`: confirms no rendering conflicts, messages received during editing, and seamless resume. Edge cases like editor crashes handled via error logging and seamless resume.
2025-08-07 03:03:43 +02:00
e1ce7ac630 fix(caps): Add features in caps_init to avoid redundant presence updates
All checks were successful
CI / Check spelling (pull_request) Successful in 1m26s
CI / Check coding style (pull_request) Successful in 1m37s
CI / Linux (debian) (pull_request) Successful in 9m54s
CI / Linux (ubuntu) (pull_request) Successful in 10m11s
CI / Linux (fedora) (pull_request) Successful in 14m56s
CI / Linux (arch) (pull_request) Successful in 16m38s
CI / Check spelling (push) Successful in 15s
CI / Check coding style (push) Successful in 26s
CI / Linux (debian) (push) Successful in 9m49s
CI / Linux (ubuntu) (push) Successful in 10m1s
CI / Linux (fedora) (push) Successful in 12m59s
CI / Linux (arch) (push) Successful in 13m51s
Previously, features like `XMPP_FEATURE_OMEMO_DEVICELIST_NOTIFY` were
added in separate init methods (e.g., `omemo_devicelist_subscribe`) via
`caps_add_feature`, triggering `cl_ev_presence_send` and sending multiple
"online" XMPP presence stanzas. This caused other clients to see users
as appearing "online" multiple times.

This change adds `STANZA_NS_MOOD_NOTIFY` and
`XMPP_FEATURE_OMEMO_DEVICELIST_NOTIFY` in `caps_init`, ensuring they are
in `prof_features` early. This allows `caps_add_feature` to exit early
when called for these features, avoiding redundant presence updates.
2025-07-19 18:33:03 +02:00
5ed8ec30f0 Update name and license
All checks were successful
CI / Linux (debian) (pull_request) Successful in 11m26s
CI / Linux (arch) (pull_request) Successful in 13m51s
CI / Linux (fedora) (pull_request) Successful in 12m27s
CI / Check coding style (pull_request) Successful in 30s
CI / Check spelling (pull_request) Successful in 14s
CI / Linux (ubuntu) (pull_request) Successful in 11m43s
CI / Linux (debian) (push) Successful in 11m31s
CI / Linux (arch) (push) Successful in 13m51s
CI / Linux (fedora) (push) Successful in 12m16s
CI / Check coding style (push) Successful in 29s
CI / Check spelling (push) Successful in 17s
CI / Linux (ubuntu) (push) Successful in 11m49s
2025-07-19 14:32:26 +02:00
Michael Vetter
091ff41c06 Merge pull request #2041 from profanity-im/some-improvements
All checks were successful
CI / Linux (debian) (pull_request) Successful in 13m45s
CI / Linux (arch) (pull_request) Successful in 16m48s
CI / Linux (fedora) (pull_request) Successful in 15m11s
CI / Check coding style (pull_request) Successful in 29s
CI / Check spelling (pull_request) Successful in 15s
CI / Linux (ubuntu) (pull_request) Successful in 13m50s
CI / Linux (debian) (push) Successful in 11m36s
CI / Linux (arch) (push) Successful in 14m0s
CI / Linux (fedora) (push) Successful in 12m25s
CI / Check coding style (push) Successful in 30s
CI / Check spelling (push) Successful in 16s
CI / Linux (ubuntu) (push) Successful in 11m55s
Summary:
- Print the final storage location when successfully decrypting a file with `aesgcm://` source.
- Less GString
- Re-factor cmd_presence()
- Improve const correctness
- Use correct free function. Fixes: 75d7663 ("Free wins summary list")

Pull Request Source: https://github.com/profanity-im/profanity/pull/2041

Author: Steffen Jaeckel <s@jaeckel.eu>
2025-07-19 00:13:49 +02:00
Steffen Jaeckel
b7afea7ec3 Use correct free function.
Fixes: 75d76638 ("Free wins summary list")

Author: Steffen Jaeckel <s@jaeckel.eu>
Signed-off-by: Steffen Jaeckel <s@jaeckel.eu>
2025-07-19 00:13:40 +02:00
7fcc14fdea build: change docs configuration to build XML
All checks were successful
CI / Check spelling (push) Successful in 17s
CI / Check coding style (push) Successful in 33s
CI / Linux (debian) (push) Successful in 9m54s
CI / Linux (ubuntu) (push) Successful in 12m4s
CI / Linux (fedora) (push) Successful in 13m45s
CI / Linux (arch) (push) Successful in 15m20s
So it can be used as a part of CI for the website
2025-07-08 01:52:56 +02:00
d07ca9953d fix: reduce logging
All checks were successful
CI / Check spelling (push) Successful in 16s
CI / Check coding style (push) Successful in 31s
CI / Linux (ubuntu) (push) Successful in 10m12s
CI / Linux (arch) (push) Successful in 12m3s
CI / Linux (debian) (push) Successful in 12m12s
CI / Linux (fedora) (push) Successful in 17m6s
If we can't debug_log because we couldn't obtain some data, but otherwise the app is working, "warning" level is clearly excessive.
2025-07-05 20:54:06 +02:00
f636090fd9 fix(window): preserve scroll offset on page_down and refactor
All checks were successful
CI / Check spelling (pull_request) Successful in 29s
CI / Check coding style (pull_request) Successful in 2m22s
CI / Linux (debian) (pull_request) Successful in 11m28s
CI / Linux (fedora) (pull_request) Successful in 16m46s
CI / Linux (arch) (pull_request) Successful in 20m29s
CI / Linux (ubuntu) (pull_request) Successful in 35m7s
CI / Check spelling (push) Successful in 17s
CI / Check coding style (push) Successful in 31s
CI / Linux (ubuntu) (push) Successful in 11m11s
CI / Linux (debian) (push) Successful in 13m26s
CI / Linux (fedora) (push) Successful in 16m10s
CI / Linux (arch) (push) Successful in 32m49s
When paging down beyond the visible chat buffer, the scroll position could
jump unexpectedly. This occurred because the visual distance from the bottom
(e.g. 6 lines until the last message) wasn’t preserved after loading new
messages from the DB.

This patch fixes that by capturing the visual offset (`current_offset`) before
the DB fetch and reapplying it after, based on the y-position of the final
buffer entry. This ensures consistent and smooth scroll transitions, even
as message heights vary.

In addition to fixing the visual glitch, this commit introduces a targeted
performance optimization: once the bottom of the chat has been reached
(`WIN_SCROLL_REACHED_BOTTOM`), we skip expensive operations entirely —
including datetime lookups and entry formatting — during further page_down
events. This reduces performance stalls
during rapid paging (e.g. holding the Page Down key at the bottom of the scrolling area).

This update also adapts to the recent change in `chatwin_db_history()`,
which now returns a `db_history_result_t` instead of `gboolean`, allowing
correct differentiation between an empty result and an actual DB error.

Together, these changes:
- Fix a scroll-jump bug in `page_down`
- Improve performance and responsiveness during rapid navigation
- Align with improved DB result handling
2025-07-02 19:27:15 +02:00
a5eab3cad5 build: Upgrade valgrind suppression file
Use newer version of Python's .supp file.
2025-07-02 15:21:44 +02:00
6871a4d46f Fix memory leak in _handle_string_or_none_result
All checks were successful
CI / Check spelling (pull_request) Successful in 4m23s
CI / Check coding style (pull_request) Successful in 10m37s
CI / Linux (fedora) (pull_request) Successful in 11m27s
CI / Linux (debian) (pull_request) Successful in 12m48s
CI / Linux (arch) (pull_request) Successful in 16m54s
CI / Linux (ubuntu) (pull_request) Successful in 48m55s
The function did not decref the Python object on type error paths,
resulting in a reference count leak. This caused memory to accumulate
over time. Added Py_XDECREF on all early return paths to properly free
the Python object reference and prevent leaks.
2025-07-02 15:05:44 +02:00
f824cde45f fix(mam, log): improve datetime handling and memory management in MAM and log fetching
- Introduce static helper `_truncate_datetime_suffix()` to safely trim datetime strings, removing unwanted suffixes like timezone offsets
- Replace manual string management with auto_gchar and g_strdup for safer, clearer ownership and to prevent leaks
- Add safety checks and logging warnings for unexpected datetime string lengths or null pointers
- Refactor _mam_rsm_id_handler to use the helper function and updated string handling
- Change log_database_get_previous_chat parameters for consistent ownership semantics, avoiding double frees and mem leaks
- Overall improve stability and prevent memory leaks during log database queries
2025-07-02 14:59:31 +02:00
40b7a12543 Clarify and improve win_page_up() scrolling offset adjustment for smoother paging
All checks were successful
CI / Check spelling (pull_request) Successful in 17s
CI / Check coding style (pull_request) Successful in 29s
CI / Linux (ubuntu) (pull_request) Successful in 11m6s
CI / Linux (debian) (pull_request) Successful in 13m9s
CI / Linux (fedora) (pull_request) Successful in 15m14s
CI / Linux (arch) (pull_request) Successful in 37m59s
CI / Check spelling (push) Successful in 16s
CI / Check coding style (push) Successful in 32s
CI / Linux (debian) (push) Successful in 9m53s
CI / Linux (ubuntu) (push) Successful in 10m9s
CI / Linux (fedora) (push) Successful in 15m8s
CI / Linux (arch) (push) Successful in 15m28s
This commit refines the existing logic in win_page_up() by:
- Improving comments to clearly explain the rationale behind adjusting
  the scroll offset relative to the first buffer entry’s visual position,
  helping future maintainers understand why this is necessary.
- Fixing offset recalculation to better handle cases where older messages
  with variable heights are loaded from the DB, improving scroll smoothness.
- Changing the logging of negative *page_start values from warning to debug,
  recognizing that this can be a normal scenario when insufficient history is loaded.
- Simplifying some conditionals and renaming variables for clearer intent.

No changes yet applied to win_page_down(), but similar improvements could
be considered in the future.

Overall, this enhances the robustness and user experience of scrolling up
in chat windows, while preserving existing functional logic.
2025-07-01 20:53:18 +02:00
6a394ae2e5 fix: Increase scrollback buffer height for chat rendering (PAD_SIZE)
f27fa98 commit introduced issue of scrolling seemingly randomly being stuck and the following message flood in the logs: "WRN: Ncurses Overflow..."

A pad is an off-screen buffer — larger than the visible terminal screen — that you can scroll, render parts of, or update selectively.

Unlike regular windows, a pad is not tied to screen size. You can create a pad that's 1,000 lines tall, even if your terminal is only 40 lines tall.

This commit addresses the issue by increasing PAD_SIZE to a reasonable number,
allowing messages from buffer to be displayed without overflowing NCurses' window size (maxy).

Minor change: add .cache folder to gitignore
2025-07-01 16:42:03 +02:00
43bab6ff8f t push origin master
All checks were successful
CI / Check spelling (push) Successful in 16s
CI / Check coding style (push) Successful in 30s
CI / Linux (ubuntu) (push) Successful in 9m56s
CI / Linux (fedora) (push) Successful in 10m29s
CI / Linux (arch) (push) Successful in 11m56s
CI / Linux (debian) (push) Successful in 12m11s
Merge branch 'fix/theme-mem-leak'
2025-06-24 17:29:55 +02:00
d5d5dc5180 ref(theme): ensure clean reinitialization and explicitly initialize static vars
All checks were successful
CI / Check spelling (pull_request) Successful in 17s
CI / Check coding style (pull_request) Successful in 32s
CI / Linux (debian) (pull_request) Successful in 9m44s
CI / Linux (fedora) (pull_request) Successful in 10m40s
CI / Linux (ubuntu) (pull_request) Successful in 12m19s
CI / Linux (arch) (pull_request) Successful in 15m41s
Improves code robustness by calling _theme_close() in theme_init() when the
theme was already initialized, preventing potential memory leaks or
inconsistent state.

Closes #4
2025-06-24 16:22:48 +02:00
33 changed files with 523 additions and 223 deletions

View File

@@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
flavor: [arch, debian, fedora, ubuntu]
flavor: [arch, debian, ubuntu]
name: Linux
steps:

3
.gitignore vendored
View File

@@ -44,6 +44,9 @@ src/gitversion.h.in
src/stamp-h1
src/plugins/profapi.lo
# clangd
.cache/
# out-of-tree build folders
build*/

View File

@@ -1865,7 +1865,7 @@ MAN_LINKS = NO
# captures the structure of the code including all documentation.
# The default value is: NO.
GENERATE_XML = NO
GENERATE_XML = YES
# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of

View File

@@ -1 +1 @@
sphinx-apidoc -f -o . src && make html
sphinx-apidoc -f -o . src && make xml

View File

@@ -1,6 +1,6 @@
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.69])
AC_PREREQ([2.72])
AC_INIT([profanity],[0.15.0],[jubalh@iodoru.org],[profanity],[https://profanity-im.github.io/])
AC_CONFIG_AUX_DIR([build-aux])
AC_CONFIG_MACRO_DIR([m4])
@@ -95,7 +95,7 @@ PKG_CHECK_MODULES([curl], [libcurl >= 7.62.0], [],
PKG_CHECK_MODULES([SQLITE], [sqlite3 >= 3.22.0], [],
[AC_MSG_ERROR([sqlite3 3.22.0 or higher is required])])
ACX_PTHREAD([], [AC_MSG_ERROR([pthread is required])])
AX_PTHREAD([],[AC_MSG_ERROR([pthread is required])])
AS_IF([test "x$PTHREAD_CC" != x], [ CC="$PTHREAD_CC" ])
### plugins

View File

@@ -5,7 +5,7 @@
#
# This file is a combination of our own rules and
# * the glib2 suppressions file that usually can be found at /usr/share/glib-2.0/valgrind/glib.supp
# * python suppressions file from http://svn.python.org/projects/python/trunk/Misc/valgrind-python.supp
# * python suppressions file from https://github.com/python/cpython/blob/main/Misc/valgrind-python.supp
#
{
@@ -1557,6 +1557,10 @@
...
fun:xdg_mime_init
}
# Python Valgrind .supp start
# taken from https://github.com/python/cpython/blob/main/Misc/valgrind-python.supp
#
# This is a valgrind suppression file that should be used when using valgrind.
#
@@ -1654,6 +1658,49 @@
fun:COMMENT_THIS_LINE_TO_DISABLE_LEAK_WARNING
}
#
# Leaks: dlopen() called without dlclose()
#
{
dlopen() called without dlclose()
Memcheck:Leak
fun:malloc
fun:malloc
fun:strdup
fun:_dl_load_cache_lookup
}
{
dlopen() called without dlclose()
Memcheck:Leak
fun:malloc
fun:malloc
fun:strdup
fun:_dl_map_object
}
{
dlopen() called without dlclose()
Memcheck:Leak
fun:malloc
fun:*
fun:_dl_new_object
}
{
dlopen() called without dlclose()
Memcheck:Leak
fun:calloc
fun:*
fun:_dl_new_object
}
{
dlopen() called without dlclose()
Memcheck:Leak
fun:calloc
fun:*
fun:_dl_check_map_versions
}
#
# Non-python specific leaks
#
@@ -2048,6 +2095,10 @@
fun:PyUnicode_FSConverter
}
# Python Valgrind .supp end
# Actual GTK things
{
GtkWidgetClass action GPtrArray

View File

@@ -1715,8 +1715,15 @@ cmd_help(ProfWin* window, const char* const command, gchar** args)
gboolean
cmd_about(ProfWin* window, const char* const command, gchar** args)
{
ProfWin* console = wins_get_console();
cons_show("");
cons_about();
win_println(console, THEME_DEFAULT, "-", "Licensed under GNU GPL v3.");
win_println(console, THEME_DEFAULT, "-", "CProof is an independent unaffiliated fork of Profanity.");
win_println(console, THEME_DEFAULT, "-", "Profanity: Copyright (C) 20122019 James Booth, 20192025 Michael Vetter");
win_println(console, THEME_DEFAULT, "-", "Copyright (C) 2025 CProof Developers");
win_println(console, THEME_DEFAULT, "-", "");
return TRUE;
}
@@ -5233,54 +5240,43 @@ cmd_console(ProfWin* window, const char* const command, gchar** args)
gboolean
cmd_presence(ProfWin* window, const char* const command, gchar** args)
{
if (strcmp(args[0], "console") != 0 && strcmp(args[0], "chat") != 0 && strcmp(args[0], "room") != 0 && strcmp(args[0], "titlebar") != 0) {
cons_bad_cmd_usage(command);
return TRUE;
}
if (strcmp(args[0], "titlebar") == 0) {
_cmd_set_boolean_preference(args[1], "Contact presence", PREF_PRESENCE);
return TRUE;
}
if (strcmp(args[1], "all") != 0 && strcmp(args[1], "online") != 0 && strcmp(args[1], "none") != 0) {
const struct presence_preferences
{
preference_t pref;
const char *name, *description;
} presence_prefs[] = {
{ .pref = PREF_STATUSES_CONSOLE, .name = "console", .description = "the console" },
{ .pref = PREF_STATUSES_CHAT, .name = "chat", .description = "chat windows" },
{ .pref = PREF_STATUSES_MUC, .name = "room", .description = "chat room windows" },
};
size_t n = 0;
for (; n < ARRAY_SIZE(presence_prefs); ++n) {
const struct presence_preferences* pp = &presence_prefs[n];
if (g_strcmp0(args[0], pp->name) != 0)
continue;
if (strcmp(args[1], "all") == 0) {
cons_show("All presence updates will appear in %s.", pp->description);
} else if (strcmp(args[1], "online") == 0) {
cons_show("Only %s presence updates will appear in %s.",
pp->pref == PREF_STATUSES_MUC ? "join/leave" : "online/offline",
pp->description);
} else if (strcmp(args[1], "none") == 0) {
cons_show("Presence updates will not appear in %s.", pp->description);
} else {
cons_bad_cmd_usage(command);
return TRUE;
}
prefs_set_string(pp->pref, args[1]);
break;
}
if (n == ARRAY_SIZE(presence_prefs)) {
cons_bad_cmd_usage(command);
return TRUE;
}
if (strcmp(args[0], "console") == 0) {
prefs_set_string(PREF_STATUSES_CONSOLE, args[1]);
if (strcmp(args[1], "all") == 0) {
cons_show("All presence updates will appear in the console.");
} else if (strcmp(args[1], "online") == 0) {
cons_show("Only online/offline presence updates will appear in the console.");
} else {
cons_show("Presence updates will not appear in the console.");
}
}
if (strcmp(args[0], "chat") == 0) {
prefs_set_string(PREF_STATUSES_CHAT, args[1]);
if (strcmp(args[1], "all") == 0) {
cons_show("All presence updates will appear in chat windows.");
} else if (strcmp(args[1], "online") == 0) {
cons_show("Only online/offline presence updates will appear in chat windows.");
} else {
cons_show("Presence updates will not appear in chat windows.");
}
}
if (strcmp(args[0], "room") == 0) {
prefs_set_string(PREF_STATUSES_MUC, args[1]);
if (strcmp(args[1], "all") == 0) {
cons_show("All presence updates will appear in chat room windows.");
} else if (strcmp(args[1], "online") == 0) {
cons_show("Only join/leave presence updates will appear in chat room windows.");
} else {
cons_show("Presence updates will not appear in chat room windows.");
}
}
return TRUE;
}
@@ -5314,7 +5310,8 @@ cmd_time(ProfWin* window, const char* const command, gchar** args)
};
gboolean redraw = FALSE;
gboolean set_all = g_strcmp0(args[0], "all") == 0;
for (size_t n = 0; n < ARRAY_SIZE(time_prefs); ++n) {
size_t n = 0;
for (; n < ARRAY_SIZE(time_prefs); ++n) {
if (!set_all && g_strcmp0(args[0], time_prefs[n].name) != 0)
continue;
const struct time_preferences* tp = &time_prefs[n];
@@ -5355,6 +5352,9 @@ cmd_time(ProfWin* window, const char* const command, gchar** args)
return TRUE;
}
}
if (!set_all && n == ARRAY_SIZE(time_prefs)) {
cons_bad_cmd_usage(command);
}
if (redraw)
ui_redraw();
return TRUE;
@@ -9527,16 +9527,7 @@ cmd_change_password(ProfWin* window, const char* const command, gchar** args)
gboolean
cmd_editor(ProfWin* window, const char* const command, gchar** args)
{
auto_gchar gchar* message = NULL;
if (get_message_from_editor(NULL, &message)) {
return TRUE;
}
rl_insert_text(message);
ui_resize();
rl_point = rl_end;
rl_forced_update_display();
get_message_from_editor_async(NULL);
return TRUE;
}

View File

@@ -171,6 +171,8 @@ typedef enum {
RESOURCE_XA
} resource_presence_t;
extern gboolean background_mode;
gboolean string_to_verbosity(const char* cmd, int* verbosity, gchar** err_msg);
gboolean create_dir(const char* name);

View File

@@ -86,9 +86,9 @@ _theme_close(void)
void
theme_init(const char* const theme_name)
{
if(defaults || theme_loc || theme) {
if (defaults || theme_loc || theme) {
log_warning("theme_init(): Detected non-NULL state (theme=%p, theme_loc=%p, defaults=%p). Cleaning up previous initialization.",
theme, theme_loc, defaults);
theme, theme_loc, defaults);
_theme_close();
}

View File

@@ -222,6 +222,7 @@ out:
void
log_database_close(void)
{
log_debug("log_database_close() called");
if (g_chatlog_database) {
sqlite3_close(g_chatlog_database);
sqlite3_shutdown();
@@ -318,19 +319,25 @@ log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_las
// Query previous chats, constraints start_time and end_time. If end_time is
// null the current time is used. from_start gets first few messages if true
// otherwise the last ones. Flip flips the order of the results
GSList*
log_database_get_previous_chat(const gchar* const contact_barejid, const char* start_time, char* end_time, gboolean from_start, gboolean flip)
db_history_result_t
log_database_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result)
{
if (!g_chatlog_database) {
log_warning("log_database_get_previous_chat() called but db is not initialized");
return DB_RESPONSE_ERROR;
}
sqlite3_stmt* stmt = NULL;
const Jid* myjid = connection_get_jid();
if (!myjid->str)
return NULL;
if (!myjid->str) {
log_warning("log_database_get_previous_chat() called but no connection detected.");
return DB_RESPONSE_ERROR;
}
// Flip order when querying older pages
gchar* sort1 = from_start ? "ASC" : "DESC";
gchar* sort2 = !flip ? "ASC" : "DESC";
GDateTime* now = g_date_time_new_now_local();
auto_gchar gchar* end_date_fmt = end_time ? end_time : g_date_time_format_iso8601(now);
auto_gchar gchar* end_date_fmt = end_time ? g_strdup(end_time) : g_date_time_format_iso8601(now);
auto_sqlite gchar* query = sqlite3_mprintf("SELECT * FROM ("
"SELECT COALESCE(B.`message`, A.`message`) AS message, "
"A.`timestamp`, A.`from_jid`, A.`from_resource`, A.`to_jid`, A.`to_resource`, A.`type`, A.`encryption`, A.`stanza_id` FROM `ChatLogs` AS A "
@@ -346,18 +353,16 @@ log_database_get_previous_chat(const gchar* const contact_barejid, const char* s
g_date_time_unref(now);
if (!query) {
log_error("Could not allocate memory");
return NULL;
log_error("Could not allocate memory.");
return DB_RESPONSE_ERROR;
}
int rc = sqlite3_prepare_v2(g_chatlog_database, query, -1, &stmt, NULL);
if (rc != SQLITE_OK) {
log_error("SQLite error in log_database_get_previous_chat(): %s", sqlite3_errmsg(g_chatlog_database));
return NULL;
log_error("SQLite error in log_database_get_previous_chat(): (error code: %d) %s", rc, sqlite3_errmsg(g_chatlog_database));
return DB_RESPONSE_ERROR;
}
GSList* history = NULL;
while (sqlite3_step(stmt) == SQLITE_ROW) {
char* message = (char*)sqlite3_column_text(stmt, 0);
char* date = (char*)sqlite3_column_text(stmt, 1);
@@ -378,11 +383,11 @@ log_database_get_previous_chat(const gchar* const contact_barejid, const char* s
msg->type = _get_message_type_type(type);
msg->enc = _get_message_enc_type(encryption);
history = g_slist_append(history, msg);
*result = g_slist_append(*result, msg);
}
sqlite3_finalize(stmt);
return history;
return g_slist_length(*result) != 0 ? DB_RESPONSE_SUCCESS : DB_RESPONSE_EMPTY;
}
static const char*

View File

@@ -40,14 +40,20 @@
#include "config/account.h"
#include "xmpp/xmpp.h"
#define MESSAGES_TO_RETRIEVE 10
#define MESSAGES_TO_RETRIEVE 100
typedef enum {
DB_RESPONSE_ERROR,
DB_RESPONSE_EMPTY,
DB_RESPONSE_SUCCESS
} db_history_result_t;
gboolean log_database_init(ProfAccount* account);
void log_database_add_incoming(ProfMessage* message);
void log_database_add_outgoing_chat(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
void log_database_add_outgoing_muc(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
void log_database_add_outgoing_muc_pm(const char* const id, const char* const barejid, const char* const message, const char* const replace_id, prof_enc_t enc);
GSList* log_database_get_previous_chat(const gchar* const contact_barejid, const char* start_time, char* end_time, gboolean from_start, gboolean flip);
db_history_result_t log_database_get_previous_chat(const gchar* const contact_barejid, const gchar* start_time, const gchar* end_time, gboolean from_start, gboolean flip, GSList** result);
ProfMessage* log_database_get_limits_info(const gchar* const contact_barejid, gboolean is_last);
void log_database_close(void);

View File

@@ -664,7 +664,8 @@ p_gpg_decrypt(const char* const cipher)
}
gpgme_key_unref(key);
} else if (error) {
log_warning("GPGME error: %s", gpgme_strerror(error));
// Emails are used only to fetch PGP recipients for debug logging. If something went wrong here, it's not worthy of warning.
log_debug("GPGME error on gpgme_get_key(): %s", gpgme_strerror(error));
}
if (recipient->next) {

View File

@@ -900,6 +900,8 @@ _python_type_error(ProfPlugin* plugin, char* hook, char* type)
g_string_free(err_msg, TRUE);
}
// Converts a Python string or None result to a C string and decreases the Python object's reference count.
// If the result is NULL, or not a string/unicode/bytes/None, logs an error and returns NULL.
static char*
_handle_string_or_none_result(ProfPlugin* plugin, PyObject* result, char* hook)
{
@@ -910,18 +912,16 @@ _handle_string_or_none_result(ProfPlugin* plugin, PyObject* result, char* hook)
}
#ifdef PY_IS_PYTHON3
if (result != Py_None && !PyUnicode_Check(result) && !PyBytes_Check(result)) {
allow_python_threads();
_python_type_error(plugin, hook, "string, unicode or None");
return NULL;
}
#else
if (result != Py_None && !PyUnicode_Check(result) && !PyString_Check(result)) {
#endif
Py_XDECREF(result);
allow_python_threads();
_python_type_error(plugin, hook, "string, unicode or None");
return NULL;
}
#endif
char* result_str = python_str_or_unicode_to_string(result);
Py_XDECREF(result);
allow_python_threads();
return result_str;
}

View File

@@ -59,6 +59,7 @@
#include "config/tlscerts.h"
#include "config/scripts.h"
#include "command/cmd_defs.h"
#include "tools/editor.h"
#include "plugins/plugins.h"
#include "event/client_events.h"
#include "ui/ui.h"
@@ -93,6 +94,9 @@ static void _connect_default(const char* const account);
pthread_mutex_t lock;
static gboolean force_quit = FALSE;
// main.c (prof_run section)
gboolean background_mode = FALSE;
void
prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_file, gchar* theme_name, gchar** commands)
{
@@ -118,42 +122,44 @@ prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_f
log_stderr_handler();
session_check_autoaway();
line = commands ? *commands : inp_readline();
if (commands && line && memcmp(line, "/sleep", 6) == 0) {
if (!g_timer_is_active(waittimer)) {
gchar* err_msg;
if (strtoi_range(line + 7, &waittime, 0, 300, &err_msg)) {
g_timer_start(waittimer);
/* Increase the minimal runtime by the waiting time
* so we can be sure there's runtime left after executing
* the last command.
*/
min_runtime += waittime;
} else {
log_error(err_msg);
g_free(err_msg);
commands = NULL;
if (!background_mode) {
line = commands ? *commands : inp_readline();
if (commands && line && memcmp(line, "/sleep", 6) == 0) {
if (!g_timer_is_active(waittimer)) {
gchar* err_msg;
if (strtoi_range(line + 7, &waittime, 0, 300, &err_msg)) {
g_timer_start(waittimer);
/* Increase the minimal runtime by the waiting time
* so we can be sure there's runtime left after executing
* the last command.
*/
min_runtime += waittime;
} else {
log_error(err_msg);
g_free(err_msg);
commands = NULL;
}
} else if (g_timer_elapsed(waittimer, NULL) >= waittime) {
g_timer_stop(waittimer);
commands++;
if (!(*commands))
commands = NULL;
}
cont = TRUE;
} else if (line) {
ProfWin* window = wins_get_current();
cont = cmd_process_input(window, line);
if (commands) {
commands++;
if (!(*commands))
commands = NULL;
} else {
free(line);
line = NULL;
}
} else if (g_timer_elapsed(waittimer, NULL) >= waittime) {
g_timer_stop(waittimer);
commands++;
if (!(*commands))
commands = NULL;
}
cont = TRUE;
} else if (line) {
ProfWin* window = wins_get_current();
cont = cmd_process_input(window, line);
if (commands) {
commands++;
if (!(*commands))
commands = NULL;
} else {
free(line);
line = NULL;
cont = TRUE;
}
} else {
cont = TRUE;
}
#ifdef HAVE_LIBOTR
@@ -163,7 +169,12 @@ prof_run(gchar* log_level, gchar* account_name, gchar* config_file, gchar* log_f
notify_remind();
session_process_events();
iq_autoping_check();
ui_update();
editor_process(wins_get_current());
if (!background_mode) {
ui_update();
}
#ifdef HAVE_GTK
tray_update();
#endif

View File

@@ -140,6 +140,9 @@ aesgcm_file_get(void* userdata)
"Downloading '%s' failed: Failed to decrypt "
"file (%s).",
https_url, gcry_strerror(crypt_res));
} else {
http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id,
"Decrypted file saved to '%s'", aesgcm_dl->filename);
}
if (aesgcm_dl->cmd_template != NULL) {

View File

@@ -38,15 +38,200 @@
#include <fcntl.h>
#include <glib.h>
#include <errno.h>
#include <stdio.h> // necessary for readline
#include <readline/readline.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pthread.h>
#include "config/files.h"
#include "config/preferences.h"
#include "editor.h"
#include "log.h"
#include "common.h"
#include "ncurses.h"
#include "ui/ui.h"
#include "xmpp/xmpp.h"
EditorTask editor_task = { 0, FALSE, NULL, NULL, PTHREAD_MUTEX_INITIALIZER, 0, NULL, NULL };
void*
editor_thread(void* arg)
{
EditorTask* task = (EditorTask*)arg;
auto_gcharv gchar** argv = g_strsplit(task->editor_cmd, " ", 0);
pid_t pid = fork();
if (pid == 0) {
execvp(argv[0], argv);
log_error("[Editor] Failed to exec %s", argv[0]);
_exit(EXIT_FAILURE);
}
pthread_mutex_lock(&task->mutex);
if (pid > 0) {
task->pid = pid;
int status;
// Unlock mutex to avoid deadlock on the main loop
pthread_mutex_unlock(&task->mutex);
waitpid(pid, &status, 0);
pthread_mutex_lock(&task->mutex);
gsize length;
if (!g_file_get_contents(task->filename, &task->result, &length, &task->error)) {
log_error("[Editor] could not read from %s: %s", task->filename,
task->error ? task->error->message : "No GLib error given");
task->result = g_strdup(""); // Ensure valid result
} else {
g_strchomp(task->result);
if (remove(task->filename) != 0) {
log_error("[Editor] error during file deletion of %s", task->filename);
} else {
log_debug("[Editor] deleted file: %s", task->filename);
}
}
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
task->error = g_error_new(G_FILE_ERROR, G_FILE_ERROR_FAILED,
"Editor process failed with status %d", WEXITSTATUS(status));
}
} else {
task->error = g_error_new(G_FILE_ERROR, G_FILE_ERROR_FAILED, "Fork failed: %s", strerror(errno));
task->result = g_strdup(""); // Ensure valid result
}
task->active = FALSE;
pthread_mutex_unlock(&task->mutex);
return NULL;
}
// Returns true if an error occurred
gboolean
get_message_from_editor_async(gchar* message)
{
// Check if editor is already running
pthread_mutex_lock(&editor_task.mutex);
if (editor_task.active) {
pthread_mutex_unlock(&editor_task.mutex);
log_error("[Editor] Editor already running");
return TRUE;
}
pthread_mutex_unlock(&editor_task.mutex);
// Create temporary file
auto_gchar gchar* filename = NULL;
GError* glib_error = NULL;
const char* jid = connection_get_barejid();
if (jid) {
filename = files_file_in_account_data_path(DIR_EDITOR, jid, "compose.md");
} else {
log_debug("[Editor] could not get JID");
auto_gchar gchar* data_dir = files_get_data_path(DIR_EDITOR);
if (!create_dir(data_dir)) {
log_error("[Editor] could not create directory %s", data_dir);
return TRUE;
}
filename = g_strdup_printf("%s/compose.md", data_dir);
}
if (!filename) {
log_error("[Editor] something went wrong while creating compose file");
return TRUE;
}
// Write message to file
gsize messagelen = message ? strlen(message) : 0;
if (!g_file_set_contents(filename, message ? message : "", messagelen, &glib_error)) {
log_error("[Editor] could not write to %s: %s", filename,
glib_error ? glib_error->message : "No GLib error given");
if (glib_error) {
g_error_free(glib_error);
}
return TRUE;
}
// Prepare editor command
auto_gchar gchar* editor = prefs_get_string(PREF_COMPOSE_EDITOR);
auto_gchar gchar* editor_with_filename = g_strdup_printf("%s %s", editor, filename);
// Suspend NCurses
def_prog_mode();
endwin();
// Initialize thread task
pthread_mutex_lock(&editor_task.mutex);
editor_task.active = TRUE;
editor_task.result = NULL;
editor_task.error = NULL;
g_free(editor_task.filename);
g_free(editor_task.editor_cmd);
editor_task.filename = g_strdup(filename);
editor_task.editor_cmd = g_strdup(editor_with_filename);
pthread_mutex_unlock(&editor_task.mutex);
// Set background mode
background_mode = TRUE;
log_debug("[Editor] Entering background mode");
// Start editor thread
if (pthread_create(&editor_task.thread, NULL, editor_thread, &editor_task) != 0) {
pthread_mutex_lock(&editor_task.mutex);
editor_task.active = FALSE;
editor_task.error = g_error_new(G_FILE_ERROR, G_FILE_ERROR_FAILED,
"Failed to create editor thread: %s", strerror(errno));
g_free(editor_task.filename);
g_free(editor_task.editor_cmd);
editor_task.filename = NULL;
editor_task.editor_cmd = NULL;
pthread_mutex_unlock(&editor_task.mutex);
background_mode = FALSE;
log_debug("[Editor] Exiting background mode");
reset_prog_mode();
doupdate();
log_error("[Editor] Failed to create editor thread");
return TRUE;
}
return FALSE;
}
void
editor_process(ProfWin* window)
{
pthread_mutex_lock(&editor_task.mutex);
if (editor_task.active) {
pthread_mutex_unlock(&editor_task.mutex);
return;
}
if (!editor_task.result) {
pthread_mutex_unlock(&editor_task.mutex);
return;
}
g_strchomp(editor_task.result);
rl_replace_line(editor_task.result, 0);
rl_point = rl_end;
rl_forced_update_display();
reset_prog_mode();
doupdate();
ui_resize();
g_free(editor_task.result);
g_free(editor_task.filename);
g_free(editor_task.editor_cmd);
editor_task.result = NULL;
editor_task.filename = NULL;
editor_task.editor_cmd = NULL;
if (editor_task.error) {
log_error("[Editor] %s", editor_task.error->message);
g_error_free(editor_task.error);
editor_task.error = NULL;
}
pthread_mutex_unlock(&editor_task.mutex);
background_mode = FALSE;
log_debug("[Editor] Exiting background mode");
}
// Deprecated synchronous editor call. Returns a message as returned_message.
// Please avoid using it, since it blocks execution of the message handling and other important functionality until the editor is closed.
// Returns true if an error occurred
gboolean
get_message_from_editor(gchar* message, gchar** returned_message)

View File

@@ -37,8 +37,24 @@
#ifndef TOOLS_EDITOR_H
#define TOOLS_EDITOR_H
#include "ui/win_types.h"
#include <glib.h>
typedef struct
{
pid_t pid; // Editor process PID
gboolean active; // Thread running
gchar* result; // Edited message
GError* error; // Error if any
pthread_mutex_t mutex; // Protect access
pthread_t thread; // Editor thread
gchar* filename; // Temporary file path
gchar* editor_cmd; // Editor command with filename
} EditorTask;
extern EditorTask editor_task;
gboolean get_message_from_editor(gchar* message, gchar** returned_message);
gboolean get_message_from_editor_async(gchar* message);
void editor_process(ProfWin* window);
#endif

View File

@@ -109,6 +109,7 @@ _buffer_add(ProfBuff buffer, const char* show_char, int pad_indent, GDateTime* t
buffer->lines += e->_lines;
while (g_slist_length(buffer->entries) >= MAX_BUFFER_SIZE) {
// Delete message from the opposite size to free buffer
GSList* buffer_entry_to_delete = append ? buffer->entries : g_slist_last(buffer->entries);
ProfBuffEntry* entry_to_delete = (ProfBuffEntry*)buffer_entry_to_delete->data;
buffer->lines -= entry_to_delete->_lines;
@@ -117,7 +118,13 @@ _buffer_add(ProfBuff buffer, const char* show_char, int pad_indent, GDateTime* t
}
if (from_jid && y_end_pos == y_start_pos) {
log_warning("Ncurses Overflow! From: %s, pos: %d, ID: %s, message: %s", from_jid, y_end_pos, id, message);
log_warning("Ncurses Overflow! From: %s, position: %d, ID: %s, append: %s, used message buffer size: %d, message buffer size: %d, rendered lines buffer size: %d",
from_jid, y_start_pos, id, append ? "TRUE" : "FALSE", g_slist_length(buffer->entries), MAX_BUFFER_SIZE, PAD_SIZE);
// At this point we have a message that caused overflow of the render buffer.
// Ideally, we want to clean other messages to rerender everything properly. To do so,
// first we need to determine the size of the message that we are trying to display,
// then we need to print the message.
// However, _buffer_add is too late in the code to do this, therefore it has to be done in the win_print_old_history.
}
buffer->entries = append ? g_slist_append(buffer->entries, e) : g_slist_prepend(buffer->entries, e);

View File

@@ -40,6 +40,7 @@
#include <stdlib.h>
#include <assert.h>
#include "glib.h"
#include "xmpp/chat_session.h"
#include "window_list.h"
#include "xmpp/roster_list.h"
@@ -561,7 +562,8 @@ static void
_chatwin_history(ProfChatWin* chatwin, const char* const contact_barejid)
{
if (!chatwin->history_shown) {
GSList* history = log_database_get_previous_chat(contact_barejid, NULL, NULL, FALSE, FALSE);
GSList* history = NULL;
log_database_get_previous_chat(contact_barejid, NULL, NULL, FALSE, FALSE, &history);
GSList* curr = history;
while (curr) {
@@ -583,15 +585,17 @@ _chatwin_history(ProfChatWin* chatwin, const char* const contact_barejid)
// Print history starting from start_time to end_time if end_time is null the
// first entry's timestamp in the buffer is used. Flip true to prepend to buffer.
// Timestamps should be in iso8601
gboolean
chatwin_db_history(ProfChatWin* chatwin, const char* start_time, char* end_time, gboolean flip)
db_history_result_t
chatwin_db_history(ProfChatWin* chatwin, const gchar* start_time, const gchar* end_time, gboolean flip)
{
if (!end_time) {
end_time = buffer_size(((ProfWin*)chatwin)->layout->buffer) == 0 ? NULL : g_date_time_format_iso8601(buffer_get_entry(((ProfWin*)chatwin)->layout->buffer, 0)->time);
auto_gchar gchar* _end_time = NULL;
if (!end_time && buffer_size(((ProfWin*)chatwin)->layout->buffer) > 0) {
_end_time = g_date_time_format_iso8601(buffer_get_entry(((ProfWin*)chatwin)->layout->buffer, 0)->time);
end_time = _end_time;
}
GSList* history = log_database_get_previous_chat(chatwin->barejid, start_time, end_time, !flip, flip);
gboolean has_items = g_slist_length(history) != 0;
GSList* history = NULL;
db_history_result_t result = log_database_get_previous_chat(chatwin->barejid, start_time, end_time, !flip, flip, &history);
GSList* curr = history;
while (curr) {
@@ -610,9 +614,12 @@ chatwin_db_history(ProfChatWin* chatwin, const char* start_time, char* end_time,
}
g_slist_free_full(history, (GDestroyNotify)message_free);
// TODO: Potential optimization
// if (flip)
// since adding messages to history without overflowing buffer does not require a win_redraw
win_redraw((ProfWin*)chatwin);
return has_items;
return result;
}
static void

View File

@@ -387,16 +387,9 @@ cons_about(void)
_cons_splash_logo();
} else {
auto_gchar gchar* prof_version = prof_get_version();
win_println(console, THEME_DEFAULT, "-", "Welcome to Profanity, version %s", prof_version);
win_println(console, THEME_DEFAULT, "-", "Welcome to CProof, version %s", prof_version);
}
win_println(console, THEME_DEFAULT, "-", "Copyright (C) 2012 - 2019 James Booth <boothj5web@gmail.com>.");
win_println(console, THEME_DEFAULT, "-", "Copyright (C) 2019 - 2025 Michael Vetter <jubalh@iodoru.org>.");
win_println(console, THEME_DEFAULT, "-", "License GPLv3+: GNU GPL version 3 or later <https://www.gnu.org/licenses/gpl.html>");
win_println(console, THEME_DEFAULT, "-", "");
win_println(console, THEME_DEFAULT, "-", "This is free software; you are free to change and redistribute it.");
win_println(console, THEME_DEFAULT, "-", "There is NO WARRANTY, to the extent permitted by law.");
win_println(console, THEME_DEFAULT, "-", "");
win_println(console, THEME_DEFAULT, "-", "Type '/help' to show complete help.");
win_println(console, THEME_DEFAULT, "-", "");
@@ -484,7 +477,7 @@ cons_show_wins(gboolean unread)
}
curr = g_slist_next(curr);
}
g_slist_free_full(window_strings, free);
g_slist_free_full(window_strings, g_free);
cons_alert(NULL);
}
@@ -505,7 +498,7 @@ cons_show_wins_attention()
}
curr = g_slist_next(curr);
}
g_slist_free_full(window_strings, free);
g_slist_free_full(window_strings, g_free);
cons_alert(NULL);
}
@@ -2704,13 +2697,12 @@ _cons_splash_logo(void)
ProfWin* console = wins_get_console();
win_println(console, THEME_DEFAULT, "-", "Welcome to");
win_println(console, THEME_SPLASH, "-", " ___ _ ");
win_println(console, THEME_SPLASH, "-", " / __) (_)_ ");
win_println(console, THEME_SPLASH, "-", " ____ ___ ___ | |__ ____ ____ _| |_ _ _ ");
win_println(console, THEME_SPLASH, "-", "| _ \\ / __) _ \\| __) _ | _ \\| | _) | | |");
win_println(console, THEME_SPLASH, "-", "| | ) | | | (_) | | | ( | | | | | | |_| |_| |");
win_println(console, THEME_SPLASH, "-", "| ||_/|_| \\___/|_| \\_||_|_| |_|_|\\___)__ |");
win_println(console, THEME_SPLASH, "-", "|_| (____/ ");
win_println(console, THEME_SPLASH, "-", " _____ _____ __ ");
win_println(console, THEME_SPLASH, "-", " / ____| __ \\ / _|");
win_println(console, THEME_SPLASH, "-", " | | | |__) | __ ___ ___ | |_ ");
win_println(console, THEME_SPLASH, "-", " | | | ___/ '__/ _ \\ / _ \\| _|");
win_println(console, THEME_SPLASH, "-", " | |____| | | | | (_) | (_) | | ");
win_println(console, THEME_SPLASH, "-", " \\_____|_| |_| \\___/ \\___/|_| ");
win_println(console, THEME_SPLASH, "-", "");
auto_gchar gchar* prof_version = prof_get_version();

View File

@@ -974,14 +974,7 @@ _inp_rl_send_to_editor(int count, int key)
auto_gchar gchar* message = NULL;
if (get_message_from_editor(rl_line_buffer, &message)) {
return 0;
}
rl_replace_line(message, 0);
ui_resize();
rl_point = rl_end;
rl_forced_update_display();
get_message_from_editor_async(rl_line_buffer);
return 0;
}

View File

@@ -43,6 +43,7 @@
#include "config/account.h"
#include "config/preferences.h"
#include "command/cmd_funcs.h"
#include "database.h"
#include "ui/win_types.h"
#include "xmpp/message.h"
#include "xmpp/muc.h"
@@ -145,7 +146,7 @@ void chatwin_set_incoming_char(ProfChatWin* chatwin, const char* const ch);
void chatwin_unset_incoming_char(ProfChatWin* chatwin);
void chatwin_set_outgoing_char(ProfChatWin* chatwin, const char* const ch);
void chatwin_unset_outgoing_char(ProfChatWin* chatwin);
gboolean chatwin_db_history(ProfChatWin* chatwin, const char* start_time, char* end_time, gboolean flip);
db_history_result_t chatwin_db_history(ProfChatWin* chatwin, const gchar* start_time, const gchar* end_time, gboolean flip);
// MUC window
ProfMucWin* mucwin_new(const char* const barejid);

View File

@@ -35,6 +35,7 @@
*/
#include "config.h"
#include "database.h"
#include "ui/window_list.h"
#include <stdlib.h>
@@ -62,9 +63,8 @@
#include "xmpp/xmpp.h"
#include "xmpp/roster_list.h"
static const int PAD_SIZE = 100;
static const char* LOADING_MESSAGE = "Loading older messages…";
static const char* CONS_WIN_TITLE = "Profanity. Type /help for help information.";
static const char* CONS_WIN_TITLE = "CProof. Type /help for help information.";
static const char* XML_WIN_TITLE = "XML Console";
#define CEILING(X) (X - (int)(X) > 0 ? (int)(X + 1) : (int)(X))
@@ -629,54 +629,68 @@ win_free(ProfWin* window)
void
win_page_up(ProfWin* window, int scroll_size)
{
// Page offset from the start of the NCurses PAD (invisible window)
int* page_start = &(window->layout->y_pos);
int page_start_initial = *page_start;
int total_rows = getcury(window->layout->win);
// Size of the visible chat page
int page_space = getmaxy(stdscr) - 4;
if (scroll_size == 0)
scroll_size = page_space;
win_scroll_state_t* scroll_state = &window->scroll_state;
*scroll_state = (*scroll_state == WIN_SCROLL_REACHED_BOTTOM) ? WIN_SCROLL_INNER : *scroll_state;
*page_start -= scroll_size;
gboolean has_scroll_past_buffer = *page_start < 0;
if (*page_start == -scroll_size && window->type == WIN_CHAT) {
if (has_scroll_past_buffer && window->type == WIN_CHAT) {
ProfChatWin* chatwin = (ProfChatWin*)window;
ProfBuffEntry* first_entry = buffer_size(window->layout->buffer) != 0 ? buffer_get_entry(window->layout->buffer, 0) : NULL;
// Don't do anything if still fetching mam messages
if (first_entry && !(first_entry->theme_item == THEME_ROOMINFO && g_strcmp0(first_entry->message, LOADING_MESSAGE) == 0)) {
gboolean is_still_fetching_mam = first_entry && (first_entry->theme_item == THEME_ROOMINFO && g_strcmp0(first_entry->message, LOADING_MESSAGE) == 0);
if (!is_still_fetching_mam) {
// WIN_SCROLL_REACHED_TOP means that we reached top of DB and there is no need to refetch data from it
gboolean is_db_offset_jump_needed = false;
if (*scroll_state != WIN_SCROLL_REACHED_TOP) {
*scroll_state = !chatwin_db_history(chatwin, NULL, NULL, TRUE) ? WIN_SCROLL_REACHED_TOP : WIN_SCROLL_INNER;
db_history_result_t db_response = chatwin_db_history(chatwin, NULL, NULL, TRUE);
is_db_offset_jump_needed = db_response == DB_RESPONSE_SUCCESS;
*scroll_state = db_response == DB_RESPONSE_EMPTY ? WIN_SCROLL_REACHED_TOP : WIN_SCROLL_INNER;
log_debug("Scroll state after DB history fetch: %s", *scroll_state ? "WIN_SCROLL_REACHED_TOP" : "WIN_SCROLL_INNER");
}
if (*scroll_state == WIN_SCROLL_REACHED_TOP && prefs_get_boolean(PREF_MAM)) {
win_print_loading_history(window);
iq_mam_request_older(chatwin);
if (*scroll_state == WIN_SCROLL_REACHED_TOP) {
*page_start = 0;
if (prefs_get_boolean(PREF_MAM)) {
win_print_loading_history(window);
iq_mam_request_older(chatwin);
}
}
int buff_size = buffer_size(window->layout->buffer);
int offset_entry_id = buff_size > 10 ? 10 : buff_size - 1;
int offset = buffer_get_entry(window->layout->buffer, offset_entry_id)->y_end_pos;
*page_start = offset - page_space;
// Recalculate scroll offset after loading older messages from the DB.
// *page_start may become negative due to paging up beyond the current buffer.
// To maintain smooth scroll positioning, we adjust it relative to the visual offset
// (y_start_pos) of the first buffer entry *before* DB fetch.
// This ensures a seamless transition as older messages are prepended, regardless
// of how many visual lines they occupy (since messages can vary in height).
// For example, if *page_start == -5 and first_entry->y_start_pos == 40,
// we shift the scroll to 35, preserving the visual context for the user.
if (is_db_offset_jump_needed) {
*page_start = first_entry ? first_entry->y_start_pos + *page_start : 0;
}
}
}
// went past beginning, show first page
if (*page_start < 0) {
log_debug("win_page_up(): *page_start adjusted to 0 due to negative value (expected if insufficient history messages). Previous value: %d", *page_start);
*page_start = 0;
}
window->layout->paged = 1;
// update only if position has changed
// Update window location only if position has changed
if (page_start_initial != *page_start) {
win_update_virtual(window);
}
// switch off page if last line and space line visible
if ((total_rows) - *page_start == page_space) {
window->layout->paged = 0;
}
// Update scrolling state
int total_rows = getcury(window->layout->win);
window->layout->paged = (total_rows) - *page_start > page_space;
}
void
@@ -696,20 +710,22 @@ win_page_down(ProfWin* window, int scroll_size)
// Scrolled down after reaching the bottom of the page
if ((*page_start > total_rows - page_space || (*page_start == page_space && *page_start >= total_rows)) && window->type == WIN_CHAT) {
int bf_size = buffer_size(window->layout->buffer);
if (bf_size > 0) {
if (bf_size > 0 && *scroll_state != WIN_SCROLL_REACHED_BOTTOM) {
// How many lines are left until end of the screen
int current_offset = total_rows - *page_start;
GDateTime* now = g_date_time_new_now_local();
ProfBuffEntry* last_entry = buffer_get_entry(window->layout->buffer, bf_size - 1);
auto_gchar gchar* start = g_date_time_format_iso8601(last_entry->time);
GDateTime* now = g_date_time_new_now_local();
gchar* end_date = g_date_time_format_iso8601(now);
g_date_time_unref(now);
if (*scroll_state != WIN_SCROLL_REACHED_BOTTOM && !chatwin_db_history((ProfChatWin*)window, start, end_date, FALSE)) {
auto_gchar gchar* end_date = g_date_time_format_iso8601(now);
db_history_result_t db_response = chatwin_db_history((ProfChatWin*)window, start, end_date, FALSE);
if (db_response == DB_RESPONSE_EMPTY)
*scroll_state = WIN_SCROLL_REACHED_BOTTOM;
} else if (*scroll_state == WIN_SCROLL_REACHED_BOTTOM) {
g_free(end_date);
}
int offset = last_entry->y_end_pos - 1;
*page_start = offset - page_space + scroll_size;
// similar to page_up (see explanation there)
// Get offset of the message that was the latest prior to loading messages from the DB
int original_pos = last_entry->y_end_pos;
*page_start = original_pos - current_offset;
g_date_time_unref(now);
}
}

View File

@@ -56,6 +56,9 @@
#include "xmpp/contact.h"
#include "xmpp/muc.h"
// Max lines per window rendered by NCurses
static const int PAD_SIZE = 10000;
void win_move_to_end(ProfWin* window);
void win_show_status_string(ProfWin* window, const char* const from,
const char* const show, const char* const status,

View File

@@ -258,22 +258,20 @@ GList*
wins_get_private_chats(const char* const roomjid)
{
GList* result = NULL;
GString* prefix = g_string_new(roomjid);
g_string_append(prefix, "/");
auto_gchar gchar* prefix = g_strdup_printf("%s/", roomjid);
GList* curr = values;
while (curr) {
ProfWin* window = curr->data;
if (window->type == WIN_PRIVATE) {
ProfPrivateWin* privatewin = (ProfPrivateWin*)window;
if (roomjid == NULL || g_str_has_prefix(privatewin->fulljid, prefix->str)) {
if (roomjid == NULL || g_str_has_prefix(privatewin->fulljid, prefix)) {
result = g_list_append(result, privatewin);
}
}
curr = g_list_next(curr);
}
g_string_free(prefix, TRUE);
return result;
}
@@ -1067,19 +1065,13 @@ wins_create_summary(gboolean unread)
while (curr) {
ProfWin* window = g_hash_table_lookup(windows, curr->data);
if (!unread || (unread && win_unread(window) > 0)) {
GString* line = g_string_new("");
int ui_index = GPOINTER_TO_INT(curr->data);
auto_gchar gchar* winstring = win_to_string(window);
if (!winstring) {
g_string_free(line, TRUE);
continue;
}
g_string_append_printf(line, "%d: %s", ui_index, winstring);
result = g_slist_append(result, strdup(line->str));
g_string_free(line, TRUE);
gchar* line = g_strdup_printf("%d: %s", ui_index, winstring);
result = g_slist_append(result, line);
}
curr = g_list_next(curr);
@@ -1108,19 +1100,13 @@ wins_create_summary_attention()
has_attention = mucwin->has_attention;
}
if (has_attention) {
GString* line = g_string_new("");
int ui_index = GPOINTER_TO_INT(curr->data);
auto_gchar gchar* winstring = win_to_string(window);
if (!winstring) {
g_string_free(line, TRUE);
continue;
}
g_string_append_printf(line, "%d: %s", ui_index, winstring);
result = g_slist_append(result, strdup(line->str));
g_string_free(line, TRUE);
gchar* line = g_strdup_printf("%d: %s", ui_index, winstring);
result = g_slist_append(result, line);
}
curr = g_list_next(curr);
}

View File

@@ -119,6 +119,14 @@ caps_init(void)
g_hash_table_add(prof_features, strdup(STANZA_NS_LAST_MESSAGE_CORRECTION));
}
if (prefs_get_boolean(PREF_MOOD)) {
g_hash_table_add(prof_features, strdup(STANZA_NS_MOOD_NOTIFY));
}
#ifdef HAVE_OMEMO
g_hash_table_add(prof_features, strdup(XMPP_FEATURE_OMEMO_DEVICELIST_NOTIFY));
#endif
my_sha1 = NULL;
}

View File

@@ -2752,6 +2752,22 @@ iq_mam_request(ProfChatWin* win, GDateTime* enddate)
return;
}
// Trim the last 3 characters from the datetime string to remove unwanted suffix (timezone offset)
static void
_truncate_datetime_suffix(gchar* str)
{
if (!str) {
log_warning("_truncate_datetime_suffix(): unexpected null datetime string");
return;
}
size_t len = strlen(str);
if (len >= 3) {
str[len - 3] = '\0';
} else {
log_warning("_truncate_datetime_suffix(): unexpected short datetime string: %s", str);
}
}
static int
_mam_rsm_id_handler(xmpp_stanza_t* const stanza, void* const userdata)
{
@@ -2773,17 +2789,17 @@ _mam_rsm_id_handler(xmpp_stanza_t* const stanza, void* const userdata)
buffer_remove_entry(window->layout->buffer, 0);
auto_char char* start_str = NULL;
auto_gchar gchar* start_str = NULL;
if (data->start_datestr) {
start_str = strdup(data->start_datestr);
start_str = g_strdup(data->start_datestr);
// Convert to iso8601
start_str[strlen(start_str) - 3] = '\0';
_truncate_datetime_suffix(start_str);
}
char* end_str = NULL;
auto_gchar gchar* end_str = NULL;
if (data->end_datestr) {
end_str = strdup(data->end_datestr);
end_str = g_strdup(data->end_datestr);
// Convert to iso8601
end_str[strlen(end_str) - 3] = '\0';
_truncate_datetime_suffix(end_str);
}
if (is_complete || !data->fetch_next) {

View File

@@ -190,7 +190,7 @@ get_nick_from_full_jid(const char* const full_room_jid)
/*
* get the fulljid, fall back to the barejid
*/
char*
const char*
jid_fulljid_or_barejid(Jid* jid)
{
if (jid->fulljid) {

View File

@@ -63,7 +63,7 @@ gboolean jid_is_valid_room_form(Jid* jid);
char* create_fulljid(const char* const barejid, const char* const resource);
char* get_nick_from_full_jid(const char* const full_room_jid);
char* jid_fulljid_or_barejid(Jid* jid);
const char* jid_fulljid_or_barejid(Jid* jid);
gchar* jid_random_resource(void);
#endif

View File

@@ -621,7 +621,7 @@ _available_handler(xmpp_stanza_t* const stanza)
}
return;
} else {
char* jid = jid_fulljid_or_barejid(xmpp_presence->jid);
const char* jid = jid_fulljid_or_barejid(xmpp_presence->jid);
log_debug("Presence available handler fired for: %s", jid);
}
@@ -632,7 +632,7 @@ _available_handler(xmpp_stanza_t* const stanza)
XMPPCaps* caps = stanza_parse_caps(stanza);
if ((g_strcmp0(my_jid->fulljid, xmpp_presence->jid->fulljid) != 0) && caps) {
log_debug("Presence contains capabilities.");
char* jid = jid_fulljid_or_barejid(xmpp_presence->jid);
const char* jid = jid_fulljid_or_barejid(xmpp_presence->jid);
_handle_caps(jid, caps);
}
stanza_free_caps(caps);
@@ -644,7 +644,7 @@ _available_handler(xmpp_stanza_t* const stanza)
const char* account_name = session_get_account_name();
int max_sessions = accounts_get_max_sessions(account_name);
if (max_sessions > 0) {
const gchar* cur_resource = accounts_get_resource(account_name);
auto_gchar gchar* cur_resource = accounts_get_resource(account_name);
int res_count = connection_count_available_resources();
if (res_count > max_sessions && g_strcmp0(cur_resource, resource->name)) {
ProfWin* console = wins_get_console();

View File

@@ -275,11 +275,8 @@ roster_remove(const char* const name, const char* const barejid)
if (contact) {
GList* resources = p_contact_get_available_resources(contact);
while (resources) {
GString* fulljid = g_string_new(barejid);
g_string_append(fulljid, "/");
g_string_append(fulljid, resources->data);
autocomplete_remove(roster->fulljid_ac, fulljid->str);
g_string_free(fulljid, TRUE);
auto_gchar gchar* fulljid = g_strdup_printf("%s/%s", barejid, (char*)resources->data);
autocomplete_remove(roster->fulljid_ac, fulljid);
resources = g_list_next(resources);
}
g_list_free(resources);

View File

@@ -281,7 +281,7 @@ shows_all_messages_in_console_when_window_not_focussed(void **state)
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
prof_input("/win 1");
assert_true(prof_output_exact("Profanity. Type /help for help information."));
assert_true(prof_output_exact("CProof. Type /help for help information."));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -322,7 +322,7 @@ shows_first_message_in_console_when_window_not_focussed(void **state)
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
prof_input("/win 1");
assert_true(prof_output_exact("Profanity. Type /help for help information."));
assert_true(prof_output_exact("CProof. Type /help for help information."));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"
@@ -368,7 +368,7 @@ shows_no_message_in_console_when_window_not_focussed(void **state)
assert_true(prof_output_exact("-> You have joined the room as stabber, role: participant, affiliation: none"));
prof_input("/win 1");
assert_true(prof_output_exact("Profanity. Type /help for help information."));
assert_true(prof_output_exact("CProof. Type /help for help information."));
stbbr_send(
"<message type='groupchat' to='stabber@localhost/profanity' from='testroom@conference.localhost/testoccupant'>"

View File

@@ -219,7 +219,7 @@ returns_fulljid_when_exists(void** state)
{
Jid* jid = jid_create("localpart@domainpart/resourcepart");
char* result = jid_fulljid_or_barejid(jid);
const char* result = jid_fulljid_or_barejid(jid);
assert_string_equal("localpart@domainpart/resourcepart", result);
@@ -231,7 +231,7 @@ returns_barejid_when_fulljid_not_exists(void** state)
{
Jid* jid = jid_create("localpart@domainpart");
char* result = jid_fulljid_or_barejid(jid);
const char* result = jid_fulljid_or_barejid(jid);
assert_string_equal("localpart@domainpart", result);