diff --git a/src/common/fe.h b/src/common/fe.h index e456ad73..3047aab0 100644 --- a/src/common/fe.h +++ b/src/common/fe.h @@ -78,6 +78,7 @@ int fe_input_add (int sok, int flags, void *func, void *data); void fe_input_remove (int tag); void fe_idle_add (void *func, void *data); void fe_set_topic (struct session *sess, char *topic, char *stripped_topic); +void fe_set_typing (struct session *sess, const char *nick, const char *state); typedef enum { FE_COLOR_NONE = 0, diff --git a/src/common/inbound.c b/src/common/inbound.c index 978c4aee..8c1ba10a 100644 --- a/src/common/inbound.c +++ b/src/common/inbound.c @@ -450,6 +450,9 @@ inbound_action (session *sess, char *chan, char *from, char *ip, char *text, fromme = TRUE; } + if (!fromme) + fe_set_typing (sess, from, "done"); + inbound_make_idtext (serv, idtext, sizeof (idtext), id); if (!fromme && !privaction) @@ -518,6 +521,9 @@ inbound_chanmsg (server *serv, session *sess, char *chan, char *from, fromme = TRUE; } + if (!fromme) + fe_set_typing (sess, from, "done"); + if (fromme) { if (prefs.hex_away_auto_unmark && serv->is_away && !tags_data->timestamp) @@ -1742,6 +1748,10 @@ inbound_toggle_caps (server *serv, const char *extensions_str, gboolean enable) serv->have_awaynotify = enable; else if (!strcmp (extension, "account-tag")) serv->have_account_tag = enable; + else if (!strcmp (extension, "message-tags")) + serv->have_message_tags = enable; + else if (!strcmp (extension, "echo-message")) + serv->have_echo_message = enable; else if (!strcmp (extension, "sasl")) { serv->have_sasl = enable; @@ -1873,6 +1883,8 @@ static const char * const supported_caps[] = { "account-tag", "extended-monitor", "standard-replies", + "message-tags", + "echo-message", /* ZNC */ "znc.in/server-time-iso", diff --git a/src/common/modes.c b/src/common/modes.c index 20f579fb..6dadc459 100644 --- a/src/common/modes.c +++ b/src/common/modes.c @@ -907,6 +907,10 @@ inbound_005 (server * serv, char *word[], const message_tags_data *tags_data) { if (g_strcmp0 (tokvalue, "ascii") == 0) serv->p_cmp = (void *)g_ascii_strcasecmp; + } else if (g_strcmp0 (tokname, "CLIENTTAGDENY") == 0) + { + g_free (serv->clienttagdeny); + serv->clienttagdeny = tokadding ? g_strdup (tokvalue) : NULL; } else if (g_strcmp0 (tokname, "CHARSET") == 0) { if (g_ascii_strcasecmp (tokvalue, "UTF-8") == 0) diff --git a/src/common/outbound.c b/src/common/outbound.c index ba8a0acb..f5e80529 100644 --- a/src/common/outbound.c +++ b/src/common/outbound.c @@ -2744,8 +2744,8 @@ cmd_me (struct session *sess, char *tbuf, char *word[], char *word_eol[]) while ((split_text = split_up_text (sess, act + offset, cmd_length, split_text))) { sess->server->p_action (sess->server, sess->channel, split_text); - /* print it to screen */ - inbound_action (sess, sess->channel, sess->server->nick, "", + if (!sess->server->have_echo_message) + inbound_action (sess, sess->channel, sess->server->nick, "", split_text, TRUE, FALSE, &no_tags); @@ -2756,8 +2756,8 @@ cmd_me (struct session *sess, char *tbuf, char *word[], char *word_eol[]) } sess->server->p_action (sess->server, sess->channel, act + offset); - /* print it to screen */ - inbound_action (sess, sess->channel, sess->server->nick, "", + if (!sess->server->have_echo_message) + inbound_action (sess, sess->channel, sess->server->nick, "", act + offset, TRUE, FALSE, &no_tags); } else { @@ -2821,6 +2821,133 @@ cmd_mop (struct session *sess, char *tbuf, char *word[], char *word_eol[]) return TRUE; } + +static gboolean +client_tag_allowed (server *serv, const char *tag) +{ + char **deny; + int i; + + if (!serv->have_message_tags) + return FALSE; + + if (!serv->clienttagdeny || !*serv->clienttagdeny) + return TRUE; + + deny = g_strsplit (serv->clienttagdeny, ",", 0); + for (i = 0; deny[i]; i++) + { + if (!strcmp (deny[i], "*") || !strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag))) + { + g_strfreev (deny); + return FALSE; + } + } + + g_strfreev (deny); + return TRUE; +} + +static char * +client_tag_escape (const char *text) +{ + GString *out; + const char *p; + + out = g_string_sized_new (strlen (text)); + for (p = text; *p; p++) + { + switch (*p) + { + case ';': + g_string_append (out, "\\:"); + break; + case ' ': + g_string_append (out, "\\s"); + break; + case '\\': + g_string_append (out, "\\\\"); + break; + case '\r': + g_string_append (out, "\\r"); + break; + case '\n': + g_string_append (out, "\\n"); + break; + default: + g_string_append_c (out, *p); + break; + } + } + + return g_string_free (out, FALSE); +} + +static int +cmd_reply (struct session *sess, char *tbuf, char *word[], char *word_eol[]) +{ + char *msgid = word[2]; + char *target = word[3]; + char *text = word_eol[4]; + char *escaped; + char *tags; + + if (!*msgid || !*target || !*text) + return FALSE; + + if (!sess->server->connected || !client_tag_allowed (sess->server, "reply")) + { + notc_msg (sess); + return TRUE; + } + + escaped = client_tag_escape (msgid); + tags = g_strdup_printf ("+reply=%s", escaped); + sess->server->p_message_tagged (sess->server, tags, target, text); + if (!sess->server->have_echo_message) + { + session *target_sess = find_dialog (sess->server, target); + message_tags_data no_tags = MESSAGE_TAGS_DATA_INIT; + + if (!target_sess) + target_sess = find_channel (sess->server, target); + if (target_sess) + inbound_chanmsg (target_sess->server, target_sess, target_sess->channel, target_sess->server->nick, text, TRUE, FALSE, &no_tags); + } + g_free (tags); + g_free (escaped); + + return TRUE; +} + +static int +cmd_typing (struct session *sess, char *tbuf, char *word[], char *word_eol[]) +{ + char *state = word[2]; + char *target = word[3]; + char tags[32]; + + if (!*state) + state = "active"; + + if (!*target) + target = sess->channel; + + if (!*target || (strcmp (state, "active") && strcmp (state, "paused") && strcmp (state, "done"))) + return FALSE; + + if (!sess->server->connected || !client_tag_allowed (sess->server, "typing")) + { + notc_msg (sess); + return TRUE; + } + + g_snprintf (tags, sizeof (tags), "+typing=%s", state); + sess->server->p_tagmsg (sess->server, tags, target); + + return TRUE; +} + static int cmd_msg (struct session *sess, char *tbuf, char *word[], char *word_eol[]) { @@ -2875,7 +3002,7 @@ cmd_msg (struct session *sess, char *tbuf, char *word[], char *word_eol[]) newsess = find_dialog (sess->server, nick); if (!newsess) newsess = find_channel (sess->server, nick); - if (newsess) + if (newsess && !sess->server->have_echo_message) { message_tags_data no_tags = MESSAGE_TAGS_DATA_INIT; @@ -4138,10 +4265,12 @@ const struct commands xc_cmds[] = { N_("RECONNECT [] [] [], Can be called just as /RECONNECT to reconnect to the current server or with /RECONNECT ALL to reconnect to all the open servers")}, #endif {"RECV", cmd_recv, 1, 0, 1, N_("RECV , send raw data to ZoiteChat, as if it was received from the IRC server")}, + {"REPLY", cmd_reply, 0, 0, 1, N_("REPLY , sends a reply-tagged message")}, {"RELOAD", cmd_reload, 0, 0, 1, N_("RELOAD , reloads a plugin or script")}, {"SAY", cmd_say, 0, 0, 1, N_("SAY , sends the text to the object in the current window")}, {"SEND", cmd_send, 0, 0, 1, N_("SEND []")}, + {"TYPING", cmd_typing, 0, 0, 1, N_("TYPING [active|paused|done] [target], sends a typing notification")}, #ifdef USE_OPENSSL {"SERVCHAN", cmd_servchan, 0, 0, 1, N_("SERVCHAN [-noproxy] [-insecure|-ssl|-ssl-noverify] , connects and joins a channel using ssl unless otherwise specified")}, @@ -4674,8 +4803,9 @@ handle_say (session *sess, char *text, int check_spch) while ((split_text = split_up_text (sess, text + offset, cmd_length, split_text))) { - inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick, - split_text, TRUE, FALSE, &no_tags); + if (!sess->server->have_echo_message) + inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick, + split_text, TRUE, FALSE, &no_tags); sess->server->p_message (sess->server, sess->channel, split_text); if (*split_text) @@ -4684,7 +4814,8 @@ handle_say (session *sess, char *text, int check_spch) g_free (split_text); } - inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick, + if (!sess->server->have_echo_message) + inbound_chanmsg (sess->server, sess, sess->channel, sess->server->nick, text + offset, TRUE, FALSE, &no_tags); sess->server->p_message (sess->server, sess->channel, text + offset); } else diff --git a/src/common/proto-irc.c b/src/common/proto-irc.c index 12fcb579..371dbc70 100644 --- a/src/common/proto-irc.c +++ b/src/common/proto-irc.c @@ -362,6 +362,18 @@ irc_message (server *serv, char *channel, char *text) tcp_sendf (serv, "PRIVMSG %s :%s\r\n", channel, text); } +static void +irc_message_tagged (server *serv, char *tags, char *channel, char *text) +{ + tcp_sendf (serv, "@%s PRIVMSG %s :%s\r\n", tags, channel, text); +} + +static void +irc_tagmsg (server *serv, char *tags, char *target) +{ + tcp_sendf (serv, "@%s TAGMSG %s\r\n", tags, target); +} + static void irc_action (server *serv, char *channel, char *act) { @@ -425,7 +437,7 @@ static int irc_raw (server *serv, char *raw) { int len; - char tbuf[4096]; + char tbuf[8704]; if (*raw) { len = strlen (raw); @@ -1335,6 +1347,29 @@ process_named_msg (session *sess, char *type, char *word[], char *word_eol[], tags_data); return; + case WORDL('T','A','G','M'): + if (tags_data->typing) + { + session *typing_sess = NULL; + char *to = word[3]; + + if (is_channel (serv, to)) + typing_sess = find_channel (serv, to); + else if (!serv->p_cmp (to, serv->nick)) + typing_sess = find_dialog (serv, nick); + if (!typing_sess) + typing_sess = sess; + if (!serv->p_cmp (nick, serv->nick)) + return; + if (!strcmp (tags_data->typing, "done")) + fe_set_typing (typing_sess, nick, "done"); + else if (!strcmp (tags_data->typing, "paused")) + fe_set_typing (typing_sess, nick, "paused"); + else if (!strcmp (tags_data->typing, "active")) + fe_set_typing (typing_sess, nick, "active"); + } + return; + case WORDL('W','A','L','L'): text = word_eol[3]; if (*text == ':') @@ -1533,39 +1568,130 @@ handle_message_tag_time (const char *time, message_tags_data *tags_data) * * See http://ircv3.atheme.org/specification/message-tags-3.2 */ +static char * +message_tag_unescape (const char *value) +{ + GString *out; + const char *p; + + if (!*value) + return NULL; + + out = g_string_sized_new (strlen (value)); + + for (p = value; *p; p++) + { + if (*p != '\\') + { + g_string_append_c (out, *p); + continue; + } + + p++; + if (!*p) + break; + + switch (*p) + { + case ':': + g_string_append_c (out, ';'); + break; + case 's': + g_string_append_c (out, ' '); + break; + case '\\': + g_string_append_c (out, '\\'); + break; + case 'r': + g_string_append_c (out, '\r'); + break; + case 'n': + g_string_append_c (out, '\n'); + break; + default: + g_string_append_c (out, *p); + break; + } + } + + value = out->str; + if (!g_utf8_validate (value, -1, NULL)) + { + g_string_free (out, TRUE); + return NULL; + } + + return g_string_free (out, FALSE); +} + static void handle_message_tags (server *serv, const char *tags_str, message_tags_data *tags_data) { char **tags; + char *time = NULL; int i; - /* FIXME We might want to avoid the allocation overhead here since - * this might be called for every message from the server. - */ tags = g_strsplit (tags_str, ";", 0); - for (i=0; tags[i]; i++) + for (i = 0; tags[i]; i++) { char *key = tags[i]; - char *value = strchr (tags[i], '='); + char *raw_value = strchr (tags[i], '='); + char *value = NULL; - if (!value) + if (!*key) continue; - *value = '\0'; - value++; + if (raw_value) + { + *raw_value = '\0'; + raw_value++; + value = message_tag_unescape (raw_value); + } if (serv->have_account_tag && !strcmp (key, "account")) - tags_data->account = g_strdup (value); - - if (serv->have_idmsg && strcmp (key, "solanum.chat/identified")) + { + g_free (tags_data->account); + tags_data->account = value; + value = NULL; + } + else if (serv->have_idmsg && !strcmp (key, "solanum.chat/identified")) + { tags_data->identified = TRUE; + } + else if (serv->have_server_time && !strcmp (key, "time")) + { + g_free (time); + time = value; + value = NULL; + } + else if (!strcmp (key, "msgid")) + { + g_free (tags_data->msgid); + tags_data->msgid = value; + value = NULL; + } + else if (!strcmp (key, "+reply")) + { + g_free (tags_data->reply); + tags_data->reply = value; + value = NULL; + } + else if (!strcmp (key, "+typing")) + { + g_free (tags_data->typing); + tags_data->typing = value; + value = NULL; + } - if (serv->have_server_time && !strcmp (key, "time")) - handle_message_tag_time (value, tags_data); + g_free (value); } - + + if (time) + handle_message_tag_time (time, tags_data); + + g_free (time); g_strfreev (tags); } @@ -1667,6 +1793,9 @@ void message_tags_data_free (message_tags_data *tags_data) { g_clear_pointer (&tags_data->account, g_free); + g_clear_pointer (&tags_data->msgid, g_free); + g_clear_pointer (&tags_data->reply, g_free); + g_clear_pointer (&tags_data->typing, g_free); } void @@ -1696,6 +1825,8 @@ proto_fill_her_up (server *serv) serv->p_set_back = irc_set_back; serv->p_set_away = irc_set_away; serv->p_message = irc_message; + serv->p_message_tagged = irc_message_tagged; + serv->p_tagmsg = irc_tagmsg; serv->p_action = irc_action; serv->p_notice = irc_notice; serv->p_topic = irc_topic; diff --git a/src/common/proto-irc.h b/src/common/proto-irc.h index 8f18576c..27eb3f28 100644 --- a/src/common/proto-irc.h +++ b/src/common/proto-irc.h @@ -28,6 +28,9 @@ NULL, /* account name */ \ FALSE, /* identified to nick */ \ (time_t)0, /* timestamp */ \ + NULL, \ + NULL, \ + NULL, \ } #define STRIP_COLON(word, word_eol, idx) (word)[(idx)][0] == ':' ? (word_eol)[(idx)]+1 : (word)[(idx)] @@ -41,6 +44,9 @@ typedef struct char *account; gboolean identified; time_t timestamp; + char *msgid; + char *reply; + char *typing; } message_tags_data; void message_tags_data_free (message_tags_data *tags_data); diff --git a/src/common/server.c b/src/common/server.c index 06732922..9c99ceff 100644 --- a/src/common/server.c +++ b/src/common/server.c @@ -1821,6 +1821,7 @@ void server_set_defaults (server *serv) { g_free (serv->chantypes); + g_clear_pointer (&serv->clienttagdeny, g_free); g_free (serv->chanmodes); g_free (serv->nick_prefixes); g_free (serv->nick_modes); @@ -1855,6 +1856,8 @@ server_set_defaults (server *serv) serv->have_extjoin = FALSE; serv->have_account_tag = FALSE; serv->have_server_time = FALSE; + serv->have_message_tags = FALSE; + serv->have_echo_message = FALSE; serv->have_sasl = FALSE; serv->have_except = FALSE; serv->have_invite = FALSE; @@ -1985,6 +1988,7 @@ server_free (server *serv) g_free (serv->nick_prefixes); g_free (serv->chanmodes); g_free (serv->chantypes); + g_free (serv->clienttagdeny); g_free (serv->bad_nick_prefixes); g_free (serv->last_away_reason); g_free (serv->encoding); diff --git a/src/common/userlist.h b/src/common/userlist.h index 244a6e67..ee5a3edf 100644 --- a/src/common/userlist.h +++ b/src/common/userlist.h @@ -31,6 +31,7 @@ struct User char *servername; char *account; time_t lasttalk; + time_t typing_time; unsigned int access; /* axs bit field */ char prefix[2]; /* @ + % */ unsigned int op:1; @@ -39,6 +40,7 @@ struct User unsigned int me:1; unsigned int away:1; unsigned int selected:1; + unsigned int typing:2; }; #define USERACCESS_SIZE 12 diff --git a/src/common/zoitechat.c b/src/common/zoitechat.c index aba1c32e..6d595e29 100644 --- a/src/common/zoitechat.c +++ b/src/common/zoitechat.c @@ -793,6 +793,11 @@ session_free (session *killsess) send_quit_or_part (killsess); + if (killsess->typing_timeout_tag) + fe_timeout_remove (killsess->typing_timeout_tag); + if (killsess->typing_animation_tag) + fe_timeout_remove (killsess->typing_animation_tag); + history_free (&killsess->history); g_free (killsess->topic); g_free (killsess->current_modes); diff --git a/src/common/zoitechat.h b/src/common/zoitechat.h index 20d3e5da..e9ba4566 100644 --- a/src/common/zoitechat.h +++ b/src/common/zoitechat.h @@ -431,6 +431,10 @@ typedef struct session char *current_modes; /* free() me */ int mode_timeout_tag; + int typing_timeout_tag; + int typing_status; + int typing_animation_tag; + int typing_animation_frame; struct session *lastlog_sess; struct nbexec *running_exec; @@ -494,6 +498,8 @@ typedef struct server void (*p_set_back)(struct server *); void (*p_set_away)(struct server *, char *reason); void (*p_message)(struct server *, char *channel, char *text); + void (*p_message_tagged)(struct server *, char *tags, char *channel, char *text); + void (*p_tagmsg)(struct server *, char *tags, char *target); void (*p_action)(struct server *, char *channel, char *act); void (*p_notice)(struct server *, char *channel, char *text); void (*p_topic)(struct server *, char *channel, char *topic); @@ -543,6 +549,7 @@ typedef struct server int loginmethod; /* see login_types[] */ char *chantypes; /* for 005 numeric - free me */ + char *clienttagdeny; char *chanmodes; /* for 005 numeric - free me */ char *nick_prefixes; /* e.g. "*@%+" */ char *nick_modes; /* e.g. "aohv" */ @@ -605,6 +612,8 @@ typedef struct server unsigned int have_extjoin:1; /* cap extended-join */ unsigned int have_account_tag:1; /* cap account-tag */ unsigned int have_server_time:1; /* cap server-time */ + unsigned int have_message_tags:1; + unsigned int have_echo_message:1; unsigned int have_sasl:1; /* SASL capability */ unsigned int have_except:1; /* ban exemptions +e */ unsigned int have_invite:1; /* invite exemptions +I */ diff --git a/src/fe-gtk/maingui.c b/src/fe-gtk/maingui.c index 3e5e83fa..c0dab285 100644 --- a/src/fe-gtk/maingui.c +++ b/src/fe-gtk/maingui.c @@ -731,6 +731,96 @@ mg_inputbox_focus (GtkWidget *widget, GdkEventFocus *event, session_gui *gui) return FALSE; } + +static gboolean +mg_client_tag_allowed (server *serv, const char *tag) +{ + char **deny; + int i; + + if (!serv->have_message_tags) + return FALSE; + + if (!serv->clienttagdeny || !*serv->clienttagdeny) + return TRUE; + + deny = g_strsplit (serv->clienttagdeny, ",", 0); + for (i = 0; deny[i]; i++) + { + if (!strcmp (deny[i], "*") || !strcmp (deny[i], tag) || (deny[i][0] == '+' && !strcmp (deny[i] + 1, tag))) + { + g_strfreev (deny); + return FALSE; + } + } + + g_strfreev (deny); + return TRUE; +} + +static void +mg_send_typing (session *sess, const char *state) +{ + char tags[32]; + + if (!sess || !sess->server->connected || !mg_client_tag_allowed (sess->server, "typing") || !sess->channel[0]) + return; + + if (sess->type != SESS_CHANNEL && sess->type != SESS_DIALOG) + return; + + g_snprintf (tags, sizeof (tags), "+typing=%s", state); + sess->server->p_tagmsg (sess->server, tags, sess->channel); +} + +static int +mg_typing_pause_cb (session *sess) +{ + sess->typing_timeout_tag = 0; + if (sess->typing_status == 1) + { + mg_send_typing (sess, "paused"); + sess->typing_status = 2; + } + return 0; +} + +static void +mg_typing_update (session *sess, const char *text) +{ + if (!sess) + return; + + if (sess->typing_timeout_tag) + { + fe_timeout_remove (sess->typing_timeout_tag); + sess->typing_timeout_tag = 0; + } + + if (!text || !*text || text[0] == prefs.hex_input_command_char[0]) + { + if (sess->typing_status) + mg_send_typing (sess, "done"); + sess->typing_status = 0; + return; + } + + if (sess->typing_status != 1) + { + mg_send_typing (sess, "active"); + sess->typing_status = 1; + } + sess->typing_timeout_tag = fe_timeout_add_seconds (6, mg_typing_pause_cb, sess); +} + +static void +mg_inputbox_changed (GtkEditable *editable, session_gui *gui) +{ + key_check_replace_on_change (editable, NULL); + if (current_sess && current_sess->gui == gui) + mg_typing_update (current_sess, gtk_entry_get_text (GTK_ENTRY (editable))); +} + void mg_inputbox_cb (GtkWidget *igad, session_gui *gui) { @@ -4562,7 +4652,7 @@ mg_create_entry (session *sess, GtkWidget *box) g_signal_connect (G_OBJECT (entry), "activate", G_CALLBACK (mg_inputbox_cb), gui); g_signal_connect (G_OBJECT (entry), "changed", - G_CALLBACK (key_check_replace_on_change), NULL); + G_CALLBACK (mg_inputbox_changed), gui); gtk_box_pack_start (GTK_BOX (hbox), entry, TRUE, TRUE, 0); gtk_widget_set_name (entry, "zoitechat-inputbox"); @@ -5194,6 +5284,12 @@ fe_set_channel (session *sess) chan_rename (sess->res->tab, sess->channel, prefs.hex_gui_tab_trunc); } +void +fe_set_typing (session *sess, const char *nick, const char *state) +{ + fe_userlist_set_typing (sess, nick, state); +} + void mg_changui_new (session *sess, restore_gui *res, int tab, int focus) { diff --git a/src/fe-gtk/userlistgui.c b/src/fe-gtk/userlistgui.c index 825a1b08..e8a6d845 100644 --- a/src/fe-gtk/userlistgui.c +++ b/src/fe-gtk/userlistgui.c @@ -19,6 +19,7 @@ #include #include #include +#include #include "fe-gtk.h" @@ -53,6 +54,36 @@ enum static void userlist_store_color (GtkListStore *store, GtkTreeIter *iter, ThemeSemanticToken token, gboolean has_token); +static const char * +userlist_typing_suffix (session *sess, struct User *user) +{ + static const char *active[] = { " [✎]", " [✎.]", " [✎..]" }; + + if (!user || !user->typing) + return ""; + + if (user->typing == 2) + return " [✎…]"; + + return active[sess->typing_animation_frame % G_N_ELEMENTS (active)]; +} + +static char * +userlist_nick_markup (session *sess, struct User *user) +{ + char *nick = g_markup_escape_text (user->nick, -1); + const char *typing = userlist_typing_suffix (sess, user); + + if (*typing) + { + char *marked = g_strdup_printf ("%s%s", nick, typing); + g_free (nick); + return marked; + } + + return nick; +} + static const char * userlist_prefix_color (char prefix) { @@ -463,6 +494,87 @@ fe_userlist_remove (session *sess, struct User *user) return sel; } + +static gboolean +userlist_typing_tick (session *sess) +{ + GtkTreeModel *model; + GtkTreeIter iter; + gboolean valid; + gboolean keep = FALSE; + time_t now = time (NULL); + + if (!sess || !sess->res || !sess->res->user_model) + return FALSE; + + sess->typing_animation_frame++; + model = GTK_TREE_MODEL (sess->res->user_model); + valid = gtk_tree_model_get_iter_first (model, &iter); + + while (valid) + { + struct User *user = NULL; + + gtk_tree_model_get (model, &iter, COL_USER, &user, -1); + if (user && user->typing) + { + char *nick; + + if ((user->typing == 1 && now - user->typing_time >= 6) || (user->typing == 2 && now - user->typing_time >= 30)) + user->typing = 0; + + nick = userlist_nick_markup (sess, user); + gtk_list_store_set (sess->res->user_model, &iter, COL_NICK, nick, -1); + g_free (nick); + if (user->typing) + keep = TRUE; + } + valid = gtk_tree_model_iter_next (model, &iter); + } + + if (!keep) + { + sess->typing_animation_tag = 0; + return FALSE; + } + + return TRUE; +} + +void +fe_userlist_set_typing (session *sess, const char *nick, const char *state) +{ + struct User *user; + GtkTreeIter *iter; + int sel; + + if (!sess || !nick || !sess->res || !sess->res->user_model) + return; + + user = userlist_find (sess, nick); + if (!user) + return; + + if (!strcmp (state, "active")) + user->typing = 1; + else if (!strcmp (state, "paused")) + user->typing = 2; + else + user->typing = 0; + user->typing_time = time (NULL); + + iter = find_row (sess, GTK_TREE_VIEW (sess->gui->user_tree), GTK_TREE_MODEL (sess->res->user_model), user, &sel); + if (iter) + { + char *nick = userlist_nick_markup (sess, user); + gtk_list_store_set (sess->res->user_model, iter, COL_NICK, nick, -1); + g_free (nick); + } + + if (user->typing && !sess->typing_animation_tag) + sess->typing_animation_tag = fe_timeout_add (350, userlist_typing_tick, sess); +} + void fe_userlist_rehash (session *sess, struct User *user) { @@ -493,9 +605,14 @@ fe_userlist_rehash (session *sess, struct User *user) } } - gtk_list_store_set (GTK_LIST_STORE (sess->res->user_model), iter, + { + char *nick = userlist_nick_markup (sess, user); + gtk_list_store_set (GTK_LIST_STORE (sess->res->user_model), iter, + COL_NICK, nick, COL_HOST, user->hostname, -1); + g_free (nick); + } userlist_store_color (GTK_LIST_STORE (sess->res->user_model), iter, nick_token, have_nick_token); } @@ -506,7 +623,6 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel) GdkPixbuf *pix = get_user_icon (sess->server, newuser); GtkTreeIter iter; char *nick; - char *nick_escaped; char *prefix = NULL; char *prefix_escaped; char prefix_text[2]; @@ -530,8 +646,7 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel) } } - nick_escaped = g_markup_escape_text (newuser->nick, -1); - nick = nick_escaped; + nick = userlist_nick_markup (sess, newuser); if (!prefs.hex_gui_ulist_icons) { if (newuser->prefix[0] != '\0' && newuser->prefix[0] != ' ') @@ -559,7 +674,7 @@ fe_userlist_insert (session *sess, struct User *newuser, gboolean sel) userlist_store_color (GTK_LIST_STORE (model), &iter, nick_token, have_nick_token); g_free (prefix); - g_free (nick_escaped); + g_free (nick); userlist_row_map_set (sess, model, newuser, &iter); @@ -757,6 +872,7 @@ userlist_add_columns (GtkTreeView * treeview) gtk_tree_view_column_pack_start (column, renderer, TRUE); gtk_tree_view_column_add_attribute (column, renderer, "markup", COL_NICK); gtk_tree_view_column_add_attribute (column, renderer, THEME_GTK_FOREGROUND_PROPERTY, COL_GDKCOLOR); + column = gtk_tree_view_get_column (GTK_TREE_VIEW (treeview), 1); gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED); gtk_tree_view_column_set_expand (column, TRUE); diff --git a/src/fe-gtk/userlistgui.h b/src/fe-gtk/userlistgui.h index 60cabb01..393ea44b 100644 --- a/src/fe-gtk/userlistgui.h +++ b/src/fe-gtk/userlistgui.h @@ -26,6 +26,7 @@ GtkWidget *userlist_create (GtkWidget *box); GtkListStore *userlist_create_model (session *sess); void userlist_show (session *sess); void userlist_select (session *sess, char *name); +void fe_userlist_set_typing (session *sess, const char *nick, const char *state); char **userlist_selection_list (GtkWidget *widget, int *num_ret); GdkPixbuf *get_user_icon (server *serv, struct User *user); diff --git a/src/fe-text/fe-text.c b/src/fe-text/fe-text.c index 7a03446b..05a509de 100644 --- a/src/fe-text/fe-text.c +++ b/src/fe-text/fe-text.c @@ -647,6 +647,11 @@ void fe_set_topic (struct session *sess, char *topic, char *stripped_topic) { } + +void +fe_set_typing (struct session *sess, const char *nick, const char *state) +{ +} void fe_cleanup (void) {