Use pkg-config to find ncursesw
[centerim5.git] / src / Conversation.cpp
blob2558d56855b048dcab067f0a9c58814e8c4bad96
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 CenterIM. 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 "gettext.h"
26 #include <cppconsui/ColorScheme.h>
27 #include <cstdlib>
28 #include <cstring>
29 #include <sys/stat.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 // Calculate inner area.
103 neww = neww < 2 ? 0 : neww - 2;
104 newh = newh < 2 ? 0 : newh - 2;
106 // ,- room_list_line_
107 // v
108 // ,-----------------------------,
109 // | view_ | room_list_ |
110 // | | |
111 // | | |
112 // |-----------------------------|<- line_
113 // | input_ |
114 // '-----------------------------'
116 int view_height = (newh * view_percentage) / 100;
118 int roomlist_percentage =
119 purple_prefs_get_int(CONF_PREFIX "/chat/roomlist_partitioning");
120 roomlist_percentage = CLAMP(roomlist_percentage, 0, 100);
122 int view_width = neww;
123 if (room_list_ != nullptr)
124 view_width = (view_width * roomlist_percentage) / 100;
126 view_->moveResize(1, 1, view_width, view_height);
127 line_->moveResize(1, 1 + view_height, neww, 1);
128 input_->moveResize(1, 1 + view_height + 1, neww, newh - view_height - 2);
130 // Place the room list if it exists.
131 if (room_list_ != nullptr) {
132 room_list_line_->moveResize(1 + view_width, 1, 1, view_height);
133 room_list_->moveResize(
134 1 + view_width + 1, 1, neww - view_width - 2, view_height);
138 bool Conversation::restoreFocus()
140 FOOTER->setText(_("%s buddy list, %s main menu, "
141 "%s/%s/%s next/prev/act conv, %s send, %s expand"),
142 "centerim|buddylist", "centerim|generalmenu", "centerim|conversation-next",
143 "centerim|conversation-prev", "centerim|conversation-active",
144 "conversation|send", "centerim|conversation-expand");
146 return Window::restoreFocus();
149 void Conversation::ungrabFocus()
151 FOOTER->setText(nullptr);
152 Window::ungrabFocus();
155 void Conversation::show()
157 // Update the scrollbar setting. It is delayed until the conversation window
158 // is actually displayed, so screen lines recalculations in TextView (caused
159 // by changing the scrollbar setting) are not triggered if it is not really
160 // necessary.
161 view_->setScrollBar(!CENTERIM->isEnabledExpandedConversationMode());
163 Window::show();
166 void Conversation::close()
168 signal_close(*this);
170 // Next line deletes this object. Do not touch any member variable after this
171 // line.
172 purple_conversation_destroy(conv_);
175 void Conversation::onScreenResized()
177 CppConsUI::Rect r = CENTERIM->getScreenArea(CenterIM::CHAT_AREA);
178 // Make room for conversation list.
179 --r.height;
181 moveResizeRect(r);
184 void Conversation::write(const char *name, const char * /*alias*/,
185 const char *message, PurpleMessageFlags flags, time_t mtime)
187 // Beep on message.
188 if (!(flags & PURPLE_MESSAGE_SEND) &&
189 purple_prefs_get_bool(CONF_PREFIX "/chat/beep_on_msg")) {
190 CppConsUI::Error error;
191 if (CppConsUI::Curses::beep(error) != 0)
192 LOG->error("%s", error.getString());
195 // Update the last_activity property.
196 PurpleConversationType type = purple_conversation_get_type(conv_);
197 time_t cur_time = time(nullptr);
199 if (type == PURPLE_CONV_TYPE_IM) {
200 PurpleBlistNode *bnode = PURPLE_BLIST_NODE(
201 purple_find_buddy(purple_conversation_get_account(conv_),
202 purple_conversation_get_name(conv_)));
203 if (bnode) {
204 purple_blist_node_set_int(bnode, "last_activity", cur_time);
206 // Inform the buddy list node that it should update its state.
207 BUDDYLIST->updateNode(bnode);
211 // Write the message.
212 int color;
213 const char *dir;
214 const char *mtype;
215 if (flags & PURPLE_MESSAGE_SEND) {
216 dir = "OUT";
217 mtype = "MSG2"; // cim5 message.
218 color = 1;
220 else if (flags & PURPLE_MESSAGE_RECV) {
221 dir = "IN";
222 mtype = "MSG2"; // cim5 message.
223 color = 2;
225 else {
226 dir = "IN";
227 mtype = "OTHER";
228 color = 0;
231 // Write text into logfile.
232 if (!(flags & PURPLE_MESSAGE_NO_LOG)) {
233 char *log_msg;
234 if (type == PURPLE_CONV_TYPE_CHAT)
235 log_msg = g_strdup_printf("\f\n%s\n%s\n%lu\n%lu\n%s: %s\n", dir, mtype,
236 mtime, cur_time, name, message);
237 else
238 log_msg = g_strdup_printf(
239 "\f\n%s\n%s\n%lu\n%lu\n%s\n", dir, mtype, mtime, cur_time, message);
240 if (logfile_ != nullptr) {
241 GError *err = nullptr;
242 if (g_io_channel_write_chars(logfile_, log_msg, -1, nullptr, &err) !=
243 G_IO_STATUS_NORMAL) {
244 LOG->error(
245 _("Error writing to conversation logfile (%s)."), err->message);
246 g_clear_error(&err);
248 if (g_io_channel_flush(logfile_, &err) != G_IO_STATUS_NORMAL) {
249 LOG->error(
250 _("Error flushing conversation logfile (%s)."), err->message);
251 g_clear_error(&err);
254 g_free(log_msg);
257 // We currently do not support displaying HTML in any way.
258 char *nohtml = stripHTML(message);
260 // Write text to the window.
261 char *time = extractTime(mtime, cur_time);
262 char *msg;
263 if (type == PURPLE_CONV_TYPE_CHAT)
264 msg = g_strdup_printf("%s %s: %s", time, name, nohtml);
265 else
266 msg = g_strdup_printf("%s %s", time, nohtml);
267 view_->append(msg, color);
268 g_free(nohtml);
269 g_free(time);
270 g_free(msg);
273 Conversation::ConversationLine::ConversationLine(const char *text)
274 : AbstractLine(AUTOSIZE, 1)
276 g_assert(text != nullptr);
278 text_ = g_strdup(text);
279 text_width_ = CppConsUI::Curses::onScreenWidth(text_);
282 Conversation::ConversationLine::~ConversationLine()
284 g_free(text_);
287 int Conversation::ConversationLine::draw(
288 CppConsUI::Curses::ViewPort area, CppConsUI::Error &error)
290 if (real_width_ == 0 || real_height_ != 1)
291 return 0;
293 int l;
294 if (text_width_ + 5 >= static_cast<unsigned>(real_width_))
295 l = 0;
296 else
297 l = real_width_ - text_width_ - 5;
299 // Use HorizontalLine colors.
300 int attrs;
301 DRAW(getAttributes(
302 CppConsUI::ColorScheme::PROPERTY_HORIZONTALLINE_LINE, &attrs, error));
303 DRAW(area.attrOn(attrs, error));
305 int i;
306 for (i = 0; i < l; ++i)
307 DRAW(area.addLineChar(i, 0, CppConsUI::Curses::LINE_HLINE, error));
308 int printed;
309 DRAW(area.addString(i, 0, text_, error, &printed));
310 i += printed;
311 for (; i < real_width_; ++i)
312 DRAW(area.addLineChar(i, 0, CppConsUI::Curses::LINE_HLINE, error));
314 DRAW(area.attrOff(attrs, error));
316 return 0;
319 char *Conversation::stripHTML(const char *str) const
321 // Almost copy&paste from libpurple/util.c:purple_markup_strip_html(), but
322 // this version does not convert tab character to a space.
324 if (str == nullptr)
325 return nullptr;
327 int i, j, k, entlen;
328 bool visible = true;
329 bool closing_td_p = false;
330 gchar *str2;
331 const gchar *cdata_close_tag = nullptr, *ent;
332 gchar *href = nullptr;
333 int href_st = 0;
335 str2 = g_strdup(str);
337 for (i = 0, j = 0; str2[i] != '\0'; ++i) {
338 if (str2[i] == '<') {
339 if (cdata_close_tag) {
340 // Note: Do not even assume any other tag is a tag in CDATA.
341 if (g_ascii_strncasecmp(
342 str2 + i, cdata_close_tag, !strlen(cdata_close_tag))) {
343 i += strlen(cdata_close_tag) - 1;
344 cdata_close_tag = nullptr;
346 continue;
348 else if (!g_ascii_strncasecmp(str2 + i, "<td", 3) && closing_td_p) {
349 str2[j++] = '\t';
350 visible = true;
352 else if (!g_ascii_strncasecmp(str2 + i, "</td>", 5)) {
353 closing_td_p = true;
354 visible = false;
356 else {
357 closing_td_p = false;
358 visible = true;
361 k = i + 1;
363 if (g_ascii_isspace(str2[k]))
364 visible = true;
365 else if (str2[k]) {
366 // Scan until we end the tag either implicitly (closed start tag) or
367 // explicitly, using a sloppy method (i.e., < or > inside quoted
368 // attributes will screw us up).
369 while (str2[k] != '\0' && str2[k] != '<' && str2[k] != '>')
370 ++k;
372 // If we have got an <a> tag with an href, save the address to print
373 // later.
374 if (g_ascii_strncasecmp(str2 + i, "<a", 2) == 0 &&
375 g_ascii_isspace(str2[i + 2])) {
376 int st; // Start of href, inclusive [.
377 int end; // End of href, exclusive ).
378 char delim = ' ';
379 // Find start of href.
380 for (st = i + 3; st < k; ++st) {
381 if (g_ascii_strncasecmp(str2 + st, "href=", 5) == 0) {
382 st += 5;
383 if (str2[st] == '"' || str2[st] == '\'') {
384 delim = str2[st];
385 ++st;
387 break;
390 // Find end of address.
391 for (end = st; end < k && str2[end] != delim; ++end) {
392 // All the work is done in the loop construct above.
395 // If there is an address, save it. If there was already one saved,
396 // kill it.
397 if (st < k) {
398 char *tmp;
399 g_free(href);
400 tmp = g_strndup(str2 + st, end - st);
401 href = purple_unescape_html(tmp);
402 g_free(tmp);
403 href_st = j;
407 // Replace </a> with an ascii representation of the address the link was
408 // pointing to.
409 else if (href != nullptr &&
410 g_ascii_strncasecmp(str2 + i, "</a>", 4) == 0) {
411 std::size_t hrlen = std::strlen(href);
413 // Only insert the href if it is different from the CDATA.
414 // 7 == strlen("http://").
415 if ((hrlen != (unsigned)(j - href_st) ||
416 std::strncmp(str2 + href_st, href, hrlen)) != 0 &&
417 (hrlen != (unsigned)(j - href_st + 7) ||
418 std::strncmp(str2 + href_st, href + 7, hrlen - 7) != 0)) {
419 str2[j++] = ' ';
420 str2[j++] = '(';
421 g_memmove(str2 + j, href, hrlen);
422 j += hrlen;
423 str2[j++] = ')';
424 g_free(href);
425 href = nullptr;
429 // Check for tags which should be mapped to newline (but ignore some of
430 // the tags at the beginning of the text).
431 else if ((j != 0 && (g_ascii_strncasecmp(str2 + i, "<p>", 3) == 0 ||
432 g_ascii_strncasecmp(str2 + i, "<tr", 3) == 0 ||
433 g_ascii_strncasecmp(str2 + i, "<hr", 3) == 0 ||
434 g_ascii_strncasecmp(str2 + i, "<li", 3) == 0 ||
435 g_ascii_strncasecmp(str2 + i, "<div", 4) == 0)) ||
436 g_ascii_strncasecmp(str2 + i, "<br", 3) == 0 ||
437 g_ascii_strncasecmp(str2 + i, "</table>", 8) == 0)
438 str2[j++] = '\n';
439 else if (g_ascii_strncasecmp(str2 + i, "<script", 7) == 0)
440 cdata_close_tag = "</script>";
441 else if (g_ascii_strncasecmp(str2 + i, "<style", 6) == 0)
442 cdata_close_tag = "</style>";
443 // Update the index and continue checking after the tag.
444 i = (str2[k] == '<' || str2[k] == '\0') ? k - 1 : k;
445 continue;
448 else if (cdata_close_tag)
449 continue;
450 else if (!g_ascii_isspace(str2[i]))
451 visible = true;
453 if (str2[i] == '&' &&
454 (ent = purple_markup_unescape_entity(str2 + i, &entlen))) {
455 while (*ent != '\0')
456 str2[j++] = *ent++;
457 i += entlen - 1;
458 continue;
461 if (visible)
462 str2[j++] = g_ascii_isspace(str2[i]) && str[i] != '\t' ? ' ' : str2[i];
465 g_free(href);
467 str2[j] = '\0';
469 return str2;
472 void Conversation::buildLogFilename()
474 PurpleAccount *account = purple_conversation_get_account(conv_);
475 PurplePlugin *prpl =
476 purple_find_prpl(purple_account_get_protocol_id(account));
477 g_assert(prpl != nullptr);
479 const char *proto_name = purple_account_get_protocol_name(account);
481 char *acct_name = g_strdup(purple_escape_filename(
482 purple_normalize(account, purple_account_get_username(account))));
484 const char *name = purple_conversation_get_name(conv_);
486 filename_ =
487 g_build_filename(purple_user_dir(), "clogs", proto_name, acct_name,
488 purple_escape_filename(purple_normalize(account, name)), nullptr);
490 char *dir = g_path_get_dirname(filename_);
491 if (g_mkdir_with_parents(dir, S_IRUSR | S_IWUSR | S_IXUSR) == -1)
492 LOG->error(_("Error creating directory '%s'."), dir);
493 g_free(dir);
495 g_free(acct_name);
498 char *Conversation::extractTime(time_t sent_time, time_t show_time) const
500 // Based on the extracttime() function from cim4.
502 // Convert to local time, note that localtime_r() should not really fail.
503 struct tm show_time_local;
504 struct tm sent_time_local;
505 if (localtime_r(&show_time, &show_time_local) == nullptr)
506 std::memset(&show_time_local, 0, sizeof(show_time_local));
507 if (localtime_r(&sent_time, &sent_time_local) == nullptr)
508 std::memset(&sent_time_local, 0, sizeof(sent_time_local));
510 // Format the times.
511 char *t1 = g_strdup(purple_date_format_long(&show_time_local));
512 char *t2 = g_strdup(purple_date_format_long(&sent_time_local));
514 int tdiff = std::abs(sent_time - show_time);
516 if (tdiff > 5 && std::strcmp(t1, t2) != 0) {
517 char *res = g_strdup_printf("%s [%s]", t1, t2);
518 g_free(t1);
519 g_free(t2);
520 return res;
523 g_free(t2);
524 return t1;
527 void Conversation::loadHistory()
529 // Open logfile.
530 GError *err = nullptr;
531 GIOChannel *chan = g_io_channel_new_file(filename_, "r", &err);
532 if (chan == nullptr) {
533 LOG->error(_("Error opening conversation logfile '%s' (%s)."), filename_,
534 err->message);
535 g_clear_error(&err);
536 return;
538 // This should never fail.
539 g_io_channel_set_encoding(chan, nullptr, nullptr);
541 GIOStatus st;
542 char *line;
543 bool new_msg = false;
544 // Read conversation logfile line by line.
545 while (new_msg ||
546 (st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) ==
547 G_IO_STATUS_NORMAL) {
548 new_msg = false;
550 // Start flag.
551 if (std::strcmp(line, "\f\n") != 0) {
552 g_free(line);
553 continue;
555 g_free(line);
557 // Parse direction (in/out).
558 if ((st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) !=
559 G_IO_STATUS_NORMAL)
560 break;
561 int color = 0;
562 if (std::strcmp(line, "OUT\n") == 0)
563 color = 1;
564 else if (std::strcmp(line, "IN\n") == 0)
565 color = 2;
566 g_free(line);
568 // Handle type.
569 if ((st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) !=
570 G_IO_STATUS_NORMAL)
571 break;
572 bool cim4 = true;
573 if (std::strcmp(line, "MSG2\n") == 0)
574 cim4 = false;
575 else if (std::strcmp(line, "OTHER\n") == 0) {
576 cim4 = false;
577 color = 0;
579 g_free(line);
581 // Sent time.
582 if ((st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) !=
583 G_IO_STATUS_NORMAL)
584 break;
585 time_t sent_time = atol(line);
586 g_free(line);
588 // Show time.
589 if ((st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) !=
590 G_IO_STATUS_NORMAL)
591 break;
592 time_t show_time = atol(line);
593 g_free(line);
595 if (!cim4) {
596 // cim5, read only one line and strip it off HTML.
597 if ((st = g_io_channel_read_line(chan, &line, nullptr, nullptr, &err)) !=
598 G_IO_STATUS_NORMAL)
599 break;
601 // Validate UTF-8.
602 if (!g_utf8_validate(line, -1, nullptr)) {
603 g_free(line);
604 LOG->error(_("Invalid message detected in conversation logfile"
605 " '%s'. The message was skipped."),
606 filename_);
607 continue;
610 // Write text to the window.
611 char *nohtml = stripHTML(line);
612 char *time = extractTime(sent_time, show_time);
613 char *msg = g_strdup_printf("%s %s", time, nohtml);
614 view_->append(msg, color);
615 g_free(nohtml);
616 g_free(time);
617 g_free(msg);
618 g_free(line);
620 else {
621 // cim4, read multiple raw lines.
622 gsize length;
623 std::string msg;
624 while ((st = g_io_channel_read_line(
625 chan, &line, &length, nullptr, &err)) == G_IO_STATUS_NORMAL &&
626 line != nullptr) {
627 if (std::strcmp(line, "\f\n") == 0) {
628 new_msg = true;
629 break;
632 // Strip '\r' if necessary.
633 if (length > 1 && line[length - 2] == '\r') {
634 line[length - 2] = '\n';
635 line[length - 1] = '\0';
637 msg.append(line);
638 g_free(line);
641 if (!new_msg) {
642 // EOL or I/O error.
643 break;
646 // Validate UTF-8.
647 if (!g_utf8_validate(msg.c_str(), -1, nullptr)) {
648 LOG->error(_("Invalid message detected in conversation logfile"
649 " '%s'. The message was skipped."),
650 filename_);
651 continue;
654 // Add the message to the window.
655 char *time = extractTime(sent_time, show_time);
656 char *final_msg = g_strdup_printf("%s %s", time, msg.c_str());
657 view_->append(final_msg, color);
658 g_free(time);
659 g_free(final_msg);
663 if (st != G_IO_STATUS_EOF) {
664 LOG->error(_("Error reading from conversation logfile '%s' (%s)."),
665 filename_, err->message);
666 g_clear_error(&err);
668 g_io_channel_unref(chan);
671 bool Conversation::processCommand(const char *raw, const char *html)
673 // Check that it is a command.
674 if (std::strncmp(raw, "/", 1) != 0)
675 return false;
677 purple_conversation_write(
678 conv_, "", html, PURPLE_MESSAGE_NO_LOG, time(nullptr));
680 char *error = nullptr;
681 // Strip the prefix and execute the command.
682 PurpleCmdStatus status =
683 purple_cmd_do_command(conv_, raw + 1, html + 1, &error);
685 bool result = true;
686 switch (status) {
687 case PURPLE_CMD_STATUS_OK:
688 break;
689 case PURPLE_CMD_STATUS_NOT_FOUND:
690 // It is not a valid command, process it as a message.
691 result = false;
692 break;
693 case PURPLE_CMD_STATUS_WRONG_ARGS:
694 purple_conversation_write(conv_, "",
695 _("Wrong number of arguments passed to the command."),
696 PURPLE_MESSAGE_NO_LOG, time(nullptr));
697 break;
698 case PURPLE_CMD_STATUS_FAILED:
699 purple_conversation_write(conv_, "",
700 error ? error : _("The command failed for an unknown reason."),
701 PURPLE_MESSAGE_NO_LOG, time(nullptr));
702 break;
703 case PURPLE_CMD_STATUS_WRONG_TYPE:
704 if (purple_conversation_get_type(conv_) == PURPLE_CONV_TYPE_IM)
705 purple_conversation_write(conv_, "",
706 _("The command only works in chats, not IMs."), PURPLE_MESSAGE_NO_LOG,
707 time(nullptr));
708 else
709 purple_conversation_write(conv_, "",
710 _("The command only works in IMs, not chats."), PURPLE_MESSAGE_NO_LOG,
711 time(nullptr));
712 break;
713 case PURPLE_CMD_STATUS_WRONG_PRPL:
714 purple_conversation_write(conv_, "",
715 _("The command does not work on this protocol."), PURPLE_MESSAGE_NO_LOG,
716 time(nullptr));
717 break;
720 g_free(error);
722 return result;
725 void Conversation::onInputTextChange(CppConsUI::TextEdit &activator)
727 PurpleConvIm *im = PURPLE_CONV_IM(conv_);
728 if (im == nullptr)
729 return;
731 if (!CONVERSATIONS->getSendTypingPref()) {
732 input_text_length_ = 0;
733 return;
736 std::size_t old_text_length = input_text_length_;
737 std::size_t new_text_length = activator.getTextLength();
738 input_text_length_ = new_text_length;
740 if (new_text_length == 0) {
741 // All text is deleted, turn off typing.
742 purple_conv_im_stop_send_typed_timeout(im);
744 serv_send_typing(purple_conversation_get_gc(conv_),
745 purple_conversation_get_name(conv_), PURPLE_NOT_TYPING);
746 return;
749 purple_conv_im_stop_send_typed_timeout(im);
750 purple_conv_im_start_send_typed_timeout(im);
752 time_t again = purple_conv_im_get_type_again(im);
753 if ((old_text_length == 0 && new_text_length != 0) ||
754 (again != 0 && time(nullptr) > again)) {
755 // The first letter is inserted or update is required for typing status.
756 unsigned int timeout = serv_send_typing(purple_conversation_get_gc(conv_),
757 purple_conversation_get_name(conv_), PURPLE_TYPING);
758 purple_conv_im_set_type_again(im, timeout);
762 void Conversation::actionSend()
764 const char *str = input_->getText();
765 if (str == nullptr || str[0] == '\0')
766 return;
768 purple_idle_touch();
770 char *escaped = purple_markup_escape_text(str, strlen(str));
771 char *html = purple_strdup_withhtml(escaped);
772 if (processCommand(str, html)) {
773 // The command was processed.
775 else {
776 PurpleConversationType type = purple_conversation_get_type(conv_);
777 if (type == PURPLE_CONV_TYPE_CHAT)
778 purple_conv_chat_send(PURPLE_CONV_CHAT(conv_), html);
779 else if (type == PURPLE_CONV_TYPE_IM)
780 purple_conv_im_send(PURPLE_CONV_IM(conv_), html);
782 g_free(html);
783 g_free(escaped);
784 input_->clear();
787 void Conversation::declareBindables()
789 declareBindable("conversation", "send",
790 sigc::mem_fun(this, &Conversation::actionSend),
791 InputProcessor::BINDABLE_OVERRIDE);
794 // vim: set tabstop=2 shiftwidth=2 textwidth=80 expandtab: