1
0
forked from blue/squawk
squawk/ui/squawk.cpp

597 lines
24 KiB
C++
Raw Normal View History

/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
2019-03-29 14:54:34 +00:00
#include "squawk.h"
#include "ui_squawk.h"
2019-04-02 15:46:18 +00:00
#include <QDebug>
#include <QIcon>
2019-03-29 14:54:34 +00:00
Squawk::Squawk(Models::Roster& p_rosterModel, QWidget *parent) :
2019-03-29 14:54:34 +00:00
QMainWindow(parent),
m_ui(new Ui::Squawk),
2022-04-03 20:53:46 +00:00
accounts(nullptr),
preferences(nullptr),
about(nullptr),
rosterModel(p_rosterModel),
contextMenu(new QMenu()),
infoWidgets(),
2022-04-03 20:53:46 +00:00
currentConversation(nullptr),
restoreSelection(),
needToRestore(false)
2019-03-29 14:54:34 +00:00
{
m_ui->setupUi(this);
m_ui->roster->setModel(&rosterModel);
m_ui->roster->setContextMenuPolicy(Qt::CustomContextMenu);
if (QApplication::style()->styleHint(QStyle::SH_ScrollBar_Transient) == 1) {
m_ui->roster->setColumnWidth(1, 52);
} else {
m_ui->roster->setColumnWidth(1, 26);
}
m_ui->roster->setIconSize(QSize(20, 20));
m_ui->roster->header()->setStretchLastSection(false);
m_ui->roster->header()->setSectionResizeMode(0, QHeaderView::Stretch);
2019-03-29 14:54:34 +00:00
for (int i = static_cast<int>(Shared::AvailabilityLowest); i < static_cast<int>(Shared::AvailabilityHighest) + 1; ++i) {
2019-07-01 13:53:01 +00:00
Shared::Availability av = static_cast<Shared::Availability>(i);
m_ui->comboBox->addItem(Shared::availabilityIcon(av), Shared::Global::getName(av));
}
m_ui->comboBox->setCurrentIndex(static_cast<int>(Shared::Availability::offline));
connect(m_ui->actionAccounts, &QAction::triggered, this, &Squawk::onAccounts);
2022-01-16 19:54:57 +00:00
connect(m_ui->actionPreferences, &QAction::triggered, this, &Squawk::onPreferences);
connect(m_ui->actionAddContact, &QAction::triggered, this, &Squawk::onNewContact);
connect(m_ui->actionAddConference, &QAction::triggered, this, &Squawk::onNewConference);
connect(m_ui->actionQuit, &QAction::triggered, this, &Squawk::quit);
connect(m_ui->comboBox, qOverload<int>(&QComboBox::activated), this, &Squawk::onComboboxActivated);
//connect(m_ui->roster, &QTreeView::doubleClicked, this, &Squawk::onRosterItemDoubleClicked);
connect(m_ui->roster, &QTreeView::customContextMenuRequested, this, &Squawk::onRosterContextMenu);
connect(m_ui->roster, &QTreeView::collapsed, this, &Squawk::itemCollapsed);
connect(m_ui->roster, &QTreeView::expanded, this, &Squawk::itemExpanded);
2020-04-11 20:00:15 +00:00
connect(m_ui->roster->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &Squawk::onRosterSelectionChanged);
2022-06-03 06:44:48 +00:00
connect(rosterModel.accountsModel, &Models::Accounts::changed, this, &Squawk::onAccountsChanged);
connect(contextMenu, &QMenu::aboutToHide, this, &Squawk::onContextAboutToHide);
2022-04-03 20:53:46 +00:00
connect(m_ui->actionAboutSquawk, &QAction::triggered, this, &Squawk::onAboutSquawkCalled);
//m_ui->mainToolBar->addWidget(m_ui->comboBox);
if (testAttribute(Qt::WA_TranslucentBackground)) {
m_ui->roster->viewport()->setAutoFillBackground(false);
}
QSettings settings;
settings.beginGroup("ui");
settings.beginGroup("window");
if (settings.contains("geometry")) {
restoreGeometry(settings.value("geometry").toByteArray());
}
if (settings.contains("state")) {
restoreState(settings.value("state").toByteArray());
}
settings.endGroup();
2020-04-11 20:00:15 +00:00
if (settings.contains("splitter")) {
m_ui->splitter->restoreState(settings.value("splitter").toByteArray());
}
settings.endGroup();
2022-06-03 06:44:48 +00:00
onAccountsChanged();
2019-03-29 14:54:34 +00:00
}
Squawk::~Squawk() {
delete contextMenu;
2019-03-29 14:54:34 +00:00
}
void Squawk::onAccounts() {
2022-04-03 20:53:46 +00:00
if (accounts == nullptr) {
accounts = new Accounts(rosterModel.accountsModel);
2019-03-29 14:54:34 +00:00
accounts->setAttribute(Qt::WA_DeleteOnClose);
connect(accounts, &Accounts::destroyed, this, &Squawk::onAccountsClosed);
connect(accounts, &Accounts::newAccount, this, &Squawk::newAccountRequest);
connect(accounts, &Accounts::changeAccount, this, &Squawk::modifyAccountRequest);
connect(accounts, &Accounts::connectAccount, this, &Squawk::connectAccount);
connect(accounts, &Accounts::disconnectAccount, this, &Squawk::disconnectAccount);
connect(accounts, &Accounts::removeAccount, this, &Squawk::removeAccountRequest);
2019-03-30 20:13:13 +00:00
2019-03-29 14:54:34 +00:00
accounts->show();
} else {
2019-03-30 20:13:13 +00:00
accounts->show();
accounts->raise();
accounts->activateWindow();
2019-03-29 14:54:34 +00:00
}
}
void Squawk::onPreferences() {
2022-04-03 20:53:46 +00:00
if (preferences == nullptr) {
2022-01-16 19:54:57 +00:00
preferences = new Settings();
preferences->setAttribute(Qt::WA_DeleteOnClose);
connect(preferences, &Settings::destroyed, this, &Squawk::onPreferencesClosed);
2022-02-19 18:31:49 +00:00
connect(preferences, &Settings::changeDownloadsPath, this, &Squawk::changeDownloadsPath);
connect(preferences, &Settings::changeTray, this, &Squawk::changeTray);
2022-01-16 19:54:57 +00:00
preferences->show();
} else {
preferences->show();
preferences->raise();
preferences->activateWindow();
}
}
void Squawk::onAccountsChanged() {
2022-06-03 06:44:48 +00:00
unsigned int size = rosterModel.accountsModel->activeSize();
if (size > 0) {
m_ui->actionAddContact->setEnabled(true);
2019-09-04 16:38:52 +00:00
m_ui->actionAddConference->setEnabled(true);
} else {
m_ui->actionAddContact->setEnabled(false);
2019-09-04 16:38:52 +00:00
m_ui->actionAddConference->setEnabled(false);
}
}
void Squawk::onNewContact() {
NewContact* nc = new NewContact(rosterModel.accountsModel, this);
connect(nc, &NewContact::accepted, this, &Squawk::onNewContactAccepted);
connect(nc, &NewContact::rejected, nc, &NewContact::deleteLater);
nc->exec();
}
void Squawk::onNewConference() {
2019-09-04 16:38:52 +00:00
JoinConference* jc = new JoinConference(rosterModel.accountsModel, this);
connect(jc, &JoinConference::accepted, this, &Squawk::onJoinConferenceAccepted);
connect(jc, &JoinConference::rejected, jc, &JoinConference::deleteLater);
2019-09-04 16:38:52 +00:00
jc->exec();
}
void Squawk::onNewContactAccepted() {
NewContact* nc = static_cast<NewContact*>(sender());
NewContact::Data value = nc->value();
emit addContactRequest(value.account, value.jid, value.name, value.groups);
nc->deleteLater();
}
void Squawk::onJoinConferenceAccepted() {
2019-09-04 16:38:52 +00:00
JoinConference* jc = static_cast<JoinConference*>(sender());
JoinConference::Data value = jc->value();
emit addRoomRequest(value.account, value.jid, value.nick, value.password, value.autoJoin);
jc->deleteLater();
}
void Squawk::closeEvent(QCloseEvent* event) {
if (accounts != nullptr)
2019-03-29 14:54:34 +00:00
accounts->close();
if (preferences != nullptr)
2022-01-16 19:54:57 +00:00
preferences->close();
if (about != nullptr)
2022-04-03 20:53:46 +00:00
about->close();
for (const std::pair<const QString, UI::Info*>& pair : infoWidgets) {
disconnect(pair.second, &UI::Info::destroyed, this, &Squawk::onInfoClosed);
pair.second->close();
}
infoWidgets.clear();
writeSettings();
emit closing();;
2019-03-29 14:54:34 +00:00
QMainWindow::closeEvent(event);
}
void Squawk::onAccountsClosed() {
accounts = nullptr;}
2019-03-29 14:54:34 +00:00
void Squawk::onPreferencesClosed() {
preferences = nullptr;}
2019-03-30 20:13:13 +00:00
void Squawk::onAboutSquawkClosed() {
about = nullptr;}
void Squawk::onComboboxActivated(int index) {
Shared::Availability av = Shared::Global::fromInt<Shared::Availability>(index);
emit changeState(av);
}
void Squawk::expand(const QModelIndex& index) {
m_ui->roster->expand(index);}
2019-04-07 14:02:41 +00:00
void Squawk::stateChanged(Shared::Availability state) {
m_ui->comboBox->setCurrentIndex(static_cast<int>(state));}
2019-04-03 21:23:51 +00:00
void Squawk::onRosterItemDoubleClicked(const QModelIndex& item) {
2019-04-09 15:04:08 +00:00
if (item.isValid()) {
Models::Item* node = static_cast<Models::Item*>(item.internalPointer());
2020-04-17 23:17:47 +00:00
if (node->type == Models::Item::reference) {
node = static_cast<Models::Reference*>(node)->dereference();
}
2022-04-03 20:53:46 +00:00
Models::Contact* contact = nullptr;
Models::Room* room = nullptr;
2019-04-09 15:04:08 +00:00
switch (node->type) {
case Models::Item::contact:
contact = static_cast<Models::Contact*>(node);
emit openConversation(Models::Roster::ElId(contact->getAccountName(), contact->getJid()));
2019-04-09 15:04:08 +00:00
break;
case Models::Item::presence:
contact = static_cast<Models::Contact*>(node->parentItem());
emit openConversation(Models::Roster::ElId(contact->getAccountName(), contact->getJid()), node->getName());
2019-04-09 15:04:08 +00:00
break;
2019-08-28 11:40:55 +00:00
case Models::Item::room:
room = static_cast<Models::Room*>(node);
emit openConversation(Models::Roster::ElId(room->getAccountName(), room->getJid()));
2019-08-28 11:40:55 +00:00
break;
2019-04-09 15:04:08 +00:00
default:
m_ui->roster->expand(item);
break;
}
}
}
2019-05-29 15:05:54 +00:00
void Squawk::closeCurrentConversation() {
if (currentConversation != nullptr) {
2020-04-11 20:00:15 +00:00
currentConversation->deleteLater();
2022-04-03 20:53:46 +00:00
currentConversation = nullptr;
2020-04-11 20:00:15 +00:00
m_ui->filler->show();
}
2019-05-29 15:05:54 +00:00
}
void Squawk::onRosterContextMenu(const QPoint& point) {
QModelIndex index = m_ui->roster->indexAt(point);
if (index.isValid()) {
Models::Item* item = static_cast<Models::Item*>(index.internalPointer());
2020-04-17 23:17:47 +00:00
if (item->type == Models::Item::reference) {
item = static_cast<Models::Reference*>(item)->dereference();
}
contextMenu->clear();
bool hasMenu = false;
bool active = item->getAccountConnectionState() == Shared::ConnectionState::connected;
switch (item->type) {
case Models::Item::account: {
Models::Account* acc = static_cast<Models::Account*>(item);
hasMenu = true;
QString name = acc->getName();
if (acc->getActive()) {
QAction* con = contextMenu->addAction(Shared::icon("network-disconnect"), tr("Deactivate"));
connect(con, &QAction::triggered, std::bind(&Squawk::disconnectAccount, this, name));
} else {
QAction* con = contextMenu->addAction(Shared::icon("network-connect"), tr("Activate"));
connect(con, &QAction::triggered, std::bind(&Squawk::connectAccount, this, name));
}
QAction* info = contextMenu->addAction(Shared::icon("user-properties"), tr("Info"));
info->setEnabled(active);
connect(info, &QAction::triggered, std::bind(&Squawk::onActivateInfo, this, name, acc->getBareJid()));
QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove"));
2022-04-13 19:02:48 +00:00
connect(remove, &QAction::triggered, std::bind(&Squawk::removeAccountRequest, this, name));
}
break;
case Models::Item::contact: {
Models::Contact* cnt = static_cast<Models::Contact*>(item);
Models::Roster::ElId id(cnt->getAccountName(), cnt->getJid());
QString cntName = cnt->getName();
hasMenu = true;
QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), tr("Open dialog"));
dialog->setEnabled(active);
connect(dialog, &QAction::triggered, std::bind(&Squawk::onRosterItemDoubleClicked, this, index));
Shared::SubscriptionState state = cnt->getState();
switch (state) {
case Shared::SubscriptionState::both:
case Shared::SubscriptionState::to: {
QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe"));
unsub->setEnabled(active);
connect(unsub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, false));
}
break;
case Shared::SubscriptionState::from:
case Shared::SubscriptionState::unknown:
case Shared::SubscriptionState::none: {
QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe"));
sub->setEnabled(active);
connect(sub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, true));
}
}
QAction* rename = contextMenu->addAction(Shared::icon("edit-rename"), tr("Rename"));
rename->setEnabled(active);
connect(rename, &QAction::triggered, [this, cntName, id]() {
QInputDialog* dialog = new QInputDialog(this);
connect(dialog, &QDialog::accepted, [this, dialog, cntName, id]() {
QString newName = dialog->textValue();
if (newName != cntName) {
emit renameContactRequest(id.account, id.name, newName);
}
dialog->deleteLater();
});
connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater);
dialog->setInputMode(QInputDialog::TextInput);
dialog->setLabelText(tr("Input new name for %1\nor leave it empty for the contact \nto be displayed as %1").arg(id.name));
dialog->setWindowTitle(tr("Renaming %1").arg(id.name));
dialog->setTextValue(cntName);
dialog->exec();
});
QMenu* groupsMenu = contextMenu->addMenu(Shared::icon("group"), tr("Groups"));
std::deque<QString> groupList = rosterModel.groupList(id.account);
for (QString groupName : groupList) {
QAction* gr = groupsMenu->addAction(groupName);
gr->setCheckable(true);
gr->setChecked(rosterModel.groupHasContact(id.account, groupName, id.name));
gr->setEnabled(active);
connect(gr, &QAction::toggled, [this, groupName, id](bool checked) {
if (checked) {
emit addContactToGroupRequest(id.account, id.name, groupName);
} else {
emit removeContactFromGroupRequest(id.account, id.name, groupName);
}
});
}
QAction* newGroup = groupsMenu->addAction(Shared::icon("group-new"), tr("New group"));
newGroup->setEnabled(active);
connect(newGroup, &QAction::triggered, [this, id]() {
QInputDialog* dialog = new QInputDialog(this);
connect(dialog, &QDialog::accepted, [this, dialog, id]() {
emit addContactToGroupRequest(id.account, id.name, dialog->textValue());
dialog->deleteLater();
});
connect(dialog, &QDialog::rejected, dialog, &QObject::deleteLater);
dialog->setInputMode(QInputDialog::TextInput);
dialog->setLabelText(tr("New group name"));
dialog->setWindowTitle(tr("Add %1 to a new group").arg(id.name));
dialog->exec();
});
QAction* info = contextMenu->addAction(Shared::icon("user-properties"), tr("Info"));
info->setEnabled(active);
connect(info, &QAction::triggered, std::bind(&Squawk::onActivateInfo, this, id.account, id.name));
QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove"));
remove->setEnabled(active);
connect(remove, &QAction::triggered, std::bind(&Squawk::removeContactRequest, this, id.account, id.name));
2019-06-14 16:36:04 +00:00
}
break;
case Models::Item::room: {
Models::Room* room = static_cast<Models::Room*>(item);
hasMenu = true;
QAction* dialog = contextMenu->addAction(Shared::icon("mail-message"), tr("Open conversation"));
dialog->setEnabled(active);
connect(dialog, &QAction::triggered, [this, index]() {
onRosterItemDoubleClicked(index);
});
Models::Roster::ElId id(room->getAccountName(), room->getJid());
if (room->getAutoJoin()) {
QAction* unsub = contextMenu->addAction(Shared::icon("news-unsubscribe"), tr("Unsubscribe"));
unsub->setEnabled(active);
connect(unsub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, false));
} else {
QAction* sub = contextMenu->addAction(Shared::icon("news-subscribe"), tr("Subscribe"));
sub->setEnabled(active);
connect(sub, &QAction::triggered, std::bind(&Squawk::changeSubscription, this, id, true));
}
QAction* remove = contextMenu->addAction(Shared::icon("edit-delete"), tr("Remove"));
remove->setEnabled(active);
connect(remove, &QAction::triggered, std::bind(&Squawk::removeRoomRequest, this, id.account, id.name));
}
break;
default:
break;
}
if (hasMenu) {
contextMenu->popup(m_ui->roster->viewport()->mapToGlobal(point));
}
}
}
void Squawk::responseInfo(const Shared::Info& info) {
std::map<QString, UI::Info*>::const_iterator itr = infoWidgets.find(info.getAddressRef());
if (itr != infoWidgets.end()) {
itr->second->setData(info);
itr->second->hideProgress();
}
}
void Squawk::onInfoClosed() {
UI::Info* info = static_cast<UI::Info*>(sender());
2023-02-21 20:27:28 +00:00
std::map<QString, UI::Info*>::const_iterator itr = infoWidgets.find(info->getJid());
if (itr == infoWidgets.end()) {
qDebug() << "Info widget has been closed but can not be found among other opened vCards, application is most probably going to crash";
return;
}
infoWidgets.erase(itr);
}
void Squawk::onActivateInfo(const QString& account, const QString& jid) {
std::map<QString, UI::Info*>::const_iterator itr = infoWidgets.find(jid);
UI::Info* info;
if (itr != infoWidgets.end()) {
info = itr->second;
} else {
info = new UI::Info(jid);
2023-02-21 20:27:28 +00:00
info->setWindowTitle(tr("Information about %1").arg(jid));
info->setAttribute(Qt::WA_DeleteOnClose);
infoWidgets.insert(std::make_pair(jid, info));
connect(info, &UI::Info::destroyed, this, &Squawk::onInfoClosed);
connect(info, &UI::Info::saveInfo, std::bind(&Squawk::onInfoSave, this, std::placeholders::_1, account));
}
info->show();
info->raise();
info->activateWindow();
info->showProgress();
emit requestInfo(account, jid);
}
void Squawk::onInfoSave(const Shared::Info& info, const QString& account) {
UI::Info* widget = static_cast<UI::Info*>(sender());
emit updateInfo(account, info);
widget->deleteLater();
}
void Squawk::writeSettings() {
QSettings settings;
settings.beginGroup("ui");
settings.beginGroup("window");
settings.setValue("geometry", saveGeometry());
settings.setValue("state", saveState());
settings.endGroup();
settings.setValue("splitter", m_ui->splitter->saveState());
settings.endGroup();
}
void Squawk::onRosterSelectionChanged(const QModelIndex& current, const QModelIndex& previous) {
if (restoreSelection.isValid() && restoreSelection == current) {
restoreSelection = QModelIndex();
return;
}
2020-04-11 20:00:15 +00:00
if (current.isValid()) {
Models::Item* node = static_cast<Models::Item*>(current.internalPointer());
if (node->type == Models::Item::reference)
2020-04-17 23:17:47 +00:00
node = static_cast<Models::Reference*>(node)->dereference();
2022-04-03 20:53:46 +00:00
Models::Contact* contact = nullptr;
Models::Room* room = nullptr;
2020-04-11 20:00:15 +00:00
QString res;
2022-04-03 20:53:46 +00:00
Models::Roster::ElId* id = nullptr;
bool hasContext = true;
2020-04-11 20:00:15 +00:00
switch (node->type) {
case Models::Item::contact:
contact = static_cast<Models::Contact*>(node);
id = new Models::Roster::ElId(contact->getAccountName(), contact->getJid());
break;
case Models::Item::presence:
contact = static_cast<Models::Contact*>(node->parentItem());
id = new Models::Roster::ElId(contact->getAccountName(), contact->getJid());
res = node->getName();
hasContext = false;
2020-04-11 20:00:15 +00:00
break;
case Models::Item::room:
room = static_cast<Models::Room*>(node);
id = new Models::Roster::ElId(room->getAccountName(), room->getJid());
break;
case Models::Item::participant:
room = static_cast<Models::Room*>(node->parentItem());
id = new Models::Roster::ElId(room->getAccountName(), room->getJid());
hasContext = false;
break;
case Models::Item::group:
hasContext = false;
2020-04-11 20:00:15 +00:00
default:
break;
}
if (hasContext && QGuiApplication::mouseButtons() & Qt::RightButton) {
2022-04-03 20:53:46 +00:00
if (id != nullptr) {
delete id;
}
needToRestore = true;
restoreSelection = previous;
return;
}
2022-04-03 20:53:46 +00:00
if (id != nullptr) {
if (currentConversation != nullptr) {
if (currentConversation->getId() == *id) {
if (contact != nullptr)
2020-04-11 20:00:15 +00:00
currentConversation->setPalResource(res);
return;
2020-04-11 20:00:15 +00:00
} else {
currentConversation->deleteLater();
}
} else {
m_ui->filler->hide();
}
Models::Account* acc = rosterModel.getAccount(id->account);
2022-04-03 20:53:46 +00:00
if (contact != nullptr) {
2020-04-11 20:00:15 +00:00
currentConversation = new Chat(acc, contact);
2022-04-03 20:53:46 +00:00
} else if (room != nullptr) {
2020-04-11 20:00:15 +00:00
currentConversation = new Room(acc, room);
}
if (!testAttribute(Qt::WA_TranslucentBackground)) {
currentConversation->setFeedFrames(true, false, true, true);
}
emit openedConversation();
2020-04-11 20:00:15 +00:00
if (res.size() > 0) {
currentConversation->setPalResource(res);
}
m_ui->splitter->insertWidget(1, currentConversation);
delete id;
} else {
closeCurrentConversation();
2020-04-11 20:00:15 +00:00
}
} else {
closeCurrentConversation();
2020-04-11 20:00:15 +00:00
}
}
void Squawk::onContextAboutToHide() {
if (needToRestore) {
needToRestore = false;
m_ui->roster->selectionModel()->setCurrentIndex(restoreSelection, QItemSelectionModel::ClearAndSelect);
}
}
2022-04-03 20:53:46 +00:00
void Squawk::onAboutSquawkCalled() {
2022-04-03 20:53:46 +00:00
if (about == nullptr) {
about = new About();
about->setAttribute(Qt::WA_DeleteOnClose);
connect(about, &Settings::destroyed, this, &Squawk::onAboutSquawkClosed);
} else {
about->raise();
about->activateWindow();
}
about->show();
2022-04-03 20:53:46 +00:00
}
Models::Roster::ElId Squawk::currentConversationId() const {
if (currentConversation == nullptr)
return Models::Roster::ElId();
else
return Models::Roster::ElId(currentConversation->getAccount(), currentConversation->getJid());
2022-04-03 20:53:46 +00:00
}
void Squawk::select(QModelIndex index) {
m_ui->roster->scrollTo(index, QAbstractItemView::EnsureVisible);
m_ui->roster->selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);
}