Renga/ui/TalkView.cpp

824 lines
22 KiB
C++

//////////////////////////////////////////////////
// Blabber [TalkView.cpp]
//////////////////////////////////////////////////
#include <cstdio>
#include <ctime>
#include <malloc.h>
#include <stdlib.h>
#include <Box.h>
#include <be_apps/NetPositive/NetPositive.h>
#include <FindDirectory.h>
#include <GridView.h>
#include <GroupLayout.h>
#include <GroupView.h>
#include <LayoutBuilder.h>
#include <Roster.h>
#include <storage/Path.h>
#include <SplitView.h>
#include "support/AppLocation.h"
#include "jabber/BlabberSettings.h"
#include "jabber/GenericFunctions.h"
#include "jabber/JabberSpeak.h"
#include "jabber/MessageRepeater.h"
#include "jabber/Messages.h"
#include "jabber/PreferencesWindow.h"
#include "jabber/SoundSystem.h"
#include "jabber/TalkListItem.h"
#include "jabber/TalkManager.h"
#include "ui/PeopleListItem.h"
#include "ui/RotateChatFilter.h"
#include "TalkView.h"
#include "gloox/mucroom.h"
#include "gloox/rostermanager.h"
#define NOTIFICATION_CHAR "√"
TalkView::TalkView(const gloox::JID *user, string group_room,
string group_username, gloox::MessageSession* session)
: BGroupView("<talk window>", B_VERTICAL)
, _session(session)
{
_chat_index = -1;
UserID* uid = NULL;
if (user) {
uid = JRoster::Instance()->FindUser(*user);
}
_group_room = group_room;
_group_username = group_username;
if (!IsGroupChat() && uid) {
_current_status = uid->OnlineStatus();
}
// FILE MENU
// status bar
_status_view = new StatusView();
_status_view->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
fTimeline = new ChatTextView("Timeline", B_WILL_DRAW | B_FRAME_EVENTS);
fTimelineScroller = new BScrollView("Timeline Scroller", fTimeline, B_WILL_DRAW, false, true);
fTimeline->TargetedByScrollView(fTimelineScroller);
fTimeline->SetFontSize(12.0);
fTimeline->SetWordWrap(true);
fTimeline->SetStylable(true);
fTimeline->MakeEditable(false);
// message control
rgb_color text_color = ui_color(B_PANEL_TEXT_COLOR);
BFont text_font(be_plain_font);
fMessageInput = new BTextView("Message Input", &text_font, &text_color, B_WILL_DRAW);
fMessageInputScroller = new BScrollView("Message Input Scroller", fMessageInput, B_WILL_DRAW, false, false);
fMessageInput->TargetedByScrollView(fMessageInputScroller);
fMessageInput->SetWordWrap(true);
// editing filter for messaging
fMessageInput->AddFilter(new EditingFilter(fMessageInput, this));
// handle splits
BSplitView* _split_talk = new BSplitView(B_VERTICAL);
_split_talk->AddChild(fTimelineScroller);
_split_talk->AddChild(fMessageInputScroller);
_split_talk->SetItemWeight(0, 12, false);
_split_talk->SetItemWeight(1, 1, false);
_split_talk->SetSpacing(0);
_split_talk->SetCollapsible(false);
_people = new BListView(NULL, B_SINGLE_SELECTION_LIST);
_people->SetExplicitMinSize(BSize(StringWidth("Firstname M. Lastname"), B_SIZE_UNSET));
_scrolled_people_pane = new BScrollView(NULL, _people, 0, false, true, B_PLAIN_BORDER);
BSplitView* _split_group_people = new BSplitView(B_HORIZONTAL);
_split_group_people->AddChild(_split_talk);
_split_group_people->AddChild(_scrolled_people_pane);
_split_group_people->SetItemWeight(0, 5, false);
_split_group_people->SetItemWeight(1, 1, false);
_split_group_people->SetSpacing(0);
if (!IsGroupChat()) {
_split_group_people->SetItemCollapsed(1, true);
_split_group_people->SetSplitterSize(0);
}
// add GUI components to BView
BGroupLayout* layout = new BGroupLayout(B_VERTICAL);
SetLayout(layout);
layout->SetSpacing(0);
AddChild(_split_group_people);
AddChild(_status_view);
fMessageInput->MakeFocus(true);
// generate window title
char buffer[1024];
string user_representation;
if (!IsGroupChat()) {
// identify the user
sprintf(buffer, "your identity is %s", _group_username.c_str());
_status_view->SetMessage(buffer);
} else if (!uid || uid->UserType() == UserID::JABBER) {
// identify the user
sprintf(buffer, "your identity is %s", JabberSpeak::Instance()->CurrentLogin().c_str());
_status_view->SetMessage(buffer);
} else {
user_representation = uid->FriendlyName();
if (user_representation.empty())
user_representation = uid->JabberUsername();
}
if (!IsGroupChat() && user_representation.empty()) {
if (uid)
user_representation = uid->FriendlyName();
else
user_representation = _session->target().bare();
}
// put Session started message
// construct timestamp
string message;
message.resize(128);
time_t now = time(NULL);
struct tm *time_struct = localtime(&now);
strftime(&message[0], message.size()-1, "Session started %e %b %y [%R:%S]", time_struct);
AddToTalk("", message.c_str(), OTHER);
TalkManager::Instance()->StartWatchingAll(this);
}
TalkView::~TalkView() {
string message;
message.resize(128);
time_t now = time(NULL);
struct tm *time_struct = localtime(&now);
strftime(&message[0], message.size()-1, "Session finished %e %b %y [%R:%S]\n---", time_struct);
AddToTalk("", message.c_str(), OTHER);
if (IsGroupChat())
JabberSpeak::Instance()->SendGroupUnvitation(_group_room, _group_username);
TalkManager::Instance()->RemoveWindow(this);
}
void TalkView::AttachedToWindow()
{
}
void TalkView::FrameResized(float width, float height)
{
BView::FrameResized(width, height);
BRect chat_rect = fTimeline->Frame();
BRect message_rect = fMessageInput->Frame();
chat_rect.OffsetTo(B_ORIGIN);
message_rect.OffsetTo(B_ORIGIN);
chat_rect.InsetBy(2.0, 2.0);
message_rect.InsetBy(2.0, 2.0);
fTimeline->SetTextRect(chat_rect);
fMessageInput->SetTextRect(message_rect);
fTimeline->Invalidate();
fTimelineScroller->Invalidate();
}
void TalkView::MessageReceived(BMessage *msg) {
switch(msg->what) {
case JAB_CLOSE_TALKS:
{
RemoveSelf();
delete this;
break;
}
case B_OBSERVER_NOTICE_CHANGE:
{
// only for groupchat
if (!IsGroupChat())
break;
switch(msg->FindInt32("be:observe_orig_what"))
{
case JAB_GROUP_CHATTER_ONLINE:
{
if (GetGroupRoom() == msg->FindString("room")) {
AddGroupChatter(msg->FindString("username"),
(gloox::MUCRoomAffiliation)msg->FindInt32("affiliation"));
}
break;
}
case JAB_GROUP_CHATTER_OFFLINE: {
RemoveGroupChatter(msg->FindString("username"));
break;
}
break;
}
break;
}
case JAB_SHOW_CHATLOG: {
// just forward to main blabber window...
BMessage *msgForward = new BMessage(*msg);
BlabberMainWindow::Instance()->PostMessage(msgForward);
break;
}
case BLAB_UPDATE_ROSTER: {
// doesn't apply to groupchat
if (!IsGroupChat())
break;
// get new status
JRoster::Instance()->Lock();
UserID* user = JRoster::Instance()->FindUser(_session->target());
if (!user) {
JRoster::Instance()->Unlock();
break;
}
UserID::online_status new_status = user->OnlineStatus();
// if we have one, check their presence
if (_current_status != new_status) {
char buffer[2048];
if (_current_status == UserID::ONLINE && new_status == UserID::OFFLINE) {
sprintf(buffer, "This user is now offline.");
AddToTalk("", buffer, OTHER);
} else if (_current_status == UserID::OFFLINE && new_status == UserID::ONLINE) {
sprintf(buffer, "This user is now online.");
AddToTalk("", buffer, OTHER);
}
}
_current_status = new_status;
JRoster::Instance()->Unlock();
break;
}
case JAB_CHAT_SENT: {
string message = fMessageInput->Text();
// eliminate empty messages
if (message.empty())
break;
if (_session == NULL) {
// FIXME is it ok to do this from window thread? Or should
// we go through main app?
gloox::MUCRoom* room = (gloox::MUCRoom*)TalkManager::Instance()
->IsExistingWindowToGroup(GetGroupRoom());
room->send(message);
} else {
_session->send(message);
AddToTalk(OurRepresentation(), message, LOCAL);
}
// Reset message input box
fMessageInput->ScrollToOffset(0);
fMessageInput->SetText("");
fMessageInput->MakeFocus(true);
break;
}
case JAB_CLOSE_CHAT: {
RemoveSelf();
delete this;
break;
}
case JAB_FOCUS_BUDDY: {
BlabberMainWindow::Instance()->Activate();
break;
}
case kIncomingMessage:
{
gloox::RosterManager* rm = JabberSpeak::Instance()->GlooxClient()->rosterManager();
gloox::RosterItem* item = rm->getRosterItem(_session->target());
if (item && !item->name().empty()) {
AddToTalk(item->name().c_str(), msg->FindString("content"), MAIN_RECIPIENT);
} else {
AddToTalk(_session->target().bare().c_str(), msg->FindString("content"), MAIN_RECIPIENT);
}
}
}
}
string TalkView::OurRepresentation() {
// use friendly name if you have it
string user = JabberSpeak::Instance()->CurrentRealName();
// and if not :)
if (user.empty())
user = JabberSpeak::Instance()->CurrentLogin();
return user;
}
void TalkView::AddToTalk(string username, string message, user_type type, bool highlight) {
// transform local identity
if (IsGroupChat() && type == LOCAL)
username = _group_username;
// history
if (type == LOCAL) {
// reset chat history index
_chat_index = -1;
// add latest
_chat_history.push_front(message);
// prune end
if (_chat_history.size() > 50)
_chat_history.pop_back();
}
// ignore empty messages
if (message.empty())
return;
// Figure out if the view is scrolled down before modifying it
BRect bounds = fTimeline->Bounds();
BRect textRect = fTimeline->TextRect();
bool scrolledDown = bounds.bottom == -1 ||
(bounds.bottom > textRect.bottom && bounds.top < textRect.bottom);
// prune trailing whitespace
while (!message.empty() && isspace(message[message.size() - 1]))
message.erase(message.size() - 1);
// create the thin (plain) and thick (bold) font
BFont thin(be_plain_font);
BFont thick(be_bold_font);
// some colors to play with
rgb_color blue = {0, 0, 255, 255};
rgb_color red = {255, 0, 0, 255};
rgb_color orange = {205, 113, 57, 255};
rgb_color messageColor = ui_color(B_DOCUMENT_TEXT_COLOR);
rgb_color backgroundColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR);
rgb_color highlightColor = mix_color(messageColor, orange, 200);
// TODO figure out a goood threshold here, this seems to work for me,
// but it may not for others
if (abs(blue.Brightness() - backgroundColor.Brightness()) < 45)
blue = { 128, 137, 252, 255 };
if (abs(red.Brightness() - backgroundColor.Brightness()) < 45)
red = { 249, 84, 87, 255 };
// some runs to play with
text_run tr_thick_blue = {0, thick, blue};
text_run tr_thick_red = {0, thick, red};
text_run tr_thick_black = {0, thick, messageColor};
text_run tr_thick_highlight = {0, thick, highlightColor};
text_run tr_thin_black = {0, thin, messageColor};
// some run array to play with (simple)
text_run_array tra_thick_blue = {1, {tr_thick_blue}};
text_run_array tra_thick_red = {1, {tr_thick_red}};
text_run_array tra_thin_black = {1, {tr_thin_black}};
// construct timestamp
char timestamp[64];
time_t now = time(NULL);
struct tm *time_struct = localtime(&now);
strftime(timestamp, 63, "[%R:%S] ", time_struct);
string time_stamp = timestamp;
BString messageString = BString(message.c_str());
if (BlabberSettings::Instance()->Tag("show-timestamp"))
fTimeline->Insert(fTimeline->TextLength(), time_stamp.c_str(), time_stamp.size(), &tra_thin_black);
if (messageString.StartsWith("/me ")) {
messageString.ReplaceFirst("/me", username.c_str());
if (type == MAIN_RECIPIENT)
fTimeline->Insert(fTimeline->TextLength(), messageString, messageString.Length(), &tra_thick_blue);
else
fTimeline->Insert(fTimeline->TextLength(), messageString, messageString.Length(), &tra_thick_red);
fTimeline->Insert(fTimeline->TextLength(), "\n", 1, &tra_thin_black);
if (scrolledDown)
fTimeline->ScrollTo(0.0, fTimeline->Bounds().bottom);
return;
}
text_run_array *this_array;
if (type == MAIN_RECIPIENT) {
if (!IsGroupChat() || !BlabberSettings::Instance()->Tag("exclude-groupchat-sounds"))
SoundSystem::Instance()->PlayMessageSound();
fTimeline->Insert(fTimeline->TextLength(), username.c_str(), username.size(), &tra_thick_blue);
fTimeline->Insert(fTimeline->TextLength(), ": ", 2, &tra_thin_black);
// Highlight messages when they mention the nickname
if (highlight) {
GenerateHyperlinkText(message, tr_thick_highlight, &this_array);
} else {
GenerateHyperlinkText(message, tr_thin_black, &this_array);
}
} else if (type == LOCAL) {
fTimeline->Insert(fTimeline->TextLength(), username.c_str(), username.size(), &tra_thick_red);
fTimeline->Insert(fTimeline->TextLength(), ": ", 2, &tra_thin_black);
GenerateHyperlinkText(message, tr_thin_black, &this_array);
} else { // SYSTEM messages
GenerateHyperlinkText(message, tr_thick_black, &this_array);
}
fTimeline->Insert(fTimeline->TextLength(), message.c_str(), message.size(), this_array);
free(this_array);
fTimeline->Insert(fTimeline->TextLength(), "\n", 1, &tra_thin_black);
if (scrolledDown)
fTimeline->ScrollTo(0.0, fTimeline->Bounds().bottom);
}
const gloox::JID& TalkView::GetUserID() {
if (_session == NULL)
debugger("Getting user ID not possible for group chat");
return _session->target();
}
string TalkView::GetGroupRoom() {
return _group_room;
}
string TalkView::GetGroupUsername() {
return _group_username;
}
bool TalkView::NewlinesAllowed() {
return false;
}
int TalkView::CountHyperlinks(string message) {
string::size_type curr_pos = 0, link_start, link_end;
string::size_type find1, find2, find3;
// keep count
int link_count = 0;
// find next link
link_start = message.find("http://", curr_pos);
find1 = message.find("ftp://", curr_pos);
if (find1 != string::npos && (link_start == string::npos || find1 < link_start)) {
link_start = find1;
}
find2 = message.find("www.", curr_pos);
if (find2 != string::npos && (link_start == string::npos || find2 < link_start)) {
// ignore if it's not at the beginning or has no whitespace
if (find2 > 0 && isalnum(message[find2 - 1])) {
// do nothing
} else if (isspace(message[find2 + 4]) || message[find2 + 4] == '.') {
// do nothing
} else {
link_start = find2;
}
}
find3 = message.find("ftp.", curr_pos);
if (find3 != string::npos && (link_start == string::npos || find3 < link_start)) {
// ignore if it's not at the beginning or has no whitespace
if (find3 > 0 && isalnum(message[find3 - 1])) {
// do nothing
} else if (isspace(message[find3 + 4]) || message[find3 + 4] == '.') {
// do nothing
} else {
link_start = find3;
}
}
while (link_start != string::npos) {
// find whitespace or end
link_end = message.find_first_of(" \t\r\n", link_start);
if (link_end == string::npos)
link_end = message.size() - 1;
// prune punctuation
while (link_start < link_end) {
if (message[link_end] == ',' || message[link_end] == '!' || message[link_end] == '.' || message[link_end] == ')' || message[link_end] == ';' || message[link_end] == ']' || message[link_end] == '>' || message[link_end] == '\'' || message[link_end] == '"') {
--link_end;
} else {
break;
}
}
if (link_start < link_end) {
++link_count;
}
curr_pos = link_end + 1;
// find next link
link_start = message.find("http://", curr_pos);
find1 = message.find("ftp://", curr_pos);
if (find1 != string::npos && (link_start == string::npos || find1 < link_start))
link_start = find1;
find2 = message.find("www.", curr_pos);
if (find2 != string::npos && (link_start == string::npos || find2 < link_start)) {
// ignore if it's not at the beginning or has no whitespace
if (find2 > 0 && isalnum(message[find2 - 1])) {
// do nothing
} else if (isspace(message[find2 + 4]) || message[find2 + 4] == '.') {
// do nothing
} else {
link_start = find2;
}
}
find3 = message.find("ftp.", curr_pos);
if (find3 != string::npos && (link_start == string::npos || find3 < link_start)) {
// ignore if it's not at the beginning or has no whitespace
if (find3 > 0 && isalnum(message[find3 - 1])) {
// do nothing
} else if (isspace(message[find3 + 4]) || message[find3 + 4] == '.') {
// do nothing
} else {
link_start = find3;
}
}
}
return link_count;
}
void TalkView::GenerateHyperlinkText(string message, text_run standard, text_run_array **tra) {
int link_count = CountHyperlinks(message);
string::size_type find1, find2, find3;
int link_index = 0;
// no links?
if (link_count == 0) {
*tra = (text_run_array *)malloc(sizeof(text_run_array));
(*tra)->count = 1;
(*tra)->runs[0].offset = standard.offset;
(*tra)->runs[0].font = standard.font;
(*tra)->runs[0].color = standard.color;
return;
}
*tra = (text_run_array *)malloc(sizeof(text_run_array) + (sizeof(text_run) * (link_count * 2 - 1)));
(*tra)->count = link_count * 2;
string::size_type curr_pos = 0, link_start, link_end;
// find next link
link_start = message.find("http://", curr_pos);
find1 = message.find("ftp://", curr_pos);
if (find1 != string::npos && (link_start == string::npos || find1 < link_start))
link_start = find1;
find2 = message.find("www.", curr_pos);
if (find2 != string::npos && (link_start == string::npos || find2 < link_start)) {
// ignore if it's not at the beginning or has no whitespace
if (find2 > 0 && isalnum(message[find2 - 1])) {
// do nothing
} else if (isspace(message[find2 + 4]) || message[find2 + 4] == '.') {
// do nothing
} else {
link_start = find2;
}
}
find3 = message.find("ftp.", curr_pos);
if (find3 != string::npos && (link_start == string::npos || find3 < link_start)) {
// ignore if it's not at the beginning or has no whitespace
if (find3 > 0 && isalnum(message[find3 - 1])) {
// do nothing
} else if (isspace(message[find3 + 4]) || message[find3 + 4] == '.') {
// do nothing
} else {
link_start = find3;
}
}
while (link_start != string::npos) {
// find whitespace or end
link_end = message.find_first_of(" \t\r\n", link_start);
if (link_end == string::npos) {
link_end = message.size() - 1;
}
// prune punctuation
while (link_start < link_end) {
if (message[link_end] == ',' || message[link_end] == '!' || message[link_end] == '.' || message[link_end] == ')' || message[link_end] == ';' || message[link_end] == ']' || message[link_end] == '>' || message[link_end] == '?' || message[link_end] == '\'' || message[link_end] == '"') {
--link_end;
} else {
break;
}
}
// add hyperlink
if (link_start < link_end) {
BFont thin(be_plain_font);
rgb_color purple = {192, 0, 192, 255};
(*tra)->runs[link_index].offset = link_start;
(*tra)->runs[link_index].font = thin;
(*tra)->runs[link_index].color = purple;
(*tra)->runs[link_index + 1].offset = link_end + 1;
(*tra)->runs[link_index + 1].font = standard.font;
(*tra)->runs[link_index + 1].color = standard.color;
}
curr_pos = link_end + 1;
if (curr_pos >= message.size()) {
break;
}
// find next link
link_start = message.find("http://", curr_pos);
find1 = message.find("ftp://", curr_pos);
if (find1 != string::npos && (link_start == string::npos || find1 < link_start)) {
link_start = find1;
}
find2 = message.find("www.", curr_pos);
if (find2 != string::npos && (link_start == string::npos || find2 < link_start)) {
// ignore if it's not at the beginning or has no whitespace
if (find2 > 0 && isalnum(message[find2 - 1])) {
// do nothing
} else if (isspace(message[find2 + 4]) || message[find2 + 4] == '.') {
// do nothing
} else {
link_start = find2;
}
}
find3 = message.find("ftp.", curr_pos);
if (find3 != string::npos && (link_start == string::npos || find3 < link_start)) {
// ignore if it's not at the beginning or has no whitespace
if (find3 > 0 && isalnum(message[find3 - 1])) {
// do nothing
} else if (isspace(message[find3 + 4]) || message[find3 + 4] == '.') {
// do nothing
} else {
link_start = find3;
}
}
link_index += 2;
}
}
static int compareStrings(const char* a, const char* b)
{
// FIXME use ICU locale aware comparison instead
int icompare = strcasecmp(a, b);
if (icompare != 0)
return icompare;
// In case the names are case-insensitive-equal, still sort them in a
// predictible way
return strcmp(a, b);
}
void TalkView::AddGroupChatter(string user, gloox::MUCRoomAffiliation affiliation) {
int i;
// create a new entry
PeopleListItem *people_item = new PeopleListItem(user, affiliation);
// exception
if (_people->CountItems() == 0) {
// add the new user
_people->AddItem(people_item);
return;
}
// add it to the list
// FIXME we should binary search for the correct position
for (i=0; i < _people->CountItems(); ++i) {
PeopleListItem *iterating_item = dynamic_cast<PeopleListItem *>(_people->ItemAt(i));
int compare = compareStrings(iterating_item->User().c_str(), user.c_str());
if (compare == 0) {
// Update existing user
// FIXME affiliation might have changed, refresh it
_people->InvalidateItem(i);
} else if (compare > 0) {
// add the new user in the middle
_people->AddItem(people_item, i);
} else if (i == (_people->CountItems() - 1)) {
// add the new user at the end
_people->AddItem(people_item);
} else {
// continue searching for the correct place
continue;
}
break;
}
}
void TalkView::RemoveGroupChatter(string username) {
// remove user
for (int i=0; i < _people->CountItems(); ++i) {
if (dynamic_cast<PeopleListItem *>(_people->ItemAt(i))->User() == username)
_people->RemoveItem(i);
}
}
void TalkView::RevealPreviousHistory() {
// boundary
if (_chat_index == 49 || _chat_index == ((int)_chat_history.size() - 1))
return;
if (_chat_index == -1)
_chat_buffer = fMessageInput->Text();
// go back
++_chat_index;
// update text
fMessageInput->SetText(_chat_history[_chat_index].c_str());
}
void TalkView::RevealNextHistory() {
// boundary
if (_chat_index == -1)
return;
// go forward
--_chat_index;
// last buffer
if (_chat_index == -1) {
fMessageInput->SetText(_chat_buffer.c_str());
} else {
// update text
fMessageInput->SetText(_chat_history[_chat_index].c_str());
}
}
bool
TalkView::IsGroupChat()
{
return !_group_room.empty();
}
void
TalkView::SetStatus(std::string message)
{
_status_view->SetMessage(message);
}