Improve error reporting from the curses wrapper
[centerim5.git] / src / Conversation.cpp
blobcc6322d9ce5a383c5066437b2dd51306672d6c57
1 // Copyright (C) 2007 Mark Pustjens <pustjens@dds.nl>
2 // Copyright (C) 2010-2015 Petr Pavlu <setup@dagobah.cz>
3 //
4 // This file is part of CenterIM.
5 //
6 // CenterIM is free software; you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License as published by
8 // the Free Software Foundation; either version 2 of the License, or
9 // (at your option) any later version.
11 // CenterIM is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program. If not, see <http://www.gnu.org/licenses/>.
19 #include "Conversation.h"
21 #include "BuddyList.h"
22 #include "Conversations.h"
23 #include "Footer.h"
25 #include <cppconsui/ColorScheme.h>
26 #include <cstdlib>
27 #include <cstring>
28 #include <sys/stat.h>
29 #include "gettext.h"
31 Conversation::Conversation(PurpleConversation *conv)
32 : Window(0, 0, 80, 24), conv_(conv), filename_(nullptr), logfile_(nullptr),
33 input_text_length_(0), room_list_(nullptr), room_list_line_(nullptr)
35 g_assert(conv_ != nullptr);
37 setColorScheme(CenterIM::SCHEME_CONVERSATION);
39 view_ = new CppConsUI::TextView(width_ - 2, height_, true, true);
40 input_ = new CppConsUI::TextEdit(width_ - 2, height_);
41 input_->signal_text_change.connect(
42 sigc::mem_fun(this, &Conversation::onInputTextChange));
43 char *name = g_strdup_printf("[%s] %s",
44 purple_account_get_protocol_name(purple_conversation_get_account(conv_)),
45 purple_conversation_get_name(conv_));
46 line_ = new ConversationLine(name);
47 g_free(name);
48 addWidget(*view_, 1, 0);
49 addWidget(*input_, 1, 1);
50 addWidget(*line_, 0, height_);
52 PurpleConversationType type = purple_conversation_get_type(conv_);
53 if (type == PURPLE_CONV_TYPE_CHAT) {
54 room_list_ = new ConversationRoomList(1, 1, conv_);
55 room_list_line_ = new CppConsUI::VerticalLine(1);
57 addWidget(*room_list_, 1, 0);
58 addWidget(*room_list_line_, 1, 0);
61 input_->grabFocus();
63 // Open logfile.
64 buildLogFilename();
66 GError *err = nullptr;
67 logfile_ = g_io_channel_new_file(filename_, "a", &err);
68 if (logfile_ == nullptr) {
69 LOG->error(_("Error opening conversation logfile '%s' (%s)."), filename_,
70 err->message);
71 g_clear_error(&err);
74 loadHistory();
76 onScreenResized();
77 declareBindables();
80 Conversation::~Conversation()
82 g_free(filename_);
83 if (logfile_ != nullptr)
84 g_io_channel_unref(logfile_);
87 bool Conversation::processInput(const TermKeyKey &key)
89 if (view_->processInput(key))
90 return true;
92 return Window::processInput(key);
95 void Conversation::moveResize(int newx, int newy, int neww, int newh)
97 Window::moveResize(newx, newy, neww, newh);
99 int view_percentage = purple_prefs_get_int(CONF_PREFIX "/chat/partitioning");
100 view_percentage = CLAMP(view_percentage, 0, 100);
102 int view_height = (newh * view_percentage) / 100;
103 if (view_height < 1)
104 view_height = 1;
106 int input_height = newh - view_height - 1;
107 if (input_height < 1)
108 input_height = 1;
110 int roomlist_percentage =
111 purple_prefs_get_int(CONF_PREFIX "/chat/roomlist_partitioning");
112 roomlist_percentage = CLAMP(roomlist_percentage, 0, 100);
114 int view_width = neww - 2;
115 if (room_list_ != nullptr)
116 view_width = (view_width * roomlist_percentage) / 100;
118 view_->moveResize(1, 0, view_width, view_height);
120 input_->moveResize(1, view_height + 1, neww - 2, input_height);
121 line_->moveResize(0, view_height, neww, 1);
123 // Place the room list if it exists.
124 if (room_list_ != nullptr) {
125 // +2 accounts for borders.
126 room_list_line_->moveResize(view_width + 1, 0, 1, view_height);
127 // Give it some padding to make it line up.
128 room_list_->moveResize(
129 view_width + 3, 0, neww - view_width - 3, view_height);
133 bool Conversation::restoreFocus()
135 FOOTER->setText(_("%s buddy list, %s main menu, "
136 "%s/%s/%s next/prev/act conv, %s send, %s expand"),
137 "centerim|buddylist", "centerim|generalmenu", "centerim|conversation-next",
138 "centerim|conversation-prev", "centerim|conversation-active",
139 "conversation|send", "centerim|conversation-expand");
141 return Window::restoreFocus();
144 void Conversation::ungrabFocus()
146 FOOTER->setText(nullptr);
147 Window::ungrabFocus();
150 void Conversation::show()
152 // Update the scrollbar setting. It is delayed until the conversation window
153 // is actually displayed, so screen lines recalculations in TextView (caused
154 // by changing the scrollbar setting) are not triggered if it is not really
155 // necessary.
156 view_->setScrollBar(!CENTERIM->isEnabledExpandedConversationMode());
158 Window::show();
161 void Conversation::close()
163 signal_close(*this);
165 // Next line deletes this object. Do not touch any member variable after this
166 // line.
167 purple_conversation_destroy(conv_);
170 void Conversation::onScreenResized()
172 CppConsUI::Rect r = CENTERIM->getScreenArea(CenterIM::CHAT_AREA);
173 // Make room for conversation list.
174 --r.height;
176 moveResizeRect(r);
179 void Conversation::write(const char *name, const char * /*alias*/,
180 const char *message, PurpleMessageFlags flags, time_t mtime)
182 // Beep on message.
183 if (!(flags & PURPLE_MESSAGE_SEND) &&
184 purple_prefs_get_bool(CONF_PREFIX "/chat/beep_on_msg")) {
185 // TODO Implement correct error handling.
186 CppConsUI::Error error;
187 CppConsUI::Curses::beep(error);
190 // Update the last_activity property.
191 PurpleConversationType type = purple_conversation_get_type(conv_);
192 time_t cur_time = time(nullptr);
194 if (type == PURPLE_CONV_TYPE_IM) {
195 PurpleBlistNode *bnode = PURPLE_BLIST_NODE(
196 purple_find_buddy(purple_conversation_get_account(conv_),
197 purple_conversation_get_name(conv_)));
198 if (bnode) {
199 purple_blist_node_set_int(bnode, "last_activity", cur_time);
201 // Inform the buddy list node that it should update its state.
202 BUDDYLIST->updateNode(bnode);
206 // Write the message.
207 int color;
208 const char *dir;
209 const char *mtype;
210 if (flags & PURPLE_MESSAGE_SEND) {
211 dir = "OUT";
212 mtype = "MSG2"; // cim5 message.
213 color = 1;
215 else if (flags & PURPLE_MESSAGE_RECV) {
216 dir = "IN";
217 mtype = "MSG2"; // cim5 message.
218 color = 2;
220 else {
221 dir = "IN";
222 mtype = "OTHER";
223 color = 0;
226 // Write text into logfile.
227 if (!(flags & PURPLE_MESSAGE_NO_LOG)) {
228 char *log_msg;
229 if (type == PURPLE_CONV_TYPE_CHAT)
230 log_msg = g_strdup_printf("\f\n%s\n%s\n%lu\n%lu\n%s: %s\n", dir, mtype,
231 mtime, cur_time, name, message);
232 else
233 log_msg = g_strdup_printf(
234 "\f\n%s\n%s\n%lu\n%lu\n%s\n", dir, mtype, mtime, cur_time, message);
235 if (logfile_ != nullptr) {
236 GError *err = nullptr;
237 if (g_io_channel_write_chars(logfile_, log_msg, -1, nullptr, &err) !=
238 G_IO_STATUS_NORMAL) {
239 LOG->error(
240 _("Error writing to conversation logfile (%s)."), err->message);
241 g_clear_error(&err);
243 if (g_io_channel_flush(logfile_, &err) != G_IO_STATUS_NORMAL) {
244 LOG->error(
245 _("Error flushing conversation logfile (%s)."), err->message);
246 g_clear_error(&err);
249 g_free(log_msg);
252 // We currently do not support displaying HTML in any way.
253 char *nohtml = stripHTML(message);
255 // Write text to the window.
256 char *time = extractTime(mtime, cur_time);
257 char *msg;
258 if (type == PURPLE_CONV_TYPE_CHAT)
259 msg = g_strdup_printf("%s %s: %s", time, name, nohtml);
260 else
261 msg = g_strdup_printf("%s %s", time, nohtml);
262 view_->append(msg, color);
263 g_free(nohtml);
264 g_free(time);
265 g_free(msg);
268 Conversation::ConversationLine::ConversationLine(const char *text)
269 : AbstractLine(AUTOSIZE, 1)
271 g_assert(text != nullptr);
273 text_ = g_strdup(text);
274 text_width_ = CppConsUI::Curses::onScreenWidth(text_);
277 Conversation::ConversationLine::~ConversationLine()
279 g_free(text_);
282 int Conversation::ConversationLine::draw(
283 CppConsUI::Curses::ViewPort area, CppConsUI::Error &error)
285 if (real_width_ == 0 || real_height_ != 1)
286 return 0;
288 int l;
289 if (text_width_ + 5 >= static_cast<unsigned>(real_width_))
290 l = 0;
291 else
292 l = real_width_ - text_width_ - 5;
294 // Use HorizontalLine colors.
295 int attrs;
296 DRAW(getAttributes(
297 CppConsUI::ColorScheme::PROPERTY_HORIZONTALLINE_LINE, &attrs, error));
298 DRAW(area.attrOn(attrs, error));
300 int i;
301 for (i = 0; i < l; ++i)
302 DRAW(area.addLineChar(i, 0, CppConsUI::Curses::LINE_HLINE, error));
303 int printed;
304 DRAW(area.addString(i, 0, text_, error, &printed));
305 i += printed;
306 for (; i < real_width_; ++i)
307 DRAW(area.addLineChar(i, 0, CppConsUI::Curses::LINE_HLINE, error));
309 DRAW(area.attrOff(attrs, error));
311 return 0;
314 char *Conversation::stripHTML(const char *str) const
316 // Almost copy&paste from libpurple/util.c:purple_markup_strip_html(), but
317 // this version does not convert tab character to a space.
319 if (str == nullptr)
320 return nullptr;
322 int i, j, k, entlen;
323 bool visible = true;
324 bool closing_td_p = false;
325 gchar *str2;
326 const gchar *cdata_close_tag = nullptr, *ent;
327 gchar *href = nullptr;
328 int href_st = 0;
330 str2 = g_strdup(str);
332 for (i = 0, j = 0; str2[i] != '\0'; ++i) {
333 if (str2[i] == '<') {
334 if (cdata_close_tag) {
335 // Note: Do not even assume any other tag is a tag in CDATA.
336 if (g_ascii_strncasecmp(
337 str2 + i, cdata_close_tag, !strlen(cdata_close_tag))) {
338 i += strlen(cdata_close_tag) - 1;
339 cdata_close_tag = nullptr;
341 continue;
343 else if (!g_ascii_strncasecmp(str2 + i, "<td", 3) && closing_td_p) {
344 str2[j++] = '\t';
345 visible = true;
347 else if (!g_ascii_strncasecmp(str2 + i, "</td>", 5)) {
348 closing_td_p = true;
349 visible = false;
351 else {
352 closing_td_p = false;
353 visible = true;
356 k = i + 1;
358 if (g_ascii_isspace(str2[k]))
359 visible = true;
360 else if (str2[k]) {
361 // Scan until we end the tag either implicitly (closed start tag) or
362 // explicitly, using a sloppy method (i.e., < or > inside quoted
363 // attributes will screw us up).
364 while (str2[k] != '\0' && str2[k] != '<' && str2[k] != '>')
365 ++k;
367 // If we have got an <a> tag with an href, save the address to print
368 // later.
369 if (g_ascii_strncasecmp(str2 + i, "<a", 2) == 0 &&
370 g_ascii_isspace(str2[i + 2])) {
371 int st; // Start of href, inclusive [.
372 int end; // End of href, exclusive ).
373 char delim = ' ';
374 // Find start of href.
375 for (st = i + 3; st < k; ++st) {
376 if (g_ascii_strncasecmp(str2 + st, "href=", 5) == 0) {
377 st += 5;
378 if (str2[st] == '"' || str2[st] == '\'') {
379 delim = str2[st];
380 ++st;
382 break;
385 // Find end of address.
386 for (end = st; end < k && str2[end] != delim; ++end) {
387 // All the work is done in the loop construct above.
390 // If there is an address, save it. If there was already one saved,
391 // kill it.
392 if (st < k) {
393 char *tmp;
394 g_free(href);
395 tmp = g_strndup(str2 + st, end - st);
396 href = purple_unescape_html(tmp);
397 g_free(tmp);
398 href_st = j;
402 // Replace </a> with an ascii representation of the address the link was
403 // pointing to.
404 else if (href != nullptr &&
405 g_ascii_strncasecmp(str2 + i, "</a>", 4) == 0) {
406 size_t hrlen = std::strlen(href);
408 // Only insert the href if it is different from the CDATA.
409 // 7 == strlen("http://").
410 if ((hrlen != (unsigned)(j - href_st) ||
411 std::strncmp(str2 + href_st, href, hrlen)) != 0 &&
412 (hrlen != (unsigned)(j - href_st + 7) ||
413 std::strncmp(str2 + href_st, href + 7, hrlen - 7) != 0)) {
414 str2[j++] = ' ';
415 str2[j++] = '(';
416 g_memmove(str2 + j, href, hrlen);
417 j += hrlen;
418 str2[j++] = ')';
419 g_free(href);
420 href = nullptr;
424 // Check for tags which should be mapped to newline (but ignore some of
425 // the tags at the beginning of the text).
426 else if ((j != 0 && (g_ascii_strncasecmp(str2 + i, "<p>", 3) == 0 ||
427 g_ascii_strncasecmp(str2 + i, "<tr", 3) == 0 ||
428 g_ascii_strncasecmp(str2 + i, "<hr", 3) == 0 ||
429 g_ascii_strncasecmp(str2 + i, "<li", 3) == 0 ||
430 g_ascii_strncasecmp(str2 + i, "<div", 4) == 0)) ||
431 g_ascii_strncasecmp(str2 + i, "<br", 3) == 0 ||
432 g_ascii_strncasecmp(str2 + i, "</table>", 8) == 0)
433 str2[j++] = '\n';
434 else if (g_ascii_strncasecmp(str2 + i, "<script", 7) == 0)
435 cdata_close_tag = "</script>";
436 else if (g_ascii_strncasecmp(str2 + i, "<style", 6) == 0)
437 cdata_close_tag = "</style>";
438 // Update the index and continue checking after the tag.
439 i = (str2[k] == '<' || str2[k] == '\0') ? k - 1 : k;
440 continue;
443 else if (cdata_close_tag)
444 continue;
445 else if (!g_ascii_isspace(str2[i]))
446 visible = true;
448 if (str2[i] == '&' &&
449 (ent = purple_markup_unescape_entity(str2 + i, &entlen))) {
450 while (*ent != '\0')
451 str2[j++] = *ent++;
452 i += entlen - 1;
453 continue;
456 if (visible)
457 str2[j++] = g_ascii_isspace(str2[i]) && str[i] != '\t' ? ' ' : str2[i];
460 g_free(href);
462 str2[j] = '\0';
464 return str2;
467 void Conversation::buildLogFilename()
469 PurpleAccount *account = purple_conversation_get_account(conv_);
470 PurplePlugin *prpl =
471 purple_find_prpl(purple_account_get_protocol_id(account));
472 g_assert(prpl != nullptr);
474 const char *proto_name = purple_account_get_protocol_name(account);
476 char *acct_name = g_strdup(purple_escape_filename(
477 purple_normalize(account, purple_account_get_username(account))));
479 const char *name = purple_conversation_get_name(conv_);
481 filename_ = g_build_filename(purple_user_dir(), "clogs", proto_name,
482 acct_name, purple_escape_filename(purple_normalize(account, name)), NULL);
484 char *dir = g_path_get_dirname(filename_);
485 if (g_mkdir_with_parents(dir, S_IRUSR | S_IWUSR | S_IXUSR) == -1)
486 LOG->error(_("Error creating directory '%s'."), dir);
487 g_free(dir);
489 g_free(acct_name);
492 char *Conversation::extractTime(time_t sent_time, time_t show_time) const
494 // Based on the extracttime() function from cim4.
496 // Convert to local time, note that localtime_r() should not really fail.
497 struct tm show_time_local;
498 struct tm sent_time_local;
499 if (localtime_r(&show_time, &show_time_local) == nullptr)
500 memset(&show_time_local, 0, sizeof(show_time_local));
501 if (localtime_r(&sent_time, &sent_time_local) == nullptr)
502 memset(&sent_time_local, 0, sizeof(sent_time_local));
504 // Format the times.
505 char *t1 = g_strdup(purple_date_format_long(&show_time_local));
506 char *t2 = g_strdup(purple_date_format_long(&sent_time_local));
508 int tdiff = std::abs(sent_time - show_time);
510 if (tdiff > 5 && std::strcmp(t1, t2) != 0) {
511 char *res = g_strdup_printf("%s [%s]", t1, t2);
512 g_free(t1);
513 g_free(t2);
514 return res;
517 g_free(t2);
518 return t1;
521 void Conversation::loadHistory()
523 // Open logfile.
524 GError *err = nullptr;
525 GIOChannel *chan = g_io_channel_new_file(filename_, "r", &err);
526 if (chan == nullptr) {
527 LOG->error(_("Error opening conversation logfile '%s' (%s)."), filename_,
528 err->message);
529 g_clear_error(&err);
530 return;
532 // This should never fail.
533 g_io_channel_set_encoding(chan, nullptr, nullptr);
535 GIOStatus st;
536 char *line;
537 bool new_msg = false;
538 // Read conversation logfile line by line.
539 while (new_msg ||
540 (st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) ==
541 G_IO_STATUS_NORMAL) {
542 new_msg = false;
544 // Start flag.
545 if (std::strcmp(line, "\f\n") != 0) {
546 g_free(line);
547 continue;
549 g_free(line);
551 // Parse direction (in/out).
552 if ((st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) !=
553 G_IO_STATUS_NORMAL)
554 break;
555 int color = 0;
556 if (std::strcmp(line, "OUT\n") == 0)
557 color = 1;
558 else if (std::strcmp(line, "IN\n") == 0)
559 color = 2;
560 g_free(line);
562 // Handle type.
563 if ((st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) !=
564 G_IO_STATUS_NORMAL)
565 break;
566 bool cim4 = true;
567 if (std::strcmp(line, "MSG2\n") == 0)
568 cim4 = false;
569 else if (std::strcmp(line, "OTHER\n") == 0) {
570 cim4 = false;
571 color = 0;
573 g_free(line);
575 // Sent time.
576 if ((st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) !=
577 G_IO_STATUS_NORMAL)
578 break;
579 time_t sent_time = atol(line);
580 g_free(line);
582 // Show time.
583 if ((st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) !=
584 G_IO_STATUS_NORMAL)
585 break;
586 time_t show_time = atol(line);
587 g_free(line);
589 if (!cim4) {
590 // cim5, read only one line and strip it off HTML.
591 if ((st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) !=
592 G_IO_STATUS_NORMAL)
593 break;
595 // Validate UTF-8.
596 if (!g_utf8_validate(line, -1, nullptr)) {
597 g_free(line);
598 LOG->error(_("Invalid message detected in conversation logfile"
599 " '%s'. The message was skipped."),
600 filename_);
601 continue;
604 // Write text to the window.
605 char *nohtml = stripHTML(line);
606 char *time = extractTime(sent_time, show_time);
607 char *msg = g_strdup_printf("%s %s", time, nohtml);
608 view_->append(msg, color);
609 g_free(nohtml);
610 g_free(time);
611 g_free(msg);
612 g_free(line);
614 else {
615 // cim4, read multiple raw lines.
616 gsize length;
617 std::string msg;
618 while ((st = g_io_channel_read_line(
619 chan, &line, &length, nullptr, &err)) == G_IO_STATUS_NORMAL &&
620 line != nullptr) {
621 if (std::strcmp(line, "\f\n") == 0) {
622 new_msg = true;
623 break;
626 // Strip '\r' if necessary.
627 if (length > 1 && line[length - 2] == '\r') {
628 line[length - 2] = '\n';
629 line[length - 1] = '\0';
631 msg.append(line);
632 g_free(line);
635 if (!new_msg) {
636 // EOL or I/O error.
637 break;
640 // Validate UTF-8.
641 if (!g_utf8_validate(msg.c_str(), -1, nullptr)) {
642 LOG->error(_("Invalid message detected in conversation logfile"
643 " '%s'. The message was skipped."),
644 filename_);
645 continue;
648 // Add the message to the window.
649 char *time = extractTime(sent_time, show_time);
650 char *final_msg = g_strdup_printf("%s %s", time, msg.c_str());
651 view_->append(final_msg, color);
652 g_free(time);
653 g_free(final_msg);
657 if (st != G_IO_STATUS_EOF) {
658 LOG->error(_("Error reading from conversation logfile '%s' (%s)."),
659 filename_, err->message);
660 g_clear_error(&err);
662 g_io_channel_unref(chan);
665 bool Conversation::processCommand(const char *raw, const char *html)
667 // Check that it is a command.
668 if (std::strncmp(raw, "/", 1) != 0)
669 return false;
671 purple_conversation_write(
672 conv_, "", html, PURPLE_MESSAGE_NO_LOG, time(nullptr));
674 char *error = nullptr;
675 // Strip the prefix and execute the command.
676 PurpleCmdStatus status =
677 purple_cmd_do_command(conv_, raw + 1, html + 1, &error);
679 bool result = true;
680 switch (status) {
681 case PURPLE_CMD_STATUS_OK:
682 break;
683 case PURPLE_CMD_STATUS_NOT_FOUND:
684 // It is not a valid command, process it as a message.
685 result = false;
686 break;
687 case PURPLE_CMD_STATUS_WRONG_ARGS:
688 purple_conversation_write(conv_, "",
689 _("Wrong number of arguments passed to the command."),
690 PURPLE_MESSAGE_NO_LOG, time(nullptr));
691 break;
692 case PURPLE_CMD_STATUS_FAILED:
693 purple_conversation_write(conv_, "",
694 error ? error : _("The command failed for an unknown reason."),
695 PURPLE_MESSAGE_NO_LOG, time(nullptr));
696 break;
697 case PURPLE_CMD_STATUS_WRONG_TYPE:
698 if (purple_conversation_get_type(conv_) == PURPLE_CONV_TYPE_IM)
699 purple_conversation_write(conv_, "",
700 _("The command only works in chats, not IMs."), PURPLE_MESSAGE_NO_LOG,
701 time(nullptr));
702 else
703 purple_conversation_write(conv_, "",
704 _("The command only works in IMs, not chats."), PURPLE_MESSAGE_NO_LOG,
705 time(nullptr));
706 break;
707 case PURPLE_CMD_STATUS_WRONG_PRPL:
708 purple_conversation_write(conv_, "",
709 _("The command does not work on this protocol."), PURPLE_MESSAGE_NO_LOG,
710 time(nullptr));
711 break;
714 g_free(error);
716 return result;
719 void Conversation::onInputTextChange(CppConsUI::TextEdit &activator)
721 PurpleConvIm *im = PURPLE_CONV_IM(conv_);
722 if (im == nullptr)
723 return;
725 if (!CONVERSATIONS->getSendTypingPref()) {
726 input_text_length_ = 0;
727 return;
730 size_t old_text_length = input_text_length_;
731 size_t new_text_length = activator.getTextLength();
732 input_text_length_ = new_text_length;
734 if (new_text_length == 0) {
735 // All text is deleted, turn off typing.
736 purple_conv_im_stop_send_typed_timeout(im);
738 serv_send_typing(purple_conversation_get_gc(conv_),
739 purple_conversation_get_name(conv_), PURPLE_NOT_TYPING);
740 return;
743 purple_conv_im_stop_send_typed_timeout(im);
744 purple_conv_im_start_send_typed_timeout(im);
746 time_t again = purple_conv_im_get_type_again(im);
747 if ((old_text_length == 0 && new_text_length != 0) ||
748 (again != 0 && time(nullptr) > again)) {
749 // The first letter is inserted or update is required for typing status.
750 unsigned int timeout = serv_send_typing(purple_conversation_get_gc(conv_),
751 purple_conversation_get_name(conv_), PURPLE_TYPING);
752 purple_conv_im_set_type_again(im, timeout);
756 void Conversation::actionSend()
758 const char *str = input_->getText();
759 if (str == nullptr || str[0] == '\0')
760 return;
762 purple_idle_touch();
764 char *escaped = purple_markup_escape_text(str, strlen(str));
765 char *html = purple_strdup_withhtml(escaped);
766 if (processCommand(str, html)) {
767 // The command was processed.
769 else {
770 PurpleConversationType type = purple_conversation_get_type(conv_);
771 if (type == PURPLE_CONV_TYPE_CHAT)
772 purple_conv_chat_send(PURPLE_CONV_CHAT(conv_), html);
773 else if (type == PURPLE_CONV_TYPE_IM)
774 purple_conv_im_send(PURPLE_CONV_IM(conv_), html);
776 g_free(html);
777 g_free(escaped);
778 input_->clear();
781 void Conversation::declareBindables()
783 declareBindable("conversation", "send",
784 sigc::mem_fun(this, &Conversation::actionSend),
785 InputProcessor::BINDABLE_OVERRIDE);
788 // vim: set tabstop=2 shiftwidth=2 textwidth=80 expandtab: