Update AUTHORS file
[centerim5.git] / src / Conversation.cpp
blobeec2e1d0e0bf1b904c06d798c4b73774502ed798
1 /*
2 * Copyright (C) 2007 Mark Pustjens <pustjens@dds.nl>
3 * Copyright (C) 2010-2015 Petr Pavlu <setup@dagobah.cz>
5 * This file is part of CenterIM.
7 * CenterIM is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * CenterIM is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #include "Conversation.h"
24 #include "BuddyList.h"
25 #include "Conversations.h"
26 #include "Footer.h"
28 #include <sys/stat.h>
29 #include "gettext.h"
31 Conversation::Conversation(PurpleConversation *conv_)
32 : Window(0, 0, 80, 24), conv(conv_), filename(NULL), logfile(NULL),
33 input_text_length(0), room_list(NULL), room_list_line(NULL)
35 g_assert(conv);
37 setColorScheme("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 = NULL;
67 if (!(logfile = g_io_channel_new_file(filename, "a", &err))) {
68 LOG->error(_("Error opening conversation logfile '%s' (%s)."), filename,
69 err->message);
70 g_clear_error(&err);
73 loadHistory();
75 onScreenResized();
76 declareBindables();
79 Conversation::~Conversation()
81 g_free(filename);
82 if (logfile)
83 g_io_channel_unref(logfile);
86 bool Conversation::processInput(const TermKeyKey &key)
88 if (view->processInput(key))
89 return true;
91 return Window::processInput(key);
94 void Conversation::moveResize(int newx, int newy, int neww, int newh)
96 Window::moveResize(newx, newy, neww, newh);
98 int view_percentage = purple_prefs_get_int(CONF_PREFIX "/chat/partitioning");
99 view_percentage = CLAMP(view_percentage, 0, 100);
101 int view_height = (newh * view_percentage) / 100;
102 if (view_height < 1)
103 view_height = 1;
105 int input_height = newh - view_height - 1;
106 if (input_height < 1)
107 input_height = 1;
109 int roomlist_percentage =
110 purple_prefs_get_int(CONF_PREFIX "/chat/roomlist_partitioning");
111 roomlist_percentage = CLAMP(roomlist_percentage, 0, 100);
113 int view_width = neww - 2;
114 if (room_list)
115 view_width = (view_width * roomlist_percentage) / 100;
117 view->moveResize(1, 0, view_width, view_height);
119 input->moveResize(1, view_height + 1, neww - 2, input_height);
120 line->moveResize(0, view_height, neww, 1);
122 // place the room list if a conversation window
123 if (room_list) {
124 // +2 accounts for borders
125 room_list_line->moveResize(view_width + 1, 0, 1, view_height);
126 // give it some padding to make it line up
127 room_list->moveResize(
128 view_width + 3, 0, neww - view_width - 3, view_height);
132 bool Conversation::restoreFocus()
134 FOOTER->setText(_("%s buddy list, %s main menu, "
135 "%s/%s/%s next/prev/act conv, %s send, %s expand"),
136 "centerim|buddylist", "centerim|generalmenu", "centerim|conversation-next",
137 "centerim|conversation-prev", "centerim|conversation-active",
138 "conversation|send", "centerim|conversation-expand");
140 return Window::restoreFocus();
143 void Conversation::ungrabFocus()
145 FOOTER->setText(NULL);
146 Window::ungrabFocus();
149 void Conversation::show()
151 /* Update the scrollbar setting. It is delayed until the conversation window
152 * is actually displayed, so screen lines recalculations in TextView (caused
153 * by changing the scrollbar setting) aren't triggered if it isn't really
154 * necessary. */
155 view->setScrollBar(!CENTERIM->isEnabledExpandedConversationMode());
157 Window::show();
160 void Conversation::close()
162 signal_close(*this);
164 /* Next line deletes this object. Don't touch any member variable after this
165 * line. */
166 purple_conversation_destroy(conv);
169 void Conversation::onScreenResized()
171 CppConsUI::Rect r = CENTERIM->getScreenArea(CenterIM::CHAT_AREA);
172 // make room for conversations list
173 r.height--;
175 moveResizeRect(r);
178 void Conversation::write(const char *name, const char * /*alias*/,
179 const char *message, PurpleMessageFlags flags, time_t mtime)
181 // beep on message
182 if (!(flags & PURPLE_MESSAGE_SEND) &&
183 purple_prefs_get_bool(CONF_PREFIX "/chat/beep_on_msg"))
184 CppConsUI::Curses::beep();
186 // update the last_activity property
187 PurpleConversationType type = purple_conversation_get_type(conv);
188 time_t cur_time = time(NULL);
190 if (type == PURPLE_CONV_TYPE_IM) {
191 PurpleBlistNode *bnode =
192 PURPLE_BLIST_NODE(purple_find_buddy(purple_conversation_get_account(conv),
193 purple_conversation_get_name(conv)));
194 if (bnode) {
195 purple_blist_node_set_int(bnode, "last_activity", cur_time);
197 // inform the buddy list node that it should update its state
198 BUDDYLIST->updateNode(bnode);
202 // write the message
203 int color;
204 const char *dir;
205 const char *mtype;
206 if (flags & PURPLE_MESSAGE_SEND) {
207 dir = "OUT";
208 mtype = "MSG2"; // cim5 message
209 color = 1;
211 else if (flags & PURPLE_MESSAGE_RECV) {
212 dir = "IN";
213 mtype = "MSG2"; // cim5 message
214 color = 2;
216 else {
217 dir = "IN";
218 mtype = "OTHER";
219 color = 0;
222 // write text into logfile
223 if (!(flags & PURPLE_MESSAGE_NO_LOG)) {
224 char *log_msg;
225 if (type == PURPLE_CONV_TYPE_CHAT)
226 log_msg = g_strdup_printf("\f\n%s\n%s\n%lu\n%lu\n%s: %s\n", dir, mtype,
227 mtime, cur_time, name, message);
228 else
229 log_msg = g_strdup_printf(
230 "\f\n%s\n%s\n%lu\n%lu\n%s\n", dir, mtype, mtime, cur_time, message);
231 if (logfile) {
232 GError *err = NULL;
233 if (g_io_channel_write_chars(logfile, log_msg, -1, NULL, &err) !=
234 G_IO_STATUS_NORMAL) {
235 LOG->error(
236 _("Error writing to conversation logfile (%s)."), err->message);
237 g_clear_error(&err);
239 if (g_io_channel_flush(logfile, &err) != G_IO_STATUS_NORMAL) {
240 LOG->error(
241 _("Error flushing conversation logfile (%s)."), err->message);
242 g_clear_error(&err);
245 g_free(log_msg);
248 // we currently don't support displaying HTML in any way
249 char *nohtml = stripHTML(message);
251 // write text to the window
252 char *time = extractTime(mtime, cur_time);
253 char *msg;
254 if (type == PURPLE_CONV_TYPE_CHAT)
255 msg = g_strdup_printf("%s %s: %s", time, name, nohtml);
256 else
257 msg = g_strdup_printf("%s %s", time, nohtml);
258 view->append(msg, color);
259 g_free(nohtml);
260 g_free(time);
261 g_free(msg);
264 Conversation::ConversationLine::ConversationLine(const char *text_)
265 : AbstractLine(AUTOSIZE, 1)
267 g_assert(text_);
269 text = g_strdup(text_);
270 text_width = CppConsUI::Curses::onScreenWidth(text);
273 Conversation::ConversationLine::~ConversationLine()
275 g_free(text);
278 void Conversation::ConversationLine::draw(CppConsUI::Curses::ViewPort area)
280 if (real_width == 0 || real_height != 1)
281 return;
283 int l;
284 if (text_width + 5 >= static_cast<unsigned>(real_width))
285 l = 0;
286 else
287 l = real_width - text_width - 5;
289 // use HorizontalLine colors
290 int attrs = getColorPair("horizontalline", "line");
291 area.attrOn(attrs);
293 int i;
294 for (i = 0; i < l; i++)
295 area.addLineChar(i, 0, CppConsUI::Curses::LINE_HLINE);
296 i += area.addString(i, 0, text);
297 for (; i < real_width; i++)
298 area.addLineChar(i, 0, CppConsUI::Curses::LINE_HLINE);
300 area.attrOff(attrs);
303 char *Conversation::stripHTML(const char *str) const
305 /* Almost copy&paste from libpurple/util.c:purple_markup_strip_html(), but
306 * this version doesn't convert tab character to a space. */
308 int i, j, k, entlen;
309 bool visible = true;
310 bool closing_td_p = false;
311 gchar *str2;
312 const gchar *cdata_close_tag = NULL, *ent;
313 gchar *href = NULL;
314 int href_st = 0;
316 if (!str)
317 return NULL;
319 str2 = g_strdup(str);
321 for (i = 0, j = 0; str2[i]; i++) {
322 if (str2[i] == '<') {
323 if (cdata_close_tag) {
324 // note: don't even assume any other tag is a tag in CDATA
325 if (g_ascii_strncasecmp(
326 str2 + i, cdata_close_tag, !strlen(cdata_close_tag))) {
327 i += strlen(cdata_close_tag) - 1;
328 cdata_close_tag = NULL;
330 continue;
332 else if (!g_ascii_strncasecmp(str2 + i, "<td", 3) && closing_td_p) {
333 str2[j++] = '\t';
334 visible = true;
336 else if (!g_ascii_strncasecmp(str2 + i, "</td>", 5)) {
337 closing_td_p = true;
338 visible = false;
340 else {
341 closing_td_p = false;
342 visible = true;
345 k = i + 1;
347 if (g_ascii_isspace(str2[k]))
348 visible = true;
349 else if (str2[k]) {
350 /* Scan until we end the tag either implicitly (closed start tag) or
351 * explicitly, using a sloppy method (i.e., < or > inside quoted
352 * attributes will screw us up). */
353 while (str2[k] && str2[k] != '<' && str2[k] != '>')
354 k++;
356 /* If we've got an <a> tag with an href, save the address to print
357 * later. */
358 if (!g_ascii_strncasecmp(str2 + i, "<a", 2) &&
359 g_ascii_isspace(str2[i + 2])) {
360 int st; // start of href, inclusive [
361 int end; // end of href, exclusive )
362 char delim = ' ';
363 // find start of href
364 for (st = i + 3; st < k; st++) {
365 if (!g_ascii_strncasecmp(str2 + st, "href=", 5)) {
366 st += 5;
367 if (str2[st] == '"' || str2[st] == '\'') {
368 delim = str2[st];
369 st++;
371 break;
374 // find end of address
375 for (end = st; end < k && str2[end] != delim; end++) {
376 // all the work is done in the loop construct above
379 /* If there's an address, save it. If there was already one saved,
380 * kill it. */
381 if (st < k) {
382 char *tmp;
383 g_free(href);
384 tmp = g_strndup(str2 + st, end - st);
385 href = purple_unescape_html(tmp);
386 g_free(tmp);
387 href_st = j;
391 /* Replace </a> with an ascii representation of the address the link
392 * was pointing to. */
393 else if (href && !g_ascii_strncasecmp(str2 + i, "</a>", 4)) {
394 size_t hrlen = strlen(href);
396 /* Only insert the href if it's different from the CDATA.
397 * 7 == strlen("http://") */
398 if ((hrlen != (unsigned)(j - href_st) ||
399 strncmp(str2 + href_st, href, hrlen)) &&
400 (hrlen != (unsigned)(j - href_st + 7) ||
401 strncmp(str2 + href_st, href + 7, hrlen - 7))) {
402 str2[j++] = ' ';
403 str2[j++] = '(';
404 g_memmove(str2 + j, href, hrlen);
405 j += hrlen;
406 str2[j++] = ')';
407 g_free(href);
408 href = NULL;
412 /* Check for tags which should be mapped to newline (but ignore some
413 * of the tags at the beginning of the text) */
414 else if ((j && (!g_ascii_strncasecmp(str2 + i, "<p>", 3) ||
415 !g_ascii_strncasecmp(str2 + i, "<tr", 3) ||
416 !g_ascii_strncasecmp(str2 + i, "<hr", 3) ||
417 !g_ascii_strncasecmp(str2 + i, "<li", 3) ||
418 !g_ascii_strncasecmp(str2 + i, "<div", 4))) ||
419 !g_ascii_strncasecmp(str2 + i, "<br", 3) ||
420 !g_ascii_strncasecmp(str2 + i, "</table>", 8))
421 str2[j++] = '\n';
422 // check for tags which begin CDATA and need to be closed
423 #if 0 // FIXME.. option is end tag optional, we can't handle this right now
424 else if (!g_ascii_strncasecmp(str2 + i, "<option", 7))
426 // FIXME we should not do this if the OPTION is SELECT'd
427 cdata_close_tag = "</option>";
429 #endif
430 else if (!g_ascii_strncasecmp(str2 + i, "<script", 7))
431 cdata_close_tag = "</script>";
432 else if (!g_ascii_strncasecmp(str2 + i, "<style", 6))
433 cdata_close_tag = "</style>";
434 // update the index and continue checking after the tag
435 i = (str2[k] == '<' || str2[k] == '\0') ? k - 1 : k;
436 continue;
439 else if (cdata_close_tag)
440 continue;
441 else if (!g_ascii_isspace(str2[i]))
442 visible = true;
444 if (str2[i] == '&' &&
445 (ent = purple_markup_unescape_entity(str2 + i, &entlen))) {
446 while (*ent)
447 str2[j++] = *ent++;
448 i += entlen - 1;
449 continue;
452 if (visible)
453 str2[j++] = g_ascii_isspace(str2[i]) && str[i] != '\t' ? ' ' : str2[i];
456 g_free(href);
458 str2[j] = '\0';
460 return str2;
463 void Conversation::buildLogFilename()
465 PurpleAccount *account;
466 PurplePlugin *prpl;
467 const char *proto_name;
468 char *acct_name;
469 char *dir;
470 const char *name;
472 account = purple_conversation_get_account(conv);
473 prpl = purple_find_prpl(purple_account_get_protocol_id(account));
474 g_assert(prpl);
476 proto_name = purple_account_get_protocol_name(account);
478 acct_name = g_strdup(purple_escape_filename(
479 purple_normalize(account, purple_account_get_username(account))));
481 name = purple_conversation_get_name(conv);
483 filename = g_build_filename(purple_user_dir(), "clogs", proto_name, acct_name,
484 purple_escape_filename(purple_normalize(account, name)), NULL);
486 dir = g_path_get_dirname(filename);
487 if (g_mkdir_with_parents(dir, S_IRUSR | S_IWUSR | S_IXUSR) == -1)
488 LOG->error(_("Error creating directory '%s'."), dir);
489 g_free(dir);
491 g_free(acct_name);
494 char *Conversation::extractTime(time_t sent_time, time_t show_time) const
496 // based on the extracttime() function from cim4
498 // convert to local time, note that localtime_r() shouldn't really fail
499 struct tm show_time_local;
500 struct tm sent_time_local;
501 if (!localtime_r(&show_time, &show_time_local))
502 memset(&show_time_local, 0, sizeof(show_time_local));
503 if (!localtime_r(&sent_time, &sent_time_local))
504 memset(&sent_time_local, 0, sizeof(sent_time_local));
506 // format the times
507 char *t1 = g_strdup(purple_date_format_long(&show_time_local));
508 char *t2 = g_strdup(purple_date_format_long(&sent_time_local));
510 int tdiff = abs(sent_time - show_time);
512 if (tdiff > 5 && strcmp(t1, t2)) {
513 char *res = g_strdup_printf("%s [%s]", t1, t2);
514 g_free(t1);
515 g_free(t2);
516 return res;
519 g_free(t2);
520 return t1;
523 void Conversation::loadHistory()
525 // open logfile
526 GError *err = NULL;
527 GIOChannel *chan;
529 if ((chan = g_io_channel_new_file(filename, "r", &err)) == NULL) {
530 LOG->error(_("Error opening conversation logfile '%s' (%s)."), filename,
531 err->message);
532 g_clear_error(&err);
533 return;
535 // this should never fail
536 g_io_channel_set_encoding(chan, NULL, NULL);
538 GIOStatus st;
539 char *line;
540 bool new_msg = false;
541 // read conversation logfile line by line
542 while (new_msg ||
543 (st = g_io_channel_read_line(chan, &line, NULL, NULL, &err)) ==
544 G_IO_STATUS_NORMAL) {
545 new_msg = false;
547 // start flag
548 if (strcmp(line, "\f\n")) {
549 g_free(line);
550 continue;
552 g_free(line);
554 // parse direction (in/out)
555 if ((st = g_io_channel_read_line(chan, &line, NULL, NULL, &err)) !=
556 G_IO_STATUS_NORMAL)
557 break;
558 int color = 0;
559 if (!strcmp(line, "OUT\n"))
560 color = 1;
561 else if (!strcmp(line, "IN\n"))
562 color = 2;
563 g_free(line);
565 // type
566 if ((st = g_io_channel_read_line(chan, &line, NULL, NULL, &err)) !=
567 G_IO_STATUS_NORMAL)
568 break;
569 bool cim4 = true;
570 if (!strcmp(line, "MSG2\n"))
571 cim4 = false;
572 else if (!strcmp(line, "OTHER\n")) {
573 cim4 = false;
574 color = 0;
576 g_free(line);
578 // sent time
579 if ((st = g_io_channel_read_line(chan, &line, NULL, NULL, &err)) !=
580 G_IO_STATUS_NORMAL)
581 break;
582 time_t sent_time = atol(line);
583 g_free(line);
585 // show time
586 if ((st = g_io_channel_read_line(chan, &line, NULL, NULL, &err)) !=
587 G_IO_STATUS_NORMAL)
588 break;
589 time_t show_time = atol(line);
590 g_free(line);
592 if (!cim4) {
593 // cim5, read only one line and strip it off HTML
594 if ((st = g_io_channel_read_line(chan, &line, NULL, NULL, &err)) !=
595 G_IO_STATUS_NORMAL)
596 break;
598 // validate UTF-8
599 if (!g_utf8_validate(line, -1, NULL)) {
600 g_free(line);
601 LOG->error(_("Invalid message detected in conversation logfile"
602 " '%s'. The message was skipped."),
603 filename);
604 continue;
607 // write text to the window
608 char *nohtml = stripHTML(line);
609 char *time = extractTime(sent_time, show_time);
610 char *msg = g_strdup_printf("%s %s", time, nohtml);
611 view->append(msg, color);
612 g_free(nohtml);
613 g_free(time);
614 g_free(msg);
615 g_free(line);
617 else {
618 // cim4, read multiple raw lines
619 gsize length;
620 std::string msg;
621 while ((st = g_io_channel_read_line(chan, &line, &length, NULL, &err)) ==
622 G_IO_STATUS_NORMAL &&
623 line != NULL) {
624 if (!strcmp(line, "\f\n")) {
625 new_msg = true;
626 break;
629 // strip '\r' if necessary
630 if (length > 1 && line[length - 2] == '\r') {
631 line[length - 2] = '\n';
632 line[length - 1] = '\0';
634 msg.append(line);
635 g_free(line);
638 if (!new_msg) {
639 // EOL or I/O error
640 break;
643 // validate UTF-8
644 if (!g_utf8_validate(msg.c_str(), -1, NULL)) {
645 LOG->error(_("Invalid message detected in conversation logfile"
646 " '%s'. The message was skipped."),
647 filename);
648 continue;
651 // add the message to the window
652 char *time = extractTime(sent_time, show_time);
653 char *final_msg = g_strdup_printf("%s %s", time, msg.c_str());
654 view->append(final_msg, color);
655 g_free(time);
656 g_free(final_msg);
660 if (st != G_IO_STATUS_EOF) {
661 LOG->error(_("Error reading from conversation logfile '%s' (%s)."),
662 filename, err->message);
663 g_clear_error(&err);
665 g_io_channel_unref(chan);
668 bool Conversation::processCommand(const char *raw, const char *html)
670 // check that it is a command
671 if (strncmp(raw, "/", 1))
672 return false;
674 purple_conversation_write(conv, "", html, PURPLE_MESSAGE_NO_LOG, time(NULL));
676 char *error = NULL;
677 // strip the prefix and execute the command
678 PurpleCmdStatus status =
679 purple_cmd_do_command(conv, raw + 1, html + 1, &error);
681 bool result = true;
682 switch (status) {
683 case PURPLE_CMD_STATUS_OK:
684 break;
685 case PURPLE_CMD_STATUS_NOT_FOUND:
686 // it isn't a valid command, send it as a message
687 result = false;
688 break;
689 case PURPLE_CMD_STATUS_WRONG_ARGS:
690 purple_conversation_write(conv, "",
691 _("Wrong number of arguments passed to the command."),
692 PURPLE_MESSAGE_NO_LOG, time(NULL));
693 break;
694 case PURPLE_CMD_STATUS_FAILED:
695 purple_conversation_write(conv, "",
696 error ? error : _("The command failed for an unknown reason."),
697 PURPLE_MESSAGE_NO_LOG, time(NULL));
698 break;
699 case PURPLE_CMD_STATUS_WRONG_TYPE:
700 if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
701 purple_conversation_write(conv, "",
702 _("The command only works in chats, not IMs."), PURPLE_MESSAGE_NO_LOG,
703 time(NULL));
704 else
705 purple_conversation_write(conv, "",
706 _("The command only works in IMs, not chats."), PURPLE_MESSAGE_NO_LOG,
707 time(NULL));
708 break;
709 case PURPLE_CMD_STATUS_WRONG_PRPL:
710 purple_conversation_write(conv, "",
711 _("The command does not work on this protocol."), PURPLE_MESSAGE_NO_LOG,
712 time(NULL));
713 break;
716 g_free(error);
718 return result;
721 void Conversation::onInputTextChange(CppConsUI::TextEdit &activator)
723 PurpleConvIm *im = PURPLE_CONV_IM(conv);
724 if (!im)
725 return;
727 if (!CONVERSATIONS->getSendTypingPref()) {
728 input_text_length = 0;
729 return;
732 size_t old_text_length = input_text_length;
733 size_t new_text_length = activator.getTextLength();
734 input_text_length = new_text_length;
736 if (!new_text_length) {
737 // all text is deleted, turn off typing
738 purple_conv_im_stop_send_typed_timeout(im);
740 serv_send_typing(purple_conversation_get_gc(conv),
741 purple_conversation_get_name(conv), PURPLE_NOT_TYPING);
742 return;
745 purple_conv_im_stop_send_typed_timeout(im);
746 purple_conv_im_start_send_typed_timeout(im);
748 time_t again = purple_conv_im_get_type_again(im);
749 if ((!old_text_length && new_text_length) || (again && time(NULL) > again)) {
750 // the first letter is inserted or update is required for typing status
751 unsigned int timeout = serv_send_typing(purple_conversation_get_gc(conv),
752 purple_conversation_get_name(conv), PURPLE_TYPING);
753 purple_conv_im_set_type_again(im, timeout);
757 void Conversation::actionSend()
759 const char *str = input->getText();
760 if (!str || !str[0])
761 return;
763 purple_idle_touch();
765 char *escaped = purple_markup_escape_text(str, strlen(str));
766 char *html = purple_strdup_withhtml(escaped);
767 if (processCommand(str, html)) {
768 // the command was processed
770 else {
771 PurpleConversationType type = purple_conversation_get_type(conv);
772 if (type == PURPLE_CONV_TYPE_CHAT)
773 purple_conv_chat_send(PURPLE_CONV_CHAT(conv), html);
774 else if (type == PURPLE_CONV_TYPE_IM)
775 purple_conv_im_send(PURPLE_CONV_IM(conv), html);
777 g_free(html);
778 g_free(escaped);
779 input->clear();
782 void Conversation::declareBindables()
784 declareBindable("conversation", "send",
785 sigc::mem_fun(this, &Conversation::actionSend),
786 InputProcessor::BINDABLE_OVERRIDE);
789 /* vim: set tabstop=2 shiftwidth=2 textwidth=80 expandtab : */