diff --git a/RingWinClient.pro b/RingWinClient.pro index 3174caf9e5e20b81efc5437785bd85e39d845d5c..735b90dfcffa0045746226bb0394023bfa69699e 100644 --- a/RingWinClient.pro +++ b/RingWinClient.pro @@ -53,11 +53,9 @@ SOURCES += main.cpp\ windowscontactbackend.cpp \ selectareadialog.cpp \ accountserializationadapter.cpp \ - instantmessagingwidget.cpp \ accountstatedelegate.cpp \ videoview.cpp \ videooverlay.cpp \ - imdelegate.cpp \ contactpicker.cpp \ globalsystemtray.cpp \ conversationitemdelegate.cpp \ @@ -79,11 +77,14 @@ SOURCES += main.cpp\ smartlistview.cpp \ accountitemdelegate.cpp \ accountlistmodel.cpp \ - messagemodel.cpp \ invitebuttonswidget.cpp \ wizardwidget.cpp \ currentaccountcombobox.cpp \ - conversationfilterbutton.cpp + conversationfilterbutton.cpp \ + messagewebpage.cpp \ + messagewebview.cpp \ + webchathelpers.cpp \ + animationhelpers.cpp HEADERS += mainwindow.h \ callwidget.h \ @@ -97,11 +98,9 @@ HEADERS += mainwindow.h \ windowscontactbackend.h \ selectareadialog.h \ accountserializationadapter.h \ - instantmessagingwidget.h \ accountstatedelegate.h \ videoview.h \ videooverlay.h \ - imdelegate.h \ contactpicker.h \ settingskey.h \ globalsystemtray.h \ @@ -126,11 +125,14 @@ HEADERS += mainwindow.h \ smartlistview.h \ accountitemdelegate.h \ accountlistmodel.h \ - messagemodel.h \ invitebuttonswidget.h \ wizardwidget.h \ currentaccountcombobox.h \ - conversationfilterbutton.h + conversationfilterbutton.h \ + messagewebpage.h \ + messagewebview.h \ + webchathelpers.h \ + animationhelpers.h contains(DEFINES, URI_PROTOCOL) { @@ -144,7 +146,6 @@ FORMS += mainwindow.ui \ accountdetails.ui \ aboutdialog.ui \ wizarddialog.ui \ - instantmessagingwidget.ui \ videoview.ui \ videooverlay.ui \ contactpicker.ui \ @@ -158,7 +159,8 @@ FORMS += mainwindow.ui \ bannedcontactswidget.ui \ photoboothwidget.ui \ invitebuttonswidget.ui \ - wizardwidget.ui + wizardwidget.ui \ + animatedoverlay.ui win32: LIBS += -lole32 -luuid -lshlwapi -lgdi32 LIBS += -lqrencode @@ -273,7 +275,9 @@ win32 { QTRUNTIME.files = $$RUNTIMEDIR/Qt5Core.dll $$RUNTIMEDIR/Qt5Widgets.dll \ $$RUNTIMEDIR/Qt5Gui.dll $$RUNTIMEDIR/Qt5Svg.dll \ $$RUNTIMEDIR/Qt5Xml.dll $$RUNTIMEDIR/Qt5WinExtras.dll \ - $$RUNTIMEDIR/Qt5Network.dll $$RUNTIMEDIR/Qt5Sql.dll + $$RUNTIMEDIR/Qt5Network.dll $$RUNTIMEDIR/Qt5Sql.dll \ + $$RUNTIMEDIR/Qt5WebEngineWidgets.dll $$RUNTIMEDIR/Qt5WebChannel.dll + QTRUNTIME.path = $$OUT_PWD/release QTDEPSRUNTIME.files = $$RUNTIMEDIR/zlib1.dll \ diff --git a/aboutdialog.cpp b/aboutdialog.cpp index 9172c1dcb356002caf9d0615ce997a89434fc582..ec5d0a78e1f0ff7d844f5305e5c6bac7990dc4da 100644 --- a/aboutdialog.cpp +++ b/aboutdialog.cpp @@ -19,6 +19,8 @@ #include "aboutdialog.h" #include "ui_aboutdialog.h" +#include "version.h" + AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDialog) @@ -27,7 +29,7 @@ AboutDialog::AboutDialog(QWidget *parent) : this->setFixedSize(this->width(),this->height()); ui->creditsWidget->hide(); - ui->gitVersionLabel->setText(QString("%1: %2").arg(tr("version"), NIGHTLY_VERSION)); + ui->gitVersionLabel->setText(QString("%1: %2").arg(tr("version"), QString(VERSION_STRING))); ui->creditsBrowser->setHtml("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">" "<html><head><meta name=\"qrichtext\" content=\"1\" /><style type=\"text/css\">" diff --git a/animatedoverlay.ui b/animatedoverlay.ui new file mode 100644 index 0000000000000000000000000000000000000000..4aff548fb1189df593a2bed666e189c9011ba1fb --- /dev/null +++ b/animatedoverlay.ui @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>AnimatedOverlay</class> + <widget class="QWidget" name="AnimatedOverlay"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <property name="spacing"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QLabel" name="backgroundLabel"> + <property name="styleSheet"> + <string notr="true"/> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/animationhelpers.cpp b/animationhelpers.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c2abd8d3654e219449625a7beb4727d3066d4380 --- /dev/null +++ b/animationhelpers.cpp @@ -0,0 +1,130 @@ +/*************************************************************************** + * Copyright (C) 2018 by Savoir-faire Linux * + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * + * * + * 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/>. * + **************************************************************************/ + +#include "animationhelpers.h" + +#include "ui_animatedoverlay.h" + +#include <QTimer> +#include <QtMath> + +OpacityAnimation::OpacityAnimation(QWidget* target, QObject* parent) + : QObject(parent), + target_(target), + timer_(nullptr), + frameTime_((1.0 / 24.0) * 1000), + startValue_(0.0), + endValue_(1.0), + t_(0), + value_(0), + duration_(1000) +{ + timer_ = new QTimer(this); + connect(timer_, SIGNAL(timeout()), this, SLOT(updateAnimation())); + + effect_ = new QGraphicsOpacityEffect(this); + effect_->setOpacity(startValue_); + + target_->setGraphicsEffect(effect_); + target_->setAutoFillBackground(true); +} + +OpacityAnimation::~OpacityAnimation() +{ +} + +void +OpacityAnimation::setFPS(const int& fps) +{ + frameTime_ = (1.0 / static_cast<double>(fps)) * 1000; +} + +void +OpacityAnimation::setFrameTime(const int& milliseconds) +{ + frameTime_ = milliseconds; +} + +void +OpacityAnimation::setDuration(const int& milliseconds) +{ + duration_ = milliseconds; +} + +void +OpacityAnimation::setStartValue(const double& value) +{ + startValue_ = value; + effect_->setOpacity(startValue_); +} + +void +OpacityAnimation::setEndValue(const double& value) +{ + endValue_ = value; +} + +void +OpacityAnimation::start() +{ + timer_->start(frameTime_); +} + +void +OpacityAnimation::stop() +{ + timer_->stop(); +} + +void +OpacityAnimation::updateAnimation() +{ + double d = (startValue_ + endValue_) * 0.5; + double a = abs(startValue_ - endValue_) * 0.5; + + t_ += frameTime_; + value_ = a * sin(2 * M_PI * t_ * duration_ * .000001) + d; + effect_->setOpacity(value_); + target_->update(); +} + +AnimatedOverlay::AnimatedOverlay(QColor color, QWidget* parent) : + QWidget(parent), + ui(new Ui::AnimatedOverlay) +{ + ui->setupUi(this); + ui->backgroundLabel->setAutoFillBackground(true); + auto values = QString("%1,%2,%3,255") + .arg(color.red()) + .arg(color.green()) + .arg(color.blue()); + ui->backgroundLabel->setStyleSheet("background-color: rgba(" + values + ");"); + + oa_ = new OpacityAnimation(this, this); + oa_->setFPS(16); + oa_->setDuration(1000); + oa_->setStartValue(0.0); + oa_->setEndValue(0.25); + oa_->start(); +} + +AnimatedOverlay::~AnimatedOverlay() +{ + disconnect(this); + delete ui; +} diff --git a/instantmessagingwidget.h b/animationhelpers.h similarity index 51% rename from instantmessagingwidget.h rename to animationhelpers.h index ed4a79acc745183561083d275a06268d12f1a84c..e9cee03f429daf60d57b58660f1227429f43ab2e 100644 --- a/instantmessagingwidget.h +++ b/animationhelpers.h @@ -1,6 +1,6 @@ /*************************************************************************** - * Copyright (C) 2015-2017 by Savoir-faire Linux * - * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>* + * Copyright (C) 2018 by Savoir-faire Linux * + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * * * * 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 * @@ -19,49 +19,56 @@ #pragma once #include <QWidget> -#include <QKeyEvent> -#include <QSettings> +#include <QPropertyAnimation> +#include <QGraphicsOpacityEffect> +#include <QMovie> -#include "call.h" -#include "media/media.h" - -#include "imdelegate.h" -#include "messagemodel.h" - -namespace Ui { -class InstantMessagingWidget; -} - -class InstantMessagingWidget final : public QWidget +class OpacityAnimation : public QObject { Q_OBJECT - public: - explicit InstantMessagingWidget(QWidget *parent = 0); - ~InstantMessagingWidget(); - void setupCallMessaging(const std::string& callId, - MessageModel *messageModel); + explicit OpacityAnimation(QWidget* target, QObject* parent = nullptr); + ~OpacityAnimation(); -protected: - virtual void keyPressEvent(QKeyEvent *event) override; - virtual void showEvent(QShowEvent * event) override; + void setFPS(const int& fps); + void setFrameTime(const int& milliseconds); + void setDuration(const int& milliseconds); + void setStartValue(const double& value); + void setEndValue(const double& value); -//UI SLOTS -private slots: - void on_sendButton_clicked(); + void start(); + void stop(); private slots: - void onIncomingMessage(const std::string& convUid, uint64_t interactionId, const lrc::api::interaction::Info& interaction); + void updateAnimation(); private: - Ui::InstantMessagingWidget *ui; - ImDelegate* imDelegate_; - std::unique_ptr<MessageModel> messageModel_; - QSettings settings_; - QMetaObject::Connection newInteractionConnection_; + QGraphicsOpacityEffect* effect_; + double value_; - void copyToClipboard(); - void updateConversationView(const std::string& convUid); + QWidget* target_; + QTimer* timer_; + int frameTime_; + double t_; + int duration_; + double startValue_; + double endValue_; }; +namespace Ui { +class AnimatedOverlay; +} + +class AnimatedOverlay : public QWidget +{ + Q_OBJECT +public: + explicit AnimatedOverlay(QColor color, QWidget* parent = 0); + ~AnimatedOverlay(); + +private: + Ui::AnimatedOverlay* ui; + + OpacityAnimation* oa_; +}; \ No newline at end of file diff --git a/build-client.bat b/build-client.bat new file mode 100644 index 0000000000000000000000000000000000000000..874ba2bbeedf1a06c4511c8e801f212eb3444e84 --- /dev/null +++ b/build-client.bat @@ -0,0 +1,117 @@ +:: Ring - native Windows client project build script + +@echo off +setlocal + +if "%1" == "/?" goto Usage +if "%~1" == "" goto Usage + +set doCompile=N +set doBuild=N + +set SCRIPTNAME=%~nx0 + +if "%1"=="compile" ( + set doCompile=Y +) else if "%1"=="build" ( + set doBuild=Y +) else ( + goto Usage +) + +set arch=N + +shift +:ParseArgs +if "%1" == "" goto FinishedArgs +if /I "%1"=="x86" ( + set arch=x86 +) else if /I "%1"=="x64" ( + set arch=x64 +) else ( + goto Usage +) +shift +goto ParseArgs + +:FinishedArgs +if "%arch%"=="x86" ( + set MSBUILD_ARGS=/nologo /p:useenv=true /p:Platform=Win32 /maxcpucount:%NUMBER_OF_PROCESSORS% +) else if "%arch%"=="x64" ( + set MSBUILD_ARGS=/nologo /p:useenv=true /p:Platform=x64 /maxcpucount:%NUMBER_OF_PROCESSORS% +) + +@setlocal + +set VSInstallerFolder="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer" +if %PROCESSOR_ARCHITECTURE%==x86 set VSInstallerFolder="%ProgramFiles%\Microsoft Visual Studio\Installer" + +pushd %VSInstallerFolder% +for /f "usebackq tokens=*" %%i in (`vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do ( + set VSLATESTDIR=%%i +) +popd + +echo VS Installation folder: %VSLATESTDIR% + +if not exist "%VSLATESTDIR%\VC\Auxiliary\Build\vcvarsall.bat" ( + echo: + echo VSInstallDir not found or not installed correctly. + goto cleanup +) + +if %PROCESSOR_ARCHITECTURE%==x86 ( + set Comp_x86=x86 10.0.15063.0 + set Comp_x64=x86_amd64 10.0.15063.0 +) else ( + set Comp_x86=amd64_x86 10.0.15063.0 + set Comp_x64=amd64 10.0.15063.0 +) + +set path=%path:"=% +if "%arch%"=="x86" ( + call "%VSLATESTDIR%"\\VC\\Auxiliary\\Build\\vcvarsall.bat %Comp_x86% +) else if "%arch%"=="x64" ( + call "%VSLATESTDIR%"\\VC\\Auxiliary\\Build\\vcvarsall.bat %Comp_x64% +) + +if "%arch%" neq "N" ( + if "%doCompile%" neq "N" ( + goto compileClient + ) else if "%doBuild%" neq "N" ( + goto buildClient + ) + goto :eof +) +goto Usage + +:compileClient +msbuild ring-client-windows.vcxproj /verbosity:normal /p:Configuration=ReleaseCompile %MSBUILD_ARGS% +goto cleanup + +:buildClient +msbuild ring-client-windows.vcxproj /verbosity:normal /p:Configuration=Release %MSBUILD_ARGS% +goto cleanup + +@endlocal + +:Usage +echo: +echo The correct usage is: +echo: +echo %0 [action] [architecture] +echo: +echo where +echo: +echo [action] is: compile ^| build +echo [architecture] is: x86 ^| x64 +echo: +echo For example: +echo %0 compile x86 - compile only x86 (for CI) +echo %0 build x64 - build x64 client +echo: +goto :eof + +:cleanup +endlocal +exit /B %ERRORLEVEL% \ No newline at end of file diff --git a/callwidget.cpp b/callwidget.cpp index 71b29a7e0f9026a7bb4971d1a918140aa77156d0..d5679f2df1a5d7e67eaf860f51ab815096354c72 100644 --- a/callwidget.cpp +++ b/callwidget.cpp @@ -27,6 +27,7 @@ #include <QClipboard> #include <QDesktopServices> #include <QComboBox> +#include <QWebEngineScript> #include <memory> @@ -48,17 +49,16 @@ #include "contactpicker.h" #include "globalsystemtray.h" #include "conversationitemdelegate.h" -#include "imdelegate.h" #include "pixbufmanipulator.h" #include "settingskey.h" #include "lrcinstance.h" -#include "messagemodel.h" +#include "animationhelpers.h" +#include "ringthemeutils.h" CallWidget::CallWidget(QWidget* parent) : NavWidget(parent), ui(new Ui::CallWidget), - menu_(new QMenu()), - imDelegate_(new ImDelegate()) + menu_(new QMenu()) { ui->setupUi(this); @@ -77,11 +77,12 @@ CallWidget::CallWidget(QWidget* parent) : // this line is not welcome here, and must be removed ProfileModel::instance().addCollection<LocalProfileCollection>(LoadOptions::FORCE_ENABLED); + QSettings settings; + // select last used account if stored in registry auto accountList = LRCInstance::accountModel().getAccountList(); if (!accountList.empty()) { std::string accountIdToStartWith; - QSettings settings; if (settings.contains(SettingsKey::selectedAccount)) { accountIdToStartWith = settings .value(SettingsKey::selectedAccount, true) @@ -102,11 +103,19 @@ CallWidget::CallWidget(QWidget* parent) : } } + if (settings.contains(SettingsKey::mainSplitterState)) { + auto splitterStates = settings.value(SettingsKey::mainSplitterState).toByteArray(); + ui->mainActivitySplitter->restoreState(splitterStates); + } + + ui->mainActivitySplitter->setCollapsible(0, false); + ui->splitter->setCollapsible(0, false); + ui->splitter->setCollapsible(1, false); + //disable dropdown shadow on combobox ui->currentAccountComboBox->view()->window()->setWindowFlags(Qt::Popup | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint); // conversation list - conversationItemDelegate_ = new ConversationItemDelegate(); ui->smartList->setContextMenuPolicy(Qt::CustomContextMenu); // setup searchingfor mini spinner @@ -114,29 +123,31 @@ CallWidget::CallWidget(QWidget* parent) : ui->spinnerLabel->setMovie(miniSpinner_); ui->spinnerLabel->hide(); - ui->listMessageView->verticalScrollBar()->setEnabled(true); - ui->listMessageView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); - ui->listMessageView->verticalScrollBar()->setStyleSheet("QScrollBar:vertical { width: 0px; }"); - - setupOutOfCallIM(); - // connections connect(ui->currentAccountComboBox, &CurrentAccountComboBox::settingsButtonClicked, this, &CallWidget::settingsButtonClicked); - connect(ui->videoWidget, SIGNAL(setChatVisibility(bool)), - ui->instantMessagingWidget, SLOT(setVisible(bool))); + connect(ui->videoWidget, &VideoView::setChatVisibility, + [this](bool visible) { + ui->callStackWidget->hide(); + }); + + connect(ui->mainActivitySplitter, &QSplitter::splitterMoved, + [this](int pos, int index) { + QSettings settings; + settings.setValue(SettingsKey::mainSplitterState, ui->mainActivitySplitter->saveState()); + }); connect(ui->videoWidget, &VideoView::videoSettingsClicked, this, &CallWidget::settingsButtonClicked); - connect(ui->buttonConversations, &QPushButton::clicked, + connect(ui->btnConversations, &QPushButton::clicked, this, &CallWidget::conversationsButtonClicked); - connect(ui->buttonInvites, &QPushButton::clicked, + connect(ui->btnInvites, &QPushButton::clicked, this, &CallWidget::invitationsButtonClicked); - connect(ui->smartList, &QListView::customContextMenuRequested, + connect(ui->smartList, &QTreeView::customContextMenuRequested, this, &CallWidget::slotCustomContextMenuRequested); connect(ui->smartList, &SmartListView::btnAcceptInviteClicked, @@ -169,48 +180,25 @@ CallWidget::CallWidget(QWidget* parent) : connect(ui->btnVideoCall, &QPushButton::clicked, this, &CallWidget::on_sendContactRequestButton_clicked); - // connect conversation filter buttons to act as radio buttons - connect(ui->buttonInvites, &ConversationFilterButton::clicked, - ui->buttonConversations, &ConversationFilterButton::setUnselected); - - connect(ui->buttonConversations, &ConversationFilterButton::clicked, - ui->buttonInvites, &ConversationFilterButton::setUnselected); - connect(ui->currentAccountComboBox, QOverload<int>::of(&CurrentAccountComboBox::currentIndexChanged), [this] { - ui->buttonConversations->setSelected(); - ui->buttonInvites->setUnselected(); - }); + ui->btnConversations->setChecked(true); + ui->btnInvites->setChecked(false); + }); + // set first view to welcome view ui->stackedWidget->setCurrentWidget(ui->welcomePage); - ui->buttonConversations->setSelected(); + ui->btnConversations->setChecked(true); + + // chat view + ui->messageView->buildView(); } CallWidget::~CallWidget() { delete ui; delete menu_; - delete imDelegate_; - delete conversationItemDelegate_; -} - -void -CallWidget::setupOutOfCallIM() -{ - ui->listMessageView->setItemDelegate(imDelegate_); - ui->listMessageView->setContextMenuPolicy(Qt::ActionsContextMenu); - - auto copyAction = new QAction(tr("Copy"), this); - ui->listMessageView->addAction(copyAction); - - connect(copyAction, &QAction::triggered, [=]() { - auto idx = ui->listMessageView->currentIndex(); - if (idx.isValid()) { - auto text = ui->listMessageView->model()->data(idx); - QApplication::clipboard()->setText(text.value<QString>()); - } - }); } void @@ -220,11 +208,24 @@ CallWidget::onIncomingMessage(const std::string& convUid, { Q_UNUSED(interactionId); if (!QApplication::focusWidget()) { + auto convModel = LRCInstance::getCurrentConversationModel(); + auto conversation = Utils::getConversationFromUid(convUid, *convModel); + auto bestName = Utils::bestNameForConversation(*conversation, *convModel); Utils::showSystemNotification(this, QString(tr("Message incoming from %1")) - .arg(QString::fromStdString(interaction.body))); + .arg(QString::fromStdString(bestName))); + } + if (convUid != selectedConvUid()) { + return; } - updateConversationView(convUid); + + auto convModel = LRCInstance::getCurrentConversationModel(); + convModel->clearUnreadInteractions(convUid); + auto currentConversation = Utils::getConversationFromUid(convUid, *convModel); + if (currentConversation == convModel->allFilteredConversations().end()) { + return; + } + ui->messageView->printNewInteraction(*convModel, interactionId, interaction); ui->conversationsFilterWidget->update(); } @@ -306,9 +307,9 @@ CallWidget::setupSmartListContextMenu(const QPoint& pos) ); }); } - smartListModel_->isContextMenuOpen_ = true; + smartListModel_->isContextMenuOpen = true; menu.exec(globalPos); - smartListModel_->isContextMenuOpen_ = false; + smartListModel_->isContextMenuOpen = false; } void @@ -386,18 +387,9 @@ CallWidget::on_cancelButton_clicked() void CallWidget::showConversationView() { - ui->stackedWidget->setCurrentWidget(ui->messagingPage); - ui->imMessageEdit->clear(); - ui->imMessageEdit->setFocus(); - disconnect(imClickedConnection_); - imClickedConnection_ = connect(ui->listMessageView, &QListView::clicked, [this](const QModelIndex& index) { - auto urlList = index.data(static_cast<int>(media::TextRecording::Role::LinkList)).value<QList<QUrl>>(); - if (urlList.size() == 1) { - QDesktopServices::openUrl(urlList.at(0)); - } else if (urlList.size()) { - //TODO Handle multiple url in one message - } - }); + ui->stackedWidget->setCurrentWidget(ui->mainActivityWidget); + ui->messageView->setFocus(); + } void @@ -475,21 +467,6 @@ CallWidget::smartListSelectionChanged(const QItemSelection &selected, const QIt selectConversation(selectedIndex); } -void -CallWidget::contactReqListCurrentChanged(const QModelIndex ¤tIdx, const QModelIndex &previousIdx) -{ - Q_UNUSED(previousIdx) - - if (currentIdx.isValid()) { - ui->contactRequestWidget->setCurrentContactRequest(currentIdx); - ui->stackedWidget->setCurrentWidget(ui->contactRequestPage); - } else { - ui->contactRequestWidget->setCurrentContactRequest(QModelIndex()); - if (ui->stackedWidget->currentWidget() == ui->contactRequestPage) - Utils::setStackWidget(ui->stackedWidget, ui->welcomePage); - } -} - void CallWidget::placeCall() { @@ -498,21 +475,27 @@ CallWidget::placeCall() Call* c = CallModel::instance().dialingCall(PhoneDirectoryModel::instance().getNumber(ui->ringContactLineEdit->text())); c->performAction(Call::Action::ACCEPT); ui->ringContactLineEdit->clear(); + auto photoRect = ui->callingPhoto->frameGeometry(); ui->callingPhoto->setPixmap( QPixmap::fromImage( GlobalInstances::pixmapManipulator() - .callPhoto(c, QSize(130,130)).value<QImage>())); + .callPhoto(c, QSize(photoRect.width(), photoRect.height())) + .value<QImage>())); } void CallWidget::conversationsButtonClicked() { + ui->btnConversations->setChecked(true); + ui->btnInvites->setChecked(false); setConversationFilter(lrc::api::profile::Type::RING); } void CallWidget::invitationsButtonClicked() { + ui->btnConversations->setChecked(false); + ui->btnInvites->setChecked(true); setConversationFilter(lrc::api::profile::Type::PENDING); } @@ -535,8 +518,8 @@ CallWidget::on_ringContactLineEdit_returnPressed() // select current temporary item and show conversation auto convModel = LRCInstance::getCurrentConversationModel(); auto conversations = convModel->allFilteredConversations(); - auto contactIsValid = Utils::isContactValid(conversations.at(0).participants.at(0), *convModel); - if (!conversations.empty() && contactIsValid) { + if (!conversations.empty() && + Utils::isContactValid(conversations.at(0).participants.at(0), *convModel)) { selectConversation(smartListModel_->index(0)); } } @@ -575,65 +558,112 @@ void CallWidget::slotShowCallView(const std::string& accountId, { Q_UNUSED(accountId); Q_UNUSED(convInfo); - qDebug() << "BehaviorController::showCallView"; - ui->stackedWidget->setCurrentWidget(ui->videoPage); + qDebug() << "slotShowCallView"; + ui->callStackWidget->show(); + ui->callStackWidget->setCurrentWidget(ui->videoPage); hideMiniSpinner(); } void CallWidget::slotShowIncomingCallView(const std::string& accountId, - const lrc::api::conversation::Info& convInfo) { + const lrc::api::conversation::Info& convInfo) +{ Q_UNUSED(accountId); - qDebug() << "BehaviorController::showIncomingCallView"; + qDebug() << "slotShowIncomingCallView"; + auto callModel = LRCInstance::getCurrentCallModel(); + + if (!callModel->hasCall(convInfo.callId)) { + return; + } + + auto convModel = LRCInstance::getCurrentConversationModel(); + ui->callerPhoto->setPixmap(QPixmap::fromImage(imageForConv(convInfo.uid))); + auto bestName = QString::fromStdString(Utils::bestNameForConversation(convInfo, *convModel)); + auto bestId = QString::fromStdString(Utils::bestIdForConversation(convInfo, *convModel)); + auto finalBestId = (bestName != bestId) ? bestId : ""; + auto call = callModel->getCall(convInfo.callId); + auto isCallSelected = selectedConvUid() == convInfo.uid; + if (call.isOutgoing) { - miniSpinner_->start(); - ui->spinnerLabel->show(); - ui->stackedWidget->setCurrentWidget(ui->outboundCallPage); - } - else { - selectSmartlistItem(convInfo.uid); - auto selectedAccountId = LRCInstance::getCurrentAccountInfo().id; - auto accountProperties = LRCInstance::accountModel().getAccountConfig(selectedAccountId); - if (!accountProperties.autoAnswer) { - ui->stackedWidget->setCurrentWidget(ui->callInvitePage); - } - else { - ui->stackedWidget->setCurrentWidget(ui->videoPage); + if (isCallSelected) { + miniSpinner_->start(); + ui->stackedWidget->setCurrentWidget(ui->mainActivityWidget); + ui->spinnerLabel->show(); + ui->callStackWidget->setCurrentWidget(ui->outgoingCallPage); + ui->callStackWidget->show(); } + } else { if (!QApplication::focusWidget()) { - auto formattedName = Utils::bestNameForConversation(convInfo, *LRCInstance::getCurrentConversationModel()); + auto formattedName = Utils::bestNameForConversation(convInfo, *convModel); Utils::showSystemNotification(this, QString(tr("Call incoming from %1")).arg(QString::fromStdString(formattedName))); } + selectSmartlistItem(convInfo.uid); + auto selectedAccountId = LRCInstance::getCurrentAccountInfo().id; + auto accountProperties = LRCInstance::accountModel().getAccountConfig(selectedAccountId); + if (accountProperties.autoAnswer) { + ui->stackedWidget->setCurrentWidget(ui->mainActivityWidget); + ui->callStackWidget->setCurrentWidget(ui->videoPage); + } else if (isCallSelected) { + ui->stackedWidget->setCurrentWidget(ui->mainActivityWidget); + ui->callStackWidget->setCurrentWidget(ui->incomingCallPage); + ui->callStackWidget->show(); + } } - if (!callModel->hasCall(convInfo.callId)) { - return; + // flashing index widget during call + for (int row = 0; row < smartListModel_->rowCount(); row++) { + QModelIndex index = smartListModel_->index(row); + auto indexUid = index.data(SmartListModel::Role::UID).value<QString>().toStdString(); + if (indexUid == convInfo.uid) { + auto widget = ui->smartList->indexWidget(index); + if (!widget) { + auto blinkingWidget = new AnimatedOverlay(RingTheme::urgentOrange_); + ui->smartList->setIndexWidget(index, blinkingWidget); + blinkingWidget->show(); + } else { + widget->show(); + } + } } ui->videoWidget->pushRenderer(convInfo.callId); - ui->instantMessagingWidget->setupCallMessaging(convInfo.callId, messageModel_.get()); + QFontMetrics primaryCallLabelFontMetrics(ui->callingBestNameLabel->font()); + QFontMetrics sencondaryCallLabelFontMetrics(ui->callingBestIdLabel->font()); - disconnect(selectedCallChanged_); - selectedCallChanged_ = connect( - callModel, - &lrc::api::NewCallModel::callStatusChanged, - [this, callModel](const std::string& callUid) { - auto call = callModel->getCall(callUid); - qDebug() << "NewCallModel::callStatusChanged: " << static_cast<int>(call.status); - } - ); + QString elidedLabel = primaryCallLabelFontMetrics.elidedText(bestName, Qt::ElideRight, ui->callerBestNameLabel->width()); + ui->callerBestNameLabel->setText(elidedLabel); + + elidedLabel = primaryCallLabelFontMetrics.elidedText(bestName, Qt::ElideRight, ui->callingBestNameLabel->width()); + ui->callingBestNameLabel->setText(elidedLabel); - ui->callerPhoto->setPixmap(QPixmap::fromImage(imageForConv(selectedConvUid()))); + elidedLabel = sencondaryCallLabelFontMetrics.elidedText(finalBestId, Qt::ElideRight, ui->callingBestIdLabel->width()); + ui->callingBestIdLabel->setText(elidedLabel); + + ui->smartList->update(); } void CallWidget::slotShowChatView(const std::string& accountId, - const lrc::api::conversation::Info& convInfo) { + const lrc::api::conversation::Info& convInfo) +{ Q_UNUSED(accountId); Q_UNUSED(convInfo); - qDebug() << "BehaviorController::showChatView"; + + for (int row = 0; row < smartListModel_->rowCount(); row++) { + QModelIndex index = smartListModel_->index(row); + auto indexUid = index.data(SmartListModel::Role::UID).value<QString>().toStdString(); + if (indexUid == convInfo.uid) { + if (auto widget = ui->smartList->indexWidget(index)) { + widget->hide(); + widget->deleteLater(); + ui->smartList->setIndexWidget(index, nullptr); + } + } + } + + ui->callStackWidget->hide(); showConversationView(); } @@ -681,6 +711,7 @@ void CallWidget::updateConversationsFilterWidget() LRCInstance::getCurrentConversationModel()->setFilter(currentTypeFilter_); } ui->conversationsFilterWidget->setVisible(invites); + ui->conversationsFilterWidget->update(); } void CallWidget::setConversationFilter(const QString & filter) @@ -705,16 +736,6 @@ CallWidget::showIMOutOfCall(const QModelIndex& nodeIdx) } catch (...) {} - if (!isRINGAccount){ - ui->imMessageEdit->setPlaceholderText("No messaging possible out of call (SIP) "); - ui->imMessageEdit->setEnabled(false); - ui->sendIMButton->hide(); - } else { - ui->imMessageEdit->setPlaceholderText("Type your message here"); - ui->imMessageEdit->setEnabled(true); - ui->sendIMButton->show(); - } - ui->imNameLabel->setText(QString(tr("%1", "%1 is the contact username")) .arg(displayName)); @@ -729,44 +750,34 @@ CallWidget::showIMOutOfCall(const QModelIndex& nodeIdx) bool shouldShowSendContactRequestBtn = !isContact && isRINGAccount; ui->sendContactRequestButton->setVisible(shouldShowSendContactRequestBtn); - showConversationView(); - + auto convModel = LRCInstance::getCurrentConversationModel(); auto currentConversation = Utils::getConversationFromUid(selectedConvUid(), - *LRCInstance::getCurrentConversationModel()); - messageModel_.reset(new MessageModel(*currentConversation, accountInfo, this->parent())); - ui->listMessageView->setModel(messageModel_.get()); - ui->listMessageView->scrollToBottom(); -} + *convModel); + ui->messageView->clear(); + ui->messageView->printHistory(*convModel, currentConversation->interactions); -void -CallWidget::on_sendIMButton_clicked() -{ - auto msg = ui->imMessageEdit->text(); - if (msg.trimmed().isEmpty()) return; - ui->imMessageEdit->clear(); - try { - LRCInstance::getCurrentConversationModel()->sendMessage(selectedConvUid(), msg.toStdString()); - } catch (...) { - qDebug() << "exception when sending message"; - } -} - -void -CallWidget::on_imMessageEdit_returnPressed() -{ - on_sendIMButton_clicked(); -} - -void -CallWidget::slotAccountMessageReceived(const QMap<QString,QString> message, - ContactMethod* cm, - media::Media::Direction dir) -{ - Q_UNUSED(message) - Q_UNUSED(dir) + showConversationView(); - ui->listMessageView->scrollToBottom(); - cm->textRecording()->setAllRead(); + // Contact Avatars + auto accInfo = &LRCInstance::getCurrentAccountInfo(); + auto contactUri = currentConversation->participants.front(); + try { + auto& contact = accInfo->contactModel->getContact(contactUri); + if (!contact.profileInfo.avatar.empty()) { + ui->messageView->setSenderImage( + accInfo->contactModel->getContactProfileId(contactUri), + contact.profileInfo.avatar); + } else { + auto avatar = Utils::conversationPhoto(selectedConvUid(), *accInfo); + QByteArray ba; + QBuffer bu(&ba); + avatar.save(&bu, "PNG"); + std::string avatarString = ba.toBase64().toStdString(); + ui->messageView->setSenderImage( + accInfo->contactModel->getContactProfileId(contactUri), + avatarString); + } + } catch (...) { } } void @@ -780,6 +791,7 @@ void CallWidget::backToWelcomePage() { deselectConversation(); + ui->messageView->hideMessages(); ui->stackedWidget->setCurrentWidget(ui->welcomePage); } @@ -855,6 +867,7 @@ CallWidget::connectConversationModel() QObject::disconnect(conversationClearedConnection); QObject::disconnect(interactionStatusUpdatedConnection_); QObject::disconnect(newInteractionConnection_); + QObject::disconnect(interactionRemovedConnection_); modelSortedConnection_ = QObject::connect( currentConversationModel, &lrc::api::ConversationModel::modelSorted, @@ -896,7 +909,7 @@ CallWidget::connectConversationModel() conversationClearedConnection = QObject::connect( currentConversationModel, &lrc::api::ConversationModel::conversationCleared, [this](const std::string& convUid) { - updateConversationView(convUid); + ui->messageView->clear(); // if currently selected, // switch to welcome screen (deselecting current smartlist item ) if (convUid != selectedConvUid()) { @@ -907,11 +920,15 @@ CallWidget::connectConversationModel() ); interactionStatusUpdatedConnection_ = QObject::connect( currentConversationModel, &lrc::api::ConversationModel::interactionStatusUpdated, - [this](const std::string& convUid) { + [this](const std::string& convUid, uint64_t interactionId, const lrc::api::interaction::Info& interaction) { if (convUid != selectedConvUid()) { return; } - updateConversationView(convUid); + auto& currentAccountInfo = LRCInstance::getCurrentAccountInfo(); + auto currentConversationModel = currentAccountInfo.conversationModel.get(); + currentConversationModel->clearUnreadInteractions(convUid); + ui->conversationsFilterWidget->update(); + ui->messageView->updateInteraction(*currentConversationModel, interactionId, interaction); } ); newInteractionConnection_ = QObject::connect( @@ -920,6 +937,13 @@ CallWidget::connectConversationModel() onIncomingMessage(convUid, interactionId, interaction); } ); + interactionRemovedConnection_ = QObject::connect( + currentConversationModel, &lrc::api::ConversationModel::interactionRemoved, + [this](const std::string& convUid, uint64_t interactionId) { + Q_UNUSED(convUid); + ui->messageView->removeInteraction(interactionId); + } + ); currentConversationModel->setFilter(""); // clear search field ui->ringContactLineEdit->setText(""); @@ -933,18 +957,8 @@ CallWidget::updateConversationView(const std::string& convUid) return; } - auto& currentAccountInfo = LRCInstance::getCurrentAccountInfo(); - auto currentConversationModel = currentAccountInfo.conversationModel.get(); - currentConversationModel->clearUnreadInteractions(convUid); - ui->conversationsFilterWidget->update(); - auto currentConversation = Utils::getConversationFromUid(convUid, - *currentConversationModel); - if (currentConversation == currentConversationModel->allFilteredConversations().end()) { - return; - } - messageModel_.reset(new MessageModel(*currentConversation, currentAccountInfo, this->parent())); - ui->listMessageView->setModel(messageModel_.get()); - ui->listMessageView->scrollToBottom(); + + } void @@ -959,6 +973,7 @@ CallWidget::selectConversation(const QModelIndex& index) const auto item = currentConversationModel->filteredConversation(index.row()); if (selectConversation(item, *currentConversationModel)) { + showIMOutOfCall(index); auto convUid = selectedConvUid(); if (!lastConvUid_.compare(convUid)) { return; @@ -968,12 +983,12 @@ CallWidget::selectConversation(const QModelIndex& index) auto callModel = LRCInstance::getCurrentCallModel(); auto conversation = Utils::getConversationFromUid(convUid, *currentConversationModel); const auto item = currentConversationModel->filteredConversation(index.row()); - if (callModel->hasCall(conversation->callId)) { - ui->stackedWidget->setCurrentWidget(ui->videoPage); - } - else { - showIMOutOfCall(index); + if (callModel->hasCall(conversation->callId) && item.callId == conversation->callId) { + ui->stackedWidget->setCurrentWidget(ui->mainActivityWidget); + ui->callStackWidget->show(); + return; } + ui->callStackWidget->hide(); } } diff --git a/callwidget.h b/callwidget.h index 7c37bcfb08db9ef9d2fabb1aca1142997cf5127c..e6b1f13f3caa5e4fc6c4edcadb35124651b42b17 100644 --- a/callwidget.h +++ b/callwidget.h @@ -30,7 +30,6 @@ #include <QMovie> #include "navwidget.h" -#include "instantmessagingwidget.h" #include "smartlistmodel.h" // old LRC @@ -51,7 +50,6 @@ #include "api/newcallmodel.h" class ConversationItemDelegate; -class ImDelegate; class QPropertyAnimation; namespace Ui { @@ -87,8 +85,6 @@ private slots: void on_refuseButton_clicked(); void on_cancelButton_clicked(); void on_smartList_doubleClicked(const QModelIndex& index); - void on_sendIMButton_clicked(); - void on_imMessageEdit_returnPressed(); void on_ringContactLineEdit_textChanged(const QString& text); void on_imBackButton_clicked(); void on_sendContactRequestButton_clicked(); @@ -101,15 +97,12 @@ private slots: private slots: void smartListSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); - void contactReqListCurrentChanged(const QModelIndex ¤tIdx, const QModelIndex &previousIdx); - void slotAccountMessageReceived(const QMap<QString,QString> message,ContactMethod* cm, media::Media::Direction dir); void onIncomingMessage(const std::string & convUid, uint64_t interactionId, const lrc::api::interaction::Info & interaction); private: void placeCall(); void conversationsButtonClicked(); void invitationsButtonClicked(); - void setupOutOfCallIM(); void setupSmartListContextMenu(const QPoint &pos); void setupQRCode(QString ringID); void backToWelcomePage(); @@ -136,14 +129,6 @@ private: const std::string& selectedConvUid(); QMenu* menu_; - ConversationItemDelegate* conversationItemDelegate_; - ImDelegate* imDelegate_; - - QMetaObject::Connection imConnection_; - QMetaObject::Connection imVisibleConnection_; - QMetaObject::Connection callChangedConnection_; - QMetaObject::Connection imClickedConnection_; - QMetaObject::Connection crListSelectionConnection_; Ui::CallWidget* ui; QMovie* miniSpinner_; @@ -154,9 +139,13 @@ private: Video::Renderer* videoRenderer_; std::string lastConvUid_ {}; lrc::api::profile::Type currentTypeFilter_{}; - std::unique_ptr<SmartListModel> smartListModel_; - std::unique_ptr<MessageModel> messageModel_; + + QMetaObject::Connection imConnection_; + QMetaObject::Connection imVisibleConnection_; + QMetaObject::Connection callChangedConnection_; + QMetaObject::Connection imClickedConnection_; + QMetaObject::Connection crListSelectionConnection_; QMetaObject::Connection modelSortedConnection_; QMetaObject::Connection modelUpdatedConnection_; QMetaObject::Connection filterChangedConnection_; @@ -167,4 +156,5 @@ private: QMetaObject::Connection conversationClearedConnection; QMetaObject::Connection selectedCallChanged_; QMetaObject::Connection smartlistSelectionConnection_; + QMetaObject::Connection interactionRemovedConnection_; }; diff --git a/callwidget.ui b/callwidget.ui index 6b8f98b0b8bccbca571ae06f4d64bf17a1582be3..dcc32b1ce1707cffd5af68d4dc99752b2c0716eb 100644 --- a/callwidget.ui +++ b/callwidget.ui @@ -6,8 +6,8 @@ <rect> <x>0</x> <y>0</y> - <width>746</width> - <height>639</height> + <width>1250</width> + <height>729</height> </rect> </property> <property name="sizePolicy"> @@ -72,7 +72,7 @@ </property> <property name="minimumSize"> <size> - <width>324</width> + <width>320</width> <height>0</height> </size> </property> @@ -93,256 +93,273 @@ <number>0</number> </property> <item row="0" column="0"> - <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0"> - <property name="spacing"> - <number>0</number> - </property> - <property name="sizeConstraint"> - <enum>QLayout::SetDefaultConstraint</enum> - </property> - <property name="topMargin"> - <number>4</number> - </property> - <item> - <widget class="CurrentAccountComboBox" name="currentAccountComboBox" native="true"> - <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>0</width> - <height>60</height> - </size> - </property> - <property name="autoFillBackground"> - <bool>false</bool> - </property> - <property name="styleSheet"> - <string notr="true"/> - </property> - </widget> - </item> - <item> - <widget class="QWidget" name="selectBar" native="true"> - <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="maximumSize"> - <size> - <width>16777214</width> - <height>30</height> - </size> - </property> - <layout class="QHBoxLayout" name="selectBar_layout" stretch="0,0,0"> - <property name="spacing"> - <number>0</number> - </property> - <property name="sizeConstraint"> - <enum>QLayout::SetDefaultConstraint</enum> - </property> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> + <widget class="QWidget" name="sidePanelLayoutWidget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0"> + <property name="spacing"> + <number>0</number> + </property> + <property name="sizeConstraint"> + <enum>QLayout::SetDefaultConstraint</enum> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>4</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="CurrentAccountComboBox" name="currentAccountComboBox" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Fixed</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>10</width> - <height>38</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="RingContactLineEdit" name="ringContactLineEdit"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>266</width> - <height>30</height> - </size> - </property> - <property name="baseSize"> - <size> - <width>244</width> - <height>30</height> - </size> - </property> - <property name="font"> - <font> - <pointsize>9</pointsize> - </font> - </property> - <property name="cursor"> - <cursorShape>IBeamCursor</cursorShape> - </property> - <property name="toolTip"> - <string>Search contact text input</string> - </property> - <property name="maxLength"> - <number>100</number> - </property> - <property name="alignment"> - <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> - </property> - <property name="placeholderText"> - <string>Search contacts or enter ring ID</string> - </property> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Fixed</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>10</width> - <height>38</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QWidget" name="smartListWidget" native="true"> - <layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0"> - <property name="spacing"> - <number>0</number> + <property name="minimumSize"> + <size> + <width>0</width> + <height>60</height> + </size> </property> - <property name="leftMargin"> - <number>1</number> + <property name="autoFillBackground"> + <bool>false</bool> </property> - <property name="topMargin"> - <number>1</number> + <property name="styleSheet"> + <string notr="true"/> </property> - <property name="rightMargin"> - <number>0</number> + </widget> + </item> + <item> + <widget class="QWidget" name="selectBar" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - <property name="bottomMargin"> - <number>1</number> + <property name="maximumSize"> + <size> + <width>16777214</width> + <height>30</height> + </size> </property> - <item> - <widget class="ConversationsFilterWidget" name="conversationsFilterWidget" native="true"> - <layout class="QHBoxLayout" name="conversationFilterLayout"> - <property name="spacing"> - <number>10</number> + <layout class="QHBoxLayout" name="selectBar_layout" stretch="0,0,0"> + <property name="spacing"> + <number>0</number> + </property> + <property name="sizeConstraint"> + <enum>QLayout::SetDefaultConstraint</enum> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> </property> - <property name="leftMargin"> - <number>10</number> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> </property> - <property name="topMargin"> - <number>10</number> + <property name="sizeHint" stdset="0"> + <size> + <width>10</width> + <height>38</height> + </size> </property> - <property name="rightMargin"> - <number>10</number> + </spacer> + </item> + <item> + <widget class="RingContactLineEdit" name="ringContactLineEdit"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>266</width> + <height>30</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>244</width> + <height>30</height> + </size> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="cursor"> + <cursorShape>IBeamCursor</cursorShape> + </property> + <property name="toolTip"> + <string>Search contact text input</string> + </property> + <property name="maxLength"> + <number>100</number> </property> - <property name="bottomMargin"> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + <property name="placeholderText"> + <string>Search contacts or enter ring ID</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>10</width> + <height>38</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QWidget" name="smartListWidget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>1</number> + </property> + <property name="topMargin"> + <number>1</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>1</number> + </property> + <item> + <widget class="ConversationsFilterWidget" name="conversationsFilterWidget" native="true"> + <layout class="QHBoxLayout" name="conversationFilterLayout"> + <property name="spacing"> + <number>10</number> + </property> + <property name="leftMargin"> + <number>10</number> + </property> + <property name="topMargin"> + <number>10</number> + </property> + <property name="rightMargin"> + <number>10</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="btnConversations"> + <property name="toolTip"> + <string>Show conversations</string> + </property> + <property name="text"> + <string>Conversations</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="btnInvites"> + <property name="toolTip"> + <string>Show invites</string> + </property> + <property name="text"> + <string>Invites</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_13"> + <property name="spacing"> <number>0</number> </property> + <property name="topMargin"> + <number>10</number> + </property> <item> - <widget class="ConversationFilterButton" name="buttonConversations" native="true"> - <property name="toolTip"> - <string>Show conversations</string> + <widget class="SmartListView" name="smartList"> + <property name="autoScrollMargin"> + <number>16</number> </property> - <property name="text" stdset="0"> - <string>Conversations</string> + <property name="indentation"> + <number>0</number> </property> - </widget> - </item> - <item> - <widget class="ConversationFilterButton" name="buttonInvites" native="true"> - <property name="toolTip"> - <string>Show invites</string> + <property name="rootIsDecorated"> + <bool>false</bool> </property> - <property name="text" stdset="0"> - <string>Invites</string> + <property name="itemsExpandable"> + <bool>false</bool> </property> + <property name="expandsOnDoubleClick"> + <bool>false</bool> + </property> + <attribute name="headerVisible"> + <bool>false</bool> + </attribute> </widget> </item> </layout> - </widget> - </item> - <item> - <layout class="QVBoxLayout" name="verticalLayout_13"> - <property name="spacing"> - <number>0</number> - </property> - <property name="topMargin"> - <number>10</number> - </property> - <item> - <widget class="SmartListView" name="smartList"> - <property name="autoScrollMargin"> - <number>16</number> - </property> - <property name="indentation"> - <number>0</number> - </property> - <property name="rootIsDecorated"> - <bool>false</bool> - </property> - <property name="itemsExpandable"> - <bool>false</bool> - </property> - <property name="expandsOnDoubleClick"> - <bool>false</bool> - </property> - <attribute name="headerVisible"> - <bool>false</bool> - </attribute> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - </layout> + </item> + </layout> + </widget> + </item> + </layout> + </widget> </item> </layout> </widget> <widget class="QWidget" name="widgetSplitterRight" native="true"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>1</horstretch> + <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> @@ -366,7 +383,7 @@ <number>0</number> </property> <item row="0" column="0"> - <widget class="QWidget" name="CallSubGroup" native="true"> + <widget class="QWidget" name="mainLayoutWidget" native="true"> <property name="sizePolicy"> <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> <horstretch>0</horstretch> @@ -404,7 +421,7 @@ <string/> </property> <property name="currentIndex"> - <number>2</number> + <number>0</number> </property> <widget class="QWidget" name="welcomePage"> <layout class="QVBoxLayout" name="verticalLayout_15"> @@ -423,70 +440,6 @@ <property name="bottomMargin"> <number>0</number> </property> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_9"> - <property name="spacing"> - <number>0</number> - </property> - <property name="topMargin"> - <number>7</number> - </property> - <property name="rightMargin"> - <number>7</number> - </property> - <item> - <spacer name="horizontalSpacer_11"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="settingsButton"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="toolTip"> - <string>Configuration menu</string> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_settings_white_48dp_2x.png</normaloff>:/images/icons/ic_settings_white_48dp_2x.png</iconset> - </property> - <property name="iconSize"> - <size> - <width>18</width> - <height>18</height> - </size> - </property> - </widget> - </item> - </layout> - </item> <item> <spacer name="verticalSpacer"> <property name="orientation"> @@ -592,8 +545,8 @@ </property> <property name="text"> <string> - This is your RingID. - Copy and share it with your friends! + This is your RingID. +Copy and share it with your friends! </string> </property> <property name="textFormat"> @@ -870,426 +823,346 @@ </item> </layout> </widget> - <widget class="QWidget" name="sendContactRequestPage"> - <layout class="QVBoxLayout" name="verticalLayout_7"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_10"> - <property name="bottomMargin"> - <number>0</number> + <widget class="QWidget" name="mainActivityWidget"> + <layout class="QGridLayout" name="gridLayout_5"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <property name="spacing"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QSplitter" name="mainActivitySplitter"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - <item> - <widget class="QPushButton" name="sendCRBackButton"> - <property name="minimumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="toolTip"> - <string>Back to homepage button</string> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_arrow_back_white_24dp.png</normaloff>:/images/icons/ic_arrow_back_white_24dp.png</iconset> - </property> - <property name="iconSize"> - <size> - <width>18</width> - <height>18</height> - </size> - </property> - <property name="flat"> - <bool>false</bool> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_14"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item> - <widget class="SendContactRequestWidget" name="sendContactRequestWidget" native="true"> - <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> + <property name="orientation"> + <enum>Qt::Horizontal</enum> </property> - <property name="locale"> - <locale language="English" country="UnitedStates"/> + <property name="handleWidth"> + <number>2</number> </property> - </widget> - </item> - </layout> - </widget> - <widget class="QWidget" name="messagingPage"> - <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <layout class="QVBoxLayout" name="verticalLayout_12" stretch="0,0"> - <property name="spacing"> - <number>0</number> - </property> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <widget class="QWidget" name="messagingHeaderWidget" native="true"> - <layout class="QHBoxLayout" name="horizontalLayout_6"> - <property name="spacing"> - <number>6</number> - </property> - <property name="leftMargin"> - <number>10</number> - </property> - <property name="topMargin"> - <number>6</number> - </property> - <property name="rightMargin"> - <number>10</number> + <property name="childrenCollapsible"> + <bool>true</bool> + </property> + <widget class="QStackedWidget" name="callStackWidget"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - <property name="bottomMargin"> - <number>6</number> + <property name="currentIndex"> + <number>1</number> </property> - <item> - <widget class="QPushButton" name="imBackButton"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="toolTip"> - <string>Back to homepage button</string> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_arrow_back_24px.svg</normaloff>:/images/icons/ic_arrow_back_24px.svg</iconset> - </property> - <property name="iconSize"> - <size> - <width>18</width> - <height>18</height> - </size> - </property> - <property name="flat"> - <bool>false</bool> + <widget class="QWidget" name="videoPage"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QGridLayout" name="gridLayout_6"> + <property name="leftMargin"> + <number>0</number> </property> - </widget> - </item> - <item> - <layout class="QVBoxLayout" name="verticalLayout_14"> - <property name="spacing"> + <property name="topMargin"> <number>0</number> </property> - <property name="leftMargin"> + <property name="rightMargin"> <number>0</number> </property> <property name="bottomMargin"> <number>0</number> </property> - <item> - <widget class="QLabel" name="imNameLabel"> + <property name="spacing"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="VideoView" name="videoWidget" native="true"> <property name="sizePolicy"> <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> - <property name="minimumSize"> - <size> - <width>200</width> - <height>0</height> - </size> - </property> - <property name="font"> - <font> - <pointsize>10</pointsize> - <weight>75</weight> - <bold>true</bold> - </font> - </property> - <property name="styleSheet"> - <string notr="true">color: rgb(63,63,63);</string> - </property> - <property name="text"> - <string/> - </property> - <property name="alignment"> - <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> - </property> - <property name="wordWrap"> - <bool>false</bool> - </property> </widget> </item> - <item> - <widget class="QLabel" name="imIdLabel"> + </layout> + </widget> + <widget class="QWidget" name="outgoingCallPage"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QGridLayout" name="gridLayout_7"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <property name="spacing"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QWidget" name="outgoingCall" native="true"> <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="minimumSize"> <size> - <width>200</width> - <height>0</height> + <width>244</width> + <height>420</height> </size> </property> - <property name="font"> - <font> - <pointsize>9</pointsize> - </font> - </property> - <property name="styleSheet"> - <string notr="true">color: rgb(192,192,192);</string> - </property> - <property name="text"> - <string/> + <property name="maximumSize"> + <size> + <width>244</width> + <height>16777215</height> + </size> </property> - <property name="alignment"> - <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + <property name="baseSize"> + <size> + <width>0</width> + <height>0</height> + </size> </property> + <layout class="QVBoxLayout" name="spinnerLayout_3"> + <property name="spacing"> + <number>10</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="callingPhoto"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>166</width> + <height>166</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>166</width> + <height>166</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>166</width> + <height>166</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="callingBestNameLabel"> + <property name="font"> + <font> + <family>Segoe UI Emoji</family> + <pointsize>12</pointsize> + </font> + </property> + <property name="styleSheet"> + <string notr="true">color: rgb(128, 128, 128);</string> + </property> + <property name="text"> + <string>best name</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="callingBestIdLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="font"> + <font> + <pointsize>8</pointsize> + </font> + </property> + <property name="styleSheet"> + <string notr="true">color: rgb(174, 174, 174);</string> + </property> + <property name="text"> + <string>best Id</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="spinnerLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>20</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_6"> + <property name="spacing"> + <number>20</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="cancelButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>56</width> + <height>56</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>56</width> + <height>56</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>56</width> + <height>56</height> + </size> + </property> + <property name="toolTip"> + <string>Cancel outgoing call</string> + </property> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="icon"> + <iconset resource="ressources.qrc"> + <normaloff>:/images/icons/ic_close_white_24dp.png</normaloff>:/images/icons/ic_close_white_24dp.png</iconset> + </property> + <property name="iconSize"> + <size> + <width>24</width> + <height>24</height> + </size> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="cancelCallLabel"> + <property name="styleSheet"> + <string notr="true">color: rgb(174, 174, 174);</string> + </property> + <property name="text"> + <string>Cancel</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> </widget> </item> </layout> - </item> - <item> - <spacer name="verticalSpacer_3"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Fixed</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>0</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - <item> - <spacer name="horizontalSpacer_6"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="btnAudioCall"> - <property name="minimumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_phone_24px.svg</normaloff>:/images/icons/ic_phone_24px.svg</iconset> - </property> - <property name="iconSize"> - <size> - <width>18</width> - <height>18</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="btnVideoCall"> - <property name="minimumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_video_call_24px.svg</normaloff>:/images/icons/ic_video_call_24px.svg</iconset> - </property> - <property name="iconSize"> - <size> - <width>18</width> - <height>18</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="sendContactRequestButton"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="toolTip"> - <string>Add to contacts</string> - </property> - <property name="styleSheet"> - <string notr="true"/> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_person_add_black_24dp_2x.png</normaloff>:/images/icons/ic_person_add_black_24dp_2x.png</iconset> - </property> - <property name="iconSize"> - <size> - <width>18</width> - <height>18</height> - </size> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="messagesHBoxLayout" stretch="0,0,0"> - <property name="spacing"> - <number>0</number> - </property> - <property name="leftMargin"> - <number>10</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>10</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Minimum</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>0</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QWidget" name="messagesListWidget" native="true"> + </widget> + <widget class="QWidget" name="incomingCallPage"> <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> - <property name="minimumSize"> - <size> - <width>200</width> - <height>0</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>640</width> - <height>16777215</height> - </size> - </property> - <layout class="QVBoxLayout" name="verticalLayout_6"> - <property name="spacing"> - <number>0</number> - </property> + <layout class="QGridLayout" name="gridLayout_8"> <property name="leftMargin"> <number>0</number> </property> @@ -1302,10 +1175,13 @@ <property name="bottomMargin"> <number>0</number> </property> - <item> - <widget class="QListView" name="listMessageView"> + <property name="spacing"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QWidget" name="callInvite" native="true"> <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> @@ -1313,86 +1189,565 @@ <property name="minimumSize"> <size> <width>200</width> - <height>0</height> + <height>420</height> </size> </property> <property name="maximumSize"> <size> - <width>640</width> - <height>16777215</height> + <width>200</width> + <height>420</height> </size> </property> - <property name="font"> - <font> - <stylestrategy>PreferAntialias</stylestrategy> - </font> - </property> - <property name="focusPolicy"> - <enum>Qt::NoFocus</enum> - </property> - <property name="frameShape"> - <enum>QFrame::NoFrame</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Sunken</enum> - </property> - <property name="horizontalScrollBarPolicy"> - <enum>Qt::ScrollBarAsNeeded</enum> - </property> - <property name="verticalScrollMode"> - <enum>QAbstractItemView::ScrollPerPixel</enum> - </property> - <property name="horizontalScrollMode"> - <enum>QAbstractItemView::ScrollPerPixel</enum> - </property> - <property name="spacing"> - <number>1</number> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - <property name="selectionRectVisible"> - <bool>false</bool> + <property name="baseSize"> + <size> + <width>200</width> + <height>420</height> + </size> </property> + <layout class="QVBoxLayout" name="callInvite" stretch="0,2,1,2,1,9"> + <property name="spacing"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="callerPhoto"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>166</width> + <height>166</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>166</width> + <height>166</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>166</width> + <height>166</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_3"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item alignment="Qt::AlignHCenter"> + <widget class="QLabel" name="callerBestNameLabel"> + <property name="font"> + <font> + <family>Segoe UI Emoji</family> + <pointsize>12</pointsize> + </font> + </property> + <property name="styleSheet"> + <string notr="true">color: rgb(128, 128, 128);</string> + </property> + <property name="text"> + <string>best name</string> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="wantToTalkLabel"> + <property name="styleSheet"> + <string notr="true">color: rgb(174, 174, 174);</string> + </property> + <property name="text"> + <string>Wants to talk to you!</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer_5"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>50</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_3" stretch="2"> + <property name="spacing"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="spacing"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <item> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <property name="spacing"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QPushButton" name="acceptButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>56</width> + <height>56</height> + </size> + </property> + <property name="toolTip"> + <string>Answer incoming call button</string> + </property> + <property name="icon"> + <iconset resource="ressources.qrc"> + <normaloff>:/images/icons/ic_done_white_24dp.png</normaloff>:/images/icons/ic_done_white_24dp.png</iconset> + </property> + <property name="iconSize"> + <size> + <width>24</width> + <height>24</height> + </size> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="acceptLabel"> + <property name="styleSheet"> + <string notr="true">color: rgb(174, 174, 174);</string> + </property> + <property name="text"> + <string>Answer</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="horizontalSpacer_21"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_7"> + <property name="spacing"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <item> + <widget class="QPushButton" name="refuseButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>56</width> + <height>56</height> + </size> + </property> + <property name="toolTip"> + <string>Ignore incoming call button</string> + </property> + <property name="layoutDirection"> + <enum>Qt::RightToLeft</enum> + </property> + <property name="icon"> + <iconset resource="ressources.qrc"> + <normaloff>:/images/icons/ic_close_white_24dp.png</normaloff>:/images/icons/ic_close_white_24dp.png</iconset> + </property> + <property name="iconSize"> + <size> + <width>24</width> + <height>24</height> + </size> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QLabel" name="refuseLabel"> + <property name="layoutDirection"> + <enum>Qt::RightToLeft</enum> + </property> + <property name="styleSheet"> + <string notr="true">color: rgb(174, 174, 174);</string> + </property> + <property name="text"> + <string>Ignore</string> + </property> + <property name="alignment"> + <set>Qt::AlignCenter</set> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </item> + </layout> </widget> </item> - <item> - <layout class="QHBoxLayout" name="imSendHBoxLayout"> + </layout> + </widget> + </widget> + <widget class="QWidget" name="messagesWidget" native="true"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>358</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>16777215</height> + </size> + </property> + <layout class="QVBoxLayout" name="verticalLayout_8"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QWidget" name="messagingHeaderWidget" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_10"> + <property name="spacing"> + <number>6</number> + </property> <property name="leftMargin"> - <number>16</number> + <number>10</number> </property> <property name="topMargin"> - <number>16</number> + <number>6</number> </property> <property name="rightMargin"> - <number>6</number> + <number>10</number> </property> <property name="bottomMargin"> - <number>16</number> + <number>6</number> </property> <item> - <widget class="QLineEdit" name="imMessageEdit"> + <widget class="QPushButton" name="imBackButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="toolTip"> + <string>Back to homepage button</string> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="ressources.qrc"> + <normaloff>:/images/icons/ic_arrow_back_24px.svg</normaloff>:/images/icons/ic_arrow_back_24px.svg</iconset> + </property> + <property name="iconSize"> + <size> + <width>18</width> + <height>18</height> + </size> + </property> + <property name="flat"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_16"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="imNameLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>200</width> + <height>0</height> + </size> + </property> + <property name="font"> + <font> + <pointsize>10</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="styleSheet"> + <string notr="true">color: rgb(63,63,63);</string> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + <property name="wordWrap"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="imIdLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>200</width> + <height>0</height> + </size> + </property> + <property name="font"> + <font> + <pointsize>9</pointsize> + </font> + </property> + <property name="styleSheet"> + <string notr="true">color: rgb(192,192,192);</string> + </property> + <property name="text"> + <string/> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + </layout> + </item> + <item> + <spacer name="verticalSpacer_6"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Fixed</enum> + </property> + <property name="sizeHint" stdset="0"> <size> <width>0</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <spacer name="horizontalSpacer_13"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="btnAudioCall"> + <property name="minimumSize"> + <size> + <width>30</width> <height>30</height> </size> </property> - <property name="font"> - <font> - <pointsize>10</pointsize> - </font> + <property name="maximumSize"> + <size> + <width>30</width> + <height>30</height> + </size> </property> - <property name="inputMask"> + <property name="text"> <string/> </property> - <property name="placeholderText"> - <string>Type your message here</string> + <property name="icon"> + <iconset resource="ressources.qrc"> + <normaloff>:/images/icons/ic_phone_24px.svg</normaloff>:/images/icons/ic_phone_24px.svg</iconset> + </property> + <property name="iconSize"> + <size> + <width>18</width> + <height>18</height> + </size> </property> </widget> </item> <item> - <widget class="QPushButton" name="sendIMButton"> + <widget class="QPushButton" name="btnVideoCall"> + <property name="minimumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>30</width> + <height>30</height> + </size> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="ressources.qrc"> + <normaloff>:/images/icons/ic_video_call_24px.svg</normaloff>:/images/icons/ic_video_call_24px.svg</iconset> + </property> + <property name="iconSize"> + <size> + <width>18</width> + <height>18</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="sendContactRequestButton"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> <property name="minimumSize"> <size> <width>30</width> @@ -1406,14 +1761,17 @@ </size> </property> <property name="toolTip"> - <string>send message</string> + <string>Add to contacts</string> + </property> + <property name="styleSheet"> + <string notr="true"/> </property> <property name="text"> <string/> </property> <property name="icon"> <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_send_24px.svg</normaloff>:/images/icons/ic_send_24px.svg</iconset> + <normaloff>:/images/icons/ic_person_add_black_24dp_2x.png</normaloff>:/images/icons/ic_person_add_black_24dp_2x.png</iconset> </property> <property name="iconSize"> <size> @@ -1424,560 +1782,81 @@ </widget> </item> </layout> - </item> - </layout> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Minimum</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>0</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - </layout> - </widget> - <widget class="QWidget" name="callInvitePage"> - <layout class="QVBoxLayout" name="verticalLayout_9"> - <property name="spacing"> - <number>0</number> - </property> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item alignment="Qt::AlignHCenter|Qt::AlignVCenter"> - <widget class="QWidget" name="callInvite" native="true"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Minimum" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>166</width> - <height>0</height> - </size> - </property> - <property name="baseSize"> - <size> - <width>166</width> - <height>0</height> - </size> - </property> - <layout class="QVBoxLayout" name="verticalLayout_4"> - <property name="spacing"> - <number>0</number> - </property> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>10</number> - </property> - <item> - <layout class="QVBoxLayout" name="verticalLayout_3"> - <property name="spacing"> - <number>0</number> - </property> - <item> - <widget class="QLabel" name="callerPhoto"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>130</width> - <height>130</height> - </size> - </property> - <property name="baseSize"> - <size> - <width>130</width> - <height>130</height> - </size> - </property> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="ressources.qrc">:/images/default_avatar_overlay.svg</pixmap> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - <item alignment="Qt::AlignHCenter"> - <widget class="QLabel" name="callerIdLabel"> - <property name="styleSheet"> - <string notr="true">color: rgb(174, 174, 174);</string> - </property> - <property name="text"> - <string>Call</string> - </property> - </widget> - </item> - <item alignment="Qt::AlignHCenter"> - <widget class="QLabel" name="callerBestIdLabel"> - <property name="styleSheet"> - <string notr="true">color: rgb(174, 174, 174); font-style: italic;</string> - </property> - <property name="text"> - <string>BestId</string> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="wantToTalkLabel"> - <property name="styleSheet"> - <string notr="true">color: rgb(174, 174, 174);</string> - </property> - <property name="text"> - <string>Wants to talk to you!</string> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <property name="topMargin"> - <number>20</number> - </property> - <item> - <widget class="QPushButton" name="acceptButton"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>56</width> - <height>56</height> - </size> - </property> - <property name="toolTip"> - <string>Answer incoming call button</string> - </property> - <property name="icon"> - <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_done_white_24dp.png</normaloff>:/images/icons/ic_done_white_24dp.png</iconset> - </property> - <property name="iconSize"> - <size> - <width>24</width> - <height>24</height> - </size> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_4"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Fixed</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="refuseButton"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>56</width> - <height>56</height> - </size> - </property> - <property name="toolTip"> - <string>Ignore incoming call button</string> - </property> - <property name="layoutDirection"> - <enum>Qt::RightToLeft</enum> - </property> - <property name="icon"> - <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_close_white_24dp.png</normaloff>:/images/icons/ic_close_white_24dp.png</iconset> - </property> - <property name="iconSize"> - <size> - <width>24</width> - <height>24</height> - </size> - </property> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_7"> - <item> - <widget class="QLabel" name="acceptLabel"> - <property name="styleSheet"> - <string notr="true">color: rgb(174, 174, 174);</string> - </property> - <property name="text"> - <string>Answer</string> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_5"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeType"> - <enum>QSizePolicy::Fixed</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QLabel" name="refuseLabel"> - <property name="layoutDirection"> - <enum>Qt::RightToLeft</enum> - </property> - <property name="styleSheet"> - <string notr="true">color: rgb(174, 174, 174);</string> - </property> - <property name="text"> - <string>Ignore</string> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - <widget class="QWidget" name="outboundCallPage"> - <layout class="QVBoxLayout" name="verticalLayout_11"> - <property name="spacing"> - <number>0</number> - </property> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item alignment="Qt::AlignHCenter|Qt::AlignVCenter"> - <widget class="QWidget" name="outboundCall" native="true"> - <layout class="QVBoxLayout" name="spinnerLayout"> - <item> - <widget class="QLabel" name="callingPhoto"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>130</width> - <height>130</height> - </size> - </property> - <property name="baseSize"> - <size> - <width>130</width> - <height>130</height> - </size> - </property> - <property name="text"> - <string/> - </property> - <property name="pixmap"> - <pixmap resource="ressources.qrc">:/images/default_avatar_overlay.svg</pixmap> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="outboundCallLabel"> - <property name="styleSheet"> - <string notr="true">color: rgb(174, 174, 174);</string> - </property> - <property name="text"> - <string>Calling</string> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - <item alignment="Qt::AlignHCenter"> - <widget class="QLabel" name="spinnerLabel"> - <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>20</width> - <height>0</height> - </size> - </property> - <property name="text"> - <string/> - </property> - </widget> - </item> - <item alignment="Qt::AlignHCenter"> - <widget class="QPushButton" name="cancelButton"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>56</width> - <height>56</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>64</width> - <height>64</height> - </size> - </property> - <property name="baseSize"> - <size> - <width>64</width> - <height>64</height> - </size> - </property> - <property name="toolTip"> - <string>Cancel outgoing call</string> - </property> - <property name="layoutDirection"> - <enum>Qt::LeftToRight</enum> - </property> - <property name="icon"> - <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_close_white_24dp.png</normaloff>:/images/icons/ic_close_white_24dp.png</iconset> - </property> - <property name="iconSize"> - <size> - <width>24</width> - <height>24</height> - </size> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="cancelCallLabel"> - <property name="styleSheet"> - <string notr="true">color: rgb(174, 174, 174);</string> - </property> - <property name="text"> - <string>Cancel</string> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - <widget class="QWidget" name="videoPage"> - <layout class="QVBoxLayout" name="verticalLayout_10"> - <property name="spacing"> - <number>0</number> - </property> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <widget class="QSplitter" name="splitter_2"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <widget class="VideoView" name="videoWidget" native="true"> - <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - </widget> - <widget class="InstantMessagingWidget" name="instantMessagingWidget" native="true"> - <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="minimumSize"> - <size> - <width>0</width> - <height>0</height> - </size> - </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="messagesHBoxLayout" stretch="0,0,0"> + <property name="spacing"> + <number>0</number> + </property> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <spacer name="horizontalSpacer_5"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="MessageWebView" name="messageView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="maximumSize"> + <size> + <width>680</width> + <height>16777215</height> + </size> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_14"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Maximum</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> </widget> </widget> </item> </layout> </widget> - <widget class="QWidget" name="contactRequestPage"> - <layout class="QVBoxLayout" name="verticalLayout_8"> - <property name="leftMargin"> - <number>5</number> - </property> - <property name="topMargin"> - <number>5</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <item> - <widget class="QPushButton" name="pendingCRBackButton"> - <property name="minimumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="toolTip"> - <string>Back to homepage button</string> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_arrow_back_white_24dp.png</normaloff>:/images/icons/ic_arrow_back_white_24dp.png</iconset> - </property> - <property name="iconSize"> - <size> - <width>18</width> - <height>18</height> - </size> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_13"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </item> - <item> - <widget class="ContactRequestWidget" name="contactRequestWidget" native="true"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - </widget> - </item> - </layout> - </widget> </widget> </item> </layout> @@ -1993,10 +1872,9 @@ </widget> <customwidgets> <customwidget> - <class>InstantMessagingWidget</class> + <class>QWebEngineView</class> <extends>QWidget</extends> - <header>instantmessagingwidget.h</header> - <container>1</container> + <header location="global">QtWebEngineWidgets/QWebEngineView</header> </customwidget> <customwidget> <class>VideoView</class> @@ -2019,18 +1897,6 @@ <extends>QTreeView</extends> <header>smartlistview.h</header> </customwidget> - <customwidget> - <class>SendContactRequestWidget</class> - <extends>QWidget</extends> - <header>sendcontactrequestwidget.h</header> - <container>1</container> - </customwidget> - <customwidget> - <class>ContactRequestWidget</class> - <extends>QWidget</extends> - <header>contactrequestwidget.h</header> - <container>1</container> - </customwidget> <customwidget> <class>ConversationsFilterWidget</class> <extends>QWidget</extends> @@ -2043,9 +1909,9 @@ <header>currentaccountcombobox.h</header> </customwidget> <customwidget> - <class>ConversationFilterButton</class> - <extends>QWidget</extends> - <header>conversationfilterbutton.h</header> + <class>MessageWebView</class> + <extends>QWebEngineView</extends> + <header>messagewebview.h</header> </customwidget> </customwidgets> <resources> diff --git a/conversationitemdelegate.cpp b/conversationitemdelegate.cpp index 7ddb014815c9b22e10342346a548b713756cb953..9c3f1f41db085346123b208bab5d2c0e92227126 100644 --- a/conversationitemdelegate.cpp +++ b/conversationitemdelegate.cpp @@ -27,11 +27,12 @@ #include "smartlistmodel.h" #include "ringthemeutils.h" #include "utils.h" +#include "lrcinstance.h" #include <ciso646> -ConversationItemDelegate::ConversationItemDelegate(QObject* parent) : - QItemDelegate(parent) +ConversationItemDelegate::ConversationItemDelegate(QObject* parent) + : QItemDelegate(parent) { } @@ -42,7 +43,7 @@ ConversationItemDelegate::paint(QPainter* painter ) const { QStyleOptionViewItem opt(option); - painter->setRenderHint(QPainter::Antialiasing); + painter->setRenderHint(QPainter::Antialiasing, true); // Not having focus removes dotted lines around the item if (opt.state & QStyle::State_HasFocus) @@ -139,8 +140,10 @@ ConversationItemDelegate::paint(QPainter* painter QSize ConversationItemDelegate::sizeHint(const QStyleOptionViewItem& option, - const QModelIndex& index) const + const QModelIndex& index) const { + Q_UNUSED(option); + Q_UNUSED(index); return QSize(0, cellHeight_); } @@ -150,6 +153,7 @@ ConversationItemDelegate::paintRingConversationItem(QPainter* painter, const QRect& rect, const QModelIndex& index) const { + Q_UNUSED(option); QFont font(painter->font()); font.setPointSize(fontSize_); QPen pen(painter->pen()); @@ -221,18 +225,37 @@ ConversationItemDelegate::paintRingConversationItem(QPainter* painter, // bottom-right: last interaction snippet QString interactionStr = index.data(static_cast<int>(SmartListModel::Role::LastInteraction)).value<QString>(); if (!interactionStr.isNull()) { - // remove phone glyphs - interactionStr.replace(QChar(0xd83d), ""); - interactionStr.replace(QChar(0xdd7d), ""); - interactionStr.replace(QChar(0xdcde), ""); - - font.setItalic(false); - font.setBold(false); - pen.setColor(RingTheme::grey_); - painter->setPen(pen); - painter->setFont(font); + painter->save(); + interactionStr = interactionStr.simplified(); + auto type = Utils::toEnum<lrc::api::interaction::Type>(index + .data(static_cast<int>(SmartListModel::Role::LastInteractionType)) + .value<int>()); + if (type == lrc::api::interaction::Type::CALL || + type == lrc::api::interaction::Type::CONTACT) { + font.setItalic(false); + font.setBold(false); + pen.setColor(RingTheme::grey_.darker(140)); + painter->setPen(pen); + painter->setFont(font); + // strip emojis if it's a call/contact type message + VectorUInt emojiless; + for (auto unicode : interactionStr.toUcs4()) { + if (!(unicode >= 0x1F000 && unicode <= 0x1FFFF)) { + emojiless.push_back(unicode); + } + } + interactionStr = QString::fromUcs4(&emojiless.at(0), emojiless.size()); + } else { + QFont emojiMsgFont(QStringLiteral("Segoe UI Emoji")); + emojiMsgFont.setItalic(false); + emojiMsgFont.setBold(false); + emojiMsgFont.setPointSize(fontSize_); + painter->setOpacity(0.7); + painter->setFont(emojiMsgFont); + } interactionStr = fontMetrics.elidedText(interactionStr, Qt::ElideRight, rectInfo2.width()); painter->drawText(rectInfo2, Qt::AlignVCenter | Qt::AlignRight, interactionStr); + painter->restore(); } } @@ -268,7 +291,7 @@ ConversationItemDelegate::paintRingInviteConversationItem(QPainter* painter, QFontMetrics fontMetrics(font); // The name is displayed at the avatar's right - QString nameStr = index.data(static_cast<int>(SmartListModel::Role::DisplayName)).value<QString>();; + QString nameStr = index.data(static_cast<int>(SmartListModel::Role::DisplayName)).value<QString>(); if (!nameStr.isNull()) { font.setItalic(false); font.setBold(true); @@ -293,6 +316,11 @@ ConversationItemDelegate::paintRingInviteConversationItem(QPainter* painter, } void -ConversationItemDelegate::paintSIPConversationItem(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const +ConversationItemDelegate::paintSIPConversationItem(QPainter* painter, + const QStyleOptionViewItem& option, + const QModelIndex& index) const { + Q_UNUSED(painter); + Q_UNUSED(option); + Q_UNUSED(index); } diff --git a/conversationsfilterwidget.cpp b/conversationsfilterwidget.cpp index 07abdf517c91e3a9a85a7db96548827e2e3e9c5a..5a34e53e2bd7311d55c68043eea4af1618bf85dc 100644 --- a/conversationsfilterwidget.cpp +++ b/conversationsfilterwidget.cpp @@ -18,9 +18,32 @@ #include "conversationsfilterwidget.h" +#include <QPainter> +#include <QDebug> + #include "ringthemeutils.h" +#include "lrcinstance.h" -#include <QPainter> +ConversationsFilterWidget::ConversationsFilterWidget(QWidget *parent) + : QWidget(parent) +{ +} + +void ConversationsFilterWidget::resizeEvent(QResizeEvent * event) +{ + using namespace lrc::api::profile; + updateNotifier(Type::RING); + updateNotifier(Type::PENDING); +} + +void +ConversationsFilterWidget::updateNotifier(lrc::api::profile::Type typeFilter) +{ + using namespace lrc::api::profile; + handleNotifierOverlay((typeFilter == Type::RING) ? "btnConversations" : "btnInvites", + (typeFilter == Type::RING) ? unreadMessagesNotifier_ : pendingInvitesNotifier_, + typeFilter); +} static inline const QRect getNotifierRect(const QRect& buttonRect) @@ -32,8 +55,8 @@ getNotifierRect(const QRect& buttonRect) void ConversationsFilterWidget::handleNotifierOverlay(const QString& buttonName, - SmartlistSelectorButtonNotifier*& notifier, - lrc::api::profile::Type filter) + SmartlistSelectorButtonNotifier*& notifier, + lrc::api::profile::Type filter) { auto button = this->findChild<QPushButton*>(buttonName); if (!button) { @@ -49,18 +72,4 @@ ConversationsFilterWidget::handleNotifierOverlay(const QString& buttonName, notifier->setGeometry(getNotifierRect(button->frameGeometry())); notifier->show(); } -} - -ConversationsFilterWidget::ConversationsFilterWidget(QWidget *parent) - : QWidget(parent) -{ -} - -void ConversationsFilterWidget::paintEvent(QPaintEvent * event) -{ - QWidget::paintEvent(event); - - using namespace lrc::api::profile; - handleNotifierOverlay("buttonConversations", unreadMessagesNotifier_, Type::RING); - handleNotifierOverlay("buttonInvites", pendingInvitesNotifier_, Type::PENDING); -} +} \ No newline at end of file diff --git a/conversationsfilterwidget.h b/conversationsfilterwidget.h index 1ce9eefe69938b0aa8e1f24a325c5e76c74909b8..35a788f22263f274b03393c575f25482ddd38395 100644 --- a/conversationsfilterwidget.h +++ b/conversationsfilterwidget.h @@ -22,20 +22,21 @@ #include <QWidget> -class ConversationsFilterWidget : public QWidget -{ - Q_OBJECT - -public: - explicit ConversationsFilterWidget(QWidget *parent = 0); - -protected: - virtual void paintEvent(QPaintEvent *event); - -private: +class ConversationsFilterWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ConversationsFilterWidget(QWidget *parent = 0); + +protected: + void resizeEvent(QResizeEvent * event); + +private: + void updateNotifier(lrc::api::profile::Type typeFilter); void handleNotifierOverlay(const QString& buttonName, SmartlistSelectorButtonNotifier*& notifier, - lrc::api::profile::Type filter); - SmartlistSelectorButtonNotifier* unreadMessagesNotifier_{ nullptr }; - SmartlistSelectorButtonNotifier* pendingInvitesNotifier_{ nullptr }; + lrc::api::profile::Type filter); + SmartlistSelectorButtonNotifier* unreadMessagesNotifier_{ nullptr }; + SmartlistSelectorButtonNotifier* pendingInvitesNotifier_{ nullptr }; }; \ No newline at end of file diff --git a/currentaccountcombobox.cpp b/currentaccountcombobox.cpp index e17323ba94007fee8e25b3717c565350b6fece07..59f0b6783fa41878e3a3a2419c460b10d726d2f0 100644 --- a/currentaccountcombobox.cpp +++ b/currentaccountcombobox.cpp @@ -60,8 +60,10 @@ CurrentAccountComboBox::CurrentAccountComboBox(QWidget* parent) }); gearPixmap_.load(":/images/icons/round-settings-24px.svg"); + gearLabel_.setPixmap(gearPixmap_); gearLabel_.setParent(this); gearLabel_.setStyleSheet("background: transparent;"); + setupSettingsButton(); } CurrentAccountComboBox::~CurrentAccountComboBox() @@ -74,19 +76,13 @@ CurrentAccountComboBox::paintEvent(QPaintEvent* e) { Q_UNUSED(e); - gearPoint_.setX(this->width() - gearSize_ - 4 * gearBorder_); - gearPoint_.setY(this->height() / 2 - gearLabel_.height() / 2 - 2 * gearBorder_); - gearLabel_.setGeometry(gearPoint_.x() - 3, gearPoint_.y(), - gearSize_ + 2 * gearBorder_, gearSize_ + 2 * gearBorder_); - gearLabel_.setMargin(gearBorder_); - QPoint p(12, 2); QPainter painter(this); painter.setRenderHints((QPainter::Antialiasing | QPainter::TextAntialiasing), true); QStyleOption opt; opt.init(this); - style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter); // create box in which to draw avatar and presence indicator QRect avatarRect(2, 2, cellHeight_, cellHeight_); // [screen awareness] @@ -136,10 +132,21 @@ CurrentAccountComboBox::paintEvent(QPaintEvent* e) painter.setPen(Qt::lightGray); painter.drawText(comboBoxRect, (Qt::AlignBottom | Qt::AlignLeft), secondaryAccountID); } +} - this->setEnabled(LRCInstance::accountModel().getAccountList().size() > 1); +void CurrentAccountComboBox::resizeEvent(QResizeEvent * event) +{ + setupSettingsButton(); +} - gearLabel_.setPixmap(gearPixmap_); +void +CurrentAccountComboBox::setupSettingsButton() +{ + gearPoint_.setX(this->width() - gearSize_ - 4 * gearBorder_); + gearPoint_.setY(this->height() / 2 - gearLabel_.height() / 2 - 2 * gearBorder_); + gearLabel_.setGeometry(gearPoint_.x() - 3, gearPoint_.y(), + gearSize_ + 2 * gearBorder_, gearSize_ + 2 * gearBorder_); + gearLabel_.setMargin(gearBorder_); } // import account background account pixmap and scale pixmap to fit in label @@ -191,7 +198,7 @@ CurrentAccountComboBox::mouseMoveEvent(QMouseEvent* mouseEvent) void CurrentAccountComboBox::showPopup() { - gearPixmap_.load(""); + gearLabel_.hide(); popupPresent = true; QComboBox::showPopup(); } @@ -199,7 +206,7 @@ CurrentAccountComboBox::showPopup() void CurrentAccountComboBox::hidePopup() { - gearPixmap_.load(":/images/icons/round-settings-24px.svg"); + gearLabel_.show(); popupPresent = false; QComboBox::hidePopup(); diff --git a/currentaccountcombobox.h b/currentaccountcombobox.h index df0367aec2245709bf6719d02c139d6d78cbbc40..586d911d60546487c8280e7744e52a43e0ca005e 100644 --- a/currentaccountcombobox.h +++ b/currentaccountcombobox.h @@ -34,20 +34,22 @@ class CurrentAccountComboBox : public QComboBox public: explicit CurrentAccountComboBox(QWidget* parent = nullptr); ~CurrentAccountComboBox(); - void accountListUpdate(); - void setCurrentIndex(int index); + void accountListUpdate(); + void setCurrentIndex(int index); -signals: - void settingsButtonClicked(); +signals: + void settingsButtonClicked(); -private: +protected: void paintEvent(QPaintEvent* e); - void importLabelPhoto(int index); + void resizeEvent(QResizeEvent *event); void mousePressEvent(QMouseEvent* mouseEvent); - - void mouseMoveEvent(QMouseEvent* event); + void mouseMoveEvent(QMouseEvent* event); void leaveEvent(QEvent * event); +private: + void importLabelPhoto(int index); + void setupSettingsButton(); void showPopup(); void hidePopup(); diff --git a/fetch-deps.bat b/fetch-deps.bat new file mode 100644 index 0000000000000000000000000000000000000000..052af8b8173f9e7c8e92b2697622f9385b583065 --- /dev/null +++ b/fetch-deps.bat @@ -0,0 +1,27 @@ +@echo off +setlocal EnableDelayedExpansion + +set cloneSubmodules=N +if "%1" == "/c" ( + set cloneSubmodules=Y +) + +if not exist "winsparkle" ( + git clone --depth=1 https://github.com/vslavik/winsparkle.git +) + +if "%cloneSubmodules%" neq "N" ( + cd winsparkle + git submodule init + git submodule update + cd .. +) + +if not exist "qrencode-win32" ( + git clone --depth=1 https://github.com/BlueDragon747/qrencode-win32.git +) + +:cleanup +endlocal +@endlocal +exit /B %ERRORLEVEL% \ No newline at end of file diff --git a/fetch-qrencodewin32.bat b/fetch-qrencodewin32.bat deleted file mode 100644 index fb5f5eb2c31920d9292877876dea7236ac4ad753..0000000000000000000000000000000000000000 --- a/fetch-qrencodewin32.bat +++ /dev/null @@ -1,45 +0,0 @@ -@echo off -setlocal EnableDelayedExpansion - -set SRC=%~dp0 - -set MSBUILD_ARGS=/nologo /p:useenv=true /p:Configuration=Release-Lib /p:Platform=x64 /verbosity:normal /maxcpucount:%NUMBER_OF_PROCESSORS% - -@setlocal - -set VSInstallerFolder="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer" -if %PROCESSOR_ARCHITECTURE%==x86 set VSInstallerFolder="%ProgramFiles%\Microsoft Visual Studio\Installer" - -pushd %VSInstallerFolder% -for /f "usebackq tokens=*" %%i in (`vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do ( - set VSLATESTDIR=%%i -) -popd - -echo VS Installation folder: %VSLATESTDIR% - -if not exist "%VSLATESTDIR%\VC\Auxiliary\Build\vcvarsall.bat" ( - echo: - echo VSInstallDir not found or not installed correctly. - goto cleanup -) - -if %PROCESSOR_ARCHITECTURE%==x86 ( - set Comp_x86=x86 10.0.15063.0 - set Comp_x64=x86_amd64 10.0.15063.0 -) else ( - set Comp_x86=amd64_x86 10.0.15063.0 - set Comp_x64=amd64 10.0.15063.0 -) - -set path=%path:"=% -call "%VSLATESTDIR%"\\VC\\Auxiliary\\Build\\vcvarsall.bat %Comp_x64% - -git clone --depth=1 https://github.com/BlueDragon747/qrencode-win32.git - -msbuild qrencode-win32/qrencode-win32/vc8/qrcodelib/qrcodelib.vcxproj %MSBUILD_ARGS% - -:cleanup -endlocal -@endlocal -exit /B %ERRORLEVEL% \ No newline at end of file diff --git a/fetch-winsparkle.bat b/fetch-winsparkle.bat deleted file mode 100644 index e50cc6d42b64a38ca717c0bcf4a44041d5fd3f09..0000000000000000000000000000000000000000 --- a/fetch-winsparkle.bat +++ /dev/null @@ -1,45 +0,0 @@ -@echo off -setlocal EnableDelayedExpansion - -set SRC=%~dp0 - -set MSBUILD_ARGS=/nologo /p:useenv=true /p:Configuration=Release /p:Platform=x64 /verbosity:normal /maxcpucount:%NUMBER_OF_PROCESSORS% - -@setlocal - -set VSInstallerFolder="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer" -if %PROCESSOR_ARCHITECTURE%==x86 set VSInstallerFolder="%ProgramFiles%\Microsoft Visual Studio\Installer" - -pushd %VSInstallerFolder% -for /f "usebackq tokens=*" %%i in (`vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do ( - set VSLATESTDIR=%%i -) -popd - -echo VS Installation folder: %VSLATESTDIR% - -if not exist "%VSLATESTDIR%\VC\Auxiliary\Build\vcvarsall.bat" ( - echo: - echo VSInstallDir not found or not installed correctly. - goto cleanup -) - -if %PROCESSOR_ARCHITECTURE%==x86 ( - set Comp_x86=x86 10.0.15063.0 - set Comp_x64=x86_amd64 10.0.15063.0 -) else ( - set Comp_x86=amd64_x86 10.0.15063.0 - set Comp_x64=amd64 10.0.15063.0 -) - -set path=%path:"=% -call "%VSLATESTDIR%"\\VC\\Auxiliary\\Build\\vcvarsall.bat %Comp_x64% - -git clone --depth=1 https://github.com/vslavik/winsparkle.git - -msbuild winsparkle/WinSparkle-2015.vcxproj %MSBUILD_ARGS% - -:cleanup -endlocal -@endlocal -exit /B %ERRORLEVEL% \ No newline at end of file diff --git a/imdelegate.cpp b/imdelegate.cpp deleted file mode 100644 index 83ca35b7e9d136e9519b29b4b8dc3a797b024fdf..0000000000000000000000000000000000000000 --- a/imdelegate.cpp +++ /dev/null @@ -1,191 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2015-2018 by Savoir-faire Linux * - * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>* - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * - * * - * 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/>. * - **************************************************************************/ - -#include "imdelegate.h" - -#include <QApplication> -#include <QSettings> -#include <QDateTime> - -#include "media/text.h" -#include "media/textrecording.h" - -#include "ringthemeutils.h" -#include "settingskey.h" -#include "messagemodel.h" -#include "utils.h" - -ImDelegate::ImDelegate(QObject *parent) - : QItemDelegate(parent) -{ -} - -void -ImDelegate::formatMsg(const QModelIndex& index, QString& msgString) const -{ - auto date = index.data(static_cast<int>(MessageModel::Role::InteractionDate)).value<QDateTime>(); - auto now = QDateTime::currentDateTime(); - QString dateString; - if (now.date() == date.date()) { - dateString = date.time().toString(); - } else { - dateString = date.toString(); - } - msgString = QString("%1<br><footer><i>%2</i></footer>").arg(msgString, dateString); -} - -void -ImDelegate::paint(QPainter* painter, - const QStyleOptionViewItem& option, - const QModelIndex& index) const -{ - if (!index.isValid()) { - return; - } - - auto msg = index.data(static_cast<int>(MessageModel::Role::Body)).toString(); - auto type = static_cast<lrc::api::interaction::Type>(index.data(static_cast<int>(MessageModel::Role::Type)).value<int>()); - auto isOutgoing = index.data(static_cast<int>(MessageModel::Role::Direction)).value<bool>(); - auto isGenerated = Utils::isInteractionGenerated(type); - auto dir = isGenerated ? Qt::AlignHCenter : (isOutgoing ? Qt::AlignRight : Qt::AlignLeft); - - QStyleOptionViewItem opt = option; - - painter->setRenderHint(QPainter::Antialiasing); - - opt.font = fontMsg_; - painter->setFont(fontMsg_); - - opt.text.clear(); - - formatMsg(index, msg); - - QTextDocument document; - document.setDefaultStyleSheet(defaultStylesheet_); - document.setDefaultFont(fontMsg_); - document.setHtml(msg); - auto textOptions = QTextOption(Qt::AlignLeft); - textOptions.setWrapMode(QTextOption::WrapMode::WordWrap); - document.setDefaultTextOption(textOptions); - - QRect textRect = getBoundingRect(dir, opt, document); - document.setTextWidth(textRect.width()); - - if (dir == Qt::AlignLeft) { - // avatar - opt.decorationSize = QSize(sizeImage_, sizeImage_); - opt.decorationPosition = QStyleOptionViewItem::Left; - opt.decorationAlignment = Qt::AlignCenter; - QRect rectAvatar(margin_ + opt.rect.left(), - margin_ + opt.rect.top(), - sizeImage_, sizeImage_); - drawDecoration(painter, opt, rectAvatar, - QPixmap::fromImage(index.data(Qt::DecorationRole).value<QImage>()) - .scaled(sizeImage_, sizeImage_, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - } else { - opt.decorationSize = QSize(); - opt.decorationPosition = QStyleOptionViewItem::Right; - } - - // message bubble - QPainterPath path; - path.addRoundedRect(textRect, bubbleRadius_, bubbleRadius_); - - if (dir == Qt::AlignRight) { - painter->fillPath(path, RingTheme::imGrey_); - } else if (dir == Qt::AlignHCenter) { - painter->fillPath(path, Qt::transparent); - } else { - painter->fillPath(path, RingTheme::imBlue_); - } - - painter->save(); - - // text - painter->translate(textRect.topLeft()); - document.drawContents(painter); - painter->restore(); -} - -QRect ImDelegate::getBoundingRect(const Qt::AlignmentFlag& dir, - const QStyleOptionViewItem &option, - QTextDocument& txtDoc) const -{ - QRect textRect; - - if (dir == Qt::AlignLeft) { - txtDoc.setTextWidth(option.rect.width() - sizeImage_ - padding_); - textRect.setRect(option.rect.left() + sizeImage_ + padding_, - option.rect.top() + padding_, - txtDoc.idealWidth(), - txtDoc.size().height()); - } else if (dir == Qt::AlignHCenter) { - txtDoc.setTextWidth(option.rect.width() - padding_); - auto optCenter = option.rect.left() + option.rect.width() / 2; - textRect.setRect(optCenter - txtDoc.idealWidth() / 2, - option.rect.top() + padding_, - txtDoc.idealWidth(), - txtDoc.size().height()); - } else { - txtDoc.setTextWidth(option.rect.width() - padding_); - textRect.setRect(option.rect.right() - padding_ - txtDoc.idealWidth(), - option.rect.top() + padding_, - txtDoc.idealWidth(), - txtDoc.size().height()); - } - return textRect; -} - -QSize -ImDelegate::sizeHint(const QStyleOptionViewItem& option, - const QModelIndex& index) const -{ - QStyleOptionViewItem opt = option; - opt.font = fontMsg_; - - QString msg = index.data(static_cast<int>(media::TextRecording::Role::FormattedHtml)).toString(); - - auto isOutgoing = index.data(static_cast<int>(MessageModel::Role::Direction)).value<bool>(); - auto isGenerated = Utils::isInteractionGenerated( - static_cast<lrc::api::interaction::Type>(index.data(static_cast<int>(MessageModel::Role::Type)).value<int>()) - ); - auto dir = isGenerated ? Qt::AlignHCenter : (isOutgoing ? Qt::AlignRight : Qt::AlignLeft); - - formatMsg(index, msg); - - QTextDocument document; - document.setDefaultFont(fontMsg_); - document.setHtml(msg); - auto textOptions = QTextOption(Qt::AlignLeft); - textOptions.setWrapMode(QTextOption::WrapMode::WordWrap); - document.setDefaultTextOption(textOptions); - - QRect boundingRect = getBoundingRect(dir, opt, document); - - QSize size(boundingRect.width() + 2 * margin_, boundingRect.height()); - - /* Keep the minimum height needed. */ - if(size.height() < sizeImage_) - size.setHeight(sizeImage_); - - size.setHeight(size.height() + 2 * margin_); - - return size; -} - diff --git a/imdelegate.h b/imdelegate.h deleted file mode 100644 index b05eb4e0edce172456b6efc26ee3ffb49af8b551..0000000000000000000000000000000000000000 --- a/imdelegate.h +++ /dev/null @@ -1,46 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2015-2017 by Savoir-faire Linux * - * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>* - * * - * 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/>. * - **************************************************************************/ - -#pragma once - -#include <QPainter> -#include <QTextDocument> -#include <QItemDelegate> - -class ImDelegate : public QItemDelegate -{ - Q_OBJECT -public: - explicit ImDelegate(QObject *parent = 0); - -protected: - void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; - QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const; -private: - void formatMsg(const QModelIndex& index, QString& msg) const; - QRect getBoundingRect(const Qt::AlignmentFlag& dir, const QStyleOptionViewItem &option, - QTextDocument& txtDoc) const; - - const QFont fontMsg_ = QFont("Arial", 10); - const QString defaultStylesheet_ = QString("body { color : black; } i { opacity: 100; font-size : 10px; text-align : right; }"); - constexpr static int sizeImage_ = 38; - constexpr static int margin_ = 5; - constexpr static int padding_ = 5; - constexpr static int bubbleRadius_ = 12; -}; - diff --git a/instantmessagingwidget.cpp b/instantmessagingwidget.cpp deleted file mode 100644 index add65b849254e6cb4278507f66d77c936b40c223..0000000000000000000000000000000000000000 --- a/instantmessagingwidget.cpp +++ /dev/null @@ -1,154 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2015-2018 by Savoir-faire Linux * - * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>* - * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * - * * - * 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/>. * - **************************************************************************/ - -#include "instantmessagingwidget.h" -#include "ui_instantmessagingwidget.h" - -#include <QApplication> -#include <QClipboard> -#include <QMenu> - -#include "media/text.h" -#include "media/textrecording.h" - -#include "imdelegate.h" -#include "globalsystemtray.h" -#include "settingskey.h" -#include "lrcinstance.h" -#include "utils.h" - -InstantMessagingWidget::InstantMessagingWidget(QWidget *parent) : - QWidget(parent), - ui(new Ui::InstantMessagingWidget) -{ - ui->setupUi(this); - - this->hide(); - - imDelegate_ = new ImDelegate(); - ui->listMessageView->setItemDelegate(imDelegate_); - ui->listMessageView->setContextMenuPolicy(Qt::ActionsContextMenu); - auto copyAction = new QAction(tr("Copy"), this); - ui->listMessageView->addAction(copyAction); - connect(copyAction, &QAction::triggered, [=]() { - copyToClipboard(); - }); -} - -InstantMessagingWidget::~InstantMessagingWidget() -{ - delete ui; - delete imDelegate_; -} - -void -InstantMessagingWidget::setupCallMessaging(const std::string& callId, - MessageModel *messageModel) -{ - ui->listMessageView->disconnect(); - ui->messageEdit->disconnect(); - if (messageModel == nullptr) { - return; - } - - using namespace lrc::api; - - ui->listMessageView->setModel(messageModel); - ui->listMessageView->scrollToBottom(); - connect(ui->messageEdit, &QLineEdit::returnPressed, - [=]() { - auto msg = ui->messageEdit->text(); - if (msg.trimmed().isEmpty()) { - return; - } - ui->messageEdit->clear(); - try { - auto selectedConvUid = LRCInstance::getSelectedConvUid(); - LRCInstance::getCurrentConversationModel()->sendMessage(selectedConvUid, msg.toStdString()); - } - catch (...) { - qDebug() << "exception when sending message"; - } - ui->listMessageView->scrollToBottom(); - }); - - QObject::disconnect(newInteractionConnection_); - newInteractionConnection_ = QObject::connect(LRCInstance::getCurrentConversationModel(), &ConversationModel::newInteraction, - [this](const std::string& convUid, uint64_t interactionId, const lrc::api::interaction::Info& interaction) { - onIncomingMessage(convUid, interactionId, interaction); - }); -} - -void -InstantMessagingWidget::keyPressEvent(QKeyEvent *event) -{ - if (event->matches(QKeySequence::Copy)) { - copyToClipboard(); - } -} - -void -InstantMessagingWidget::showEvent(QShowEvent *event) -{ - Q_UNUSED(event) - ui->messageEdit->setFocus(); -} - -void -InstantMessagingWidget::copyToClipboard() -{ - auto idx = ui->listMessageView->currentIndex(); - if (idx.isValid()) { - auto text = ui->listMessageView->model()->data(idx); - - QApplication::clipboard()->setText(text.value<QString>()); - } -} - -void -InstantMessagingWidget::updateConversationView(const std::string & convUid) -{ - auto& currentAccountInfo = LRCInstance::getCurrentAccountInfo(); - auto currentConversationModel = currentAccountInfo.conversationModel.get(); - currentConversationModel->clearUnreadInteractions(convUid); - auto currentConversation = Utils::getConversationFromUid(convUid, *currentConversationModel); - if (currentConversation == currentConversationModel->allFilteredConversations().end()) { - return; - } - messageModel_.reset(new MessageModel(*currentConversation, currentAccountInfo, this->parent())); - ui->listMessageView->setModel(messageModel_.get()); - ui->listMessageView->scrollToBottom(); - this->show(); -} - -void -InstantMessagingWidget::on_sendButton_clicked() -{ - emit ui->messageEdit->returnPressed(); -} - -void -InstantMessagingWidget::onIncomingMessage(const std::string& convUid, uint64_t interactionId, const lrc::api::interaction::Info& interaction) -{ - if (!QApplication::activeWindow() && settings_.value(SettingsKey::enableNotifications).toBool()) { - GlobalSystemTray::instance().showMessage("Ring: Message Received", QString::fromStdString(interaction.body)); - QApplication::alert(this, 5000); - } - updateConversationView(convUid); -} diff --git a/instantmessagingwidget.ui b/instantmessagingwidget.ui deleted file mode 100644 index 9dd2becd0c79a68ddea79780467a80f72dd4e000..0000000000000000000000000000000000000000 --- a/instantmessagingwidget.ui +++ /dev/null @@ -1,124 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>InstantMessagingWidget</class> - <widget class="QWidget" name="InstantMessagingWidget"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>400</width> - <height>300</height> - </rect> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="windowTitle"> - <string/> - </property> - <layout class="QVBoxLayout" name="verticalLayout"> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <widget class="QListView" name="listMessageView"> - <property name="toolTip"> - <string>Message list</string> - </property> - <property name="frameShape"> - <enum>QFrame::NoFrame</enum> - </property> - <property name="verticalScrollMode"> - <enum>QAbstractItemView::ScrollPerPixel</enum> - </property> - <property name="horizontalScrollMode"> - <enum>QAbstractItemView::ScrollPerPixel</enum> - </property> - </widget> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <property name="leftMargin"> - <number>6</number> - </property> - <property name="topMargin"> - <number>6</number> - </property> - <property name="rightMargin"> - <number>6</number> - </property> - <property name="bottomMargin"> - <number>6</number> - </property> - <item> - <widget class="QLineEdit" name="messageEdit"> - <property name="minimumSize"> - <size> - <width>0</width> - <height>30</height> - </size> - </property> - <property name="toolTip"> - <string>Message input</string> - </property> - <property name="placeholderText"> - <string>Send text message...</string> - </property> - <property name="clearButtonEnabled"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="sendButton"> - <property name="minimumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="maximumSize"> - <size> - <width>30</width> - <height>30</height> - </size> - </property> - <property name="toolTip"> - <string>Send message button</string> - </property> - <property name="text"> - <string/> - </property> - <property name="icon"> - <iconset resource="ressources.qrc"> - <normaloff>:/images/icons/ic_send_white_24dp.png</normaloff>:/images/icons/ic_send_white_24dp.png</iconset> - </property> - <property name="iconSize"> - <size> - <width>18</width> - <height>18</height> - </size> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - <resources> - <include location="ressources.qrc"/> - </resources> - <connections/> -</ui> diff --git a/invitebuttonswidget.cpp b/invitebuttonswidget.cpp index ee18f90c02679c1d97d79068c6f15f1688b818a9..cbe2504ddea19954b66c415c7adca690367c4629 100644 --- a/invitebuttonswidget.cpp +++ b/invitebuttonswidget.cpp @@ -37,11 +37,10 @@ InviteButtonsWidget::InviteButtonsWidget(QWidget* parent) : [=]() { emit btnBlockInviteClicked(); }); - } InviteButtonsWidget::~InviteButtonsWidget() { disconnect(this); delete ui; -} \ No newline at end of file +} diff --git a/invitebuttonswidget.h b/invitebuttonswidget.h index 7e1919bae1307a8e14a3ec5a36ad89ecb5dddd3b..7f53a571247536ffdb599fd92867b3b012548538 100644 --- a/invitebuttonswidget.h +++ b/invitebuttonswidget.h @@ -19,6 +19,7 @@ #pragma once #include <QWidget> +#include <QItemDelegate> namespace Ui { class InviteButtonsWidget; diff --git a/linkify.js b/linkify.js new file mode 100644 index 0000000000000000000000000000000000000000..8da2f895295fdc7222e93a614a51532457aab6e5 --- /dev/null +++ b/linkify.js @@ -0,0 +1,1271 @@ +/* + * Copyright (c) 2016 SoapBox Innovations Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. +*/ + +;(function () { +'use strict'; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; + +(function (exports) { + 'use strict'; + + function inherits(parent, child) { + var props = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; + + var extended = Object.create(parent.prototype); + for (var p in props) { + extended[p] = props[p]; + } + extended.constructor = child; + child.prototype = extended; + return child; + } + + var defaults = { + defaultProtocol: 'http', + events: null, + format: noop, + formatHref: noop, + nl2br: false, + tagName: 'a', + target: typeToTarget, + validate: true, + ignoreTags: [], + attributes: null, + className: 'linkified' }; + + function Options(opts) { + opts = opts || {}; + + this.defaultProtocol = opts.defaultProtocol || defaults.defaultProtocol; + this.events = opts.events || defaults.events; + this.format = opts.format || defaults.format; + this.formatHref = opts.formatHref || defaults.formatHref; + this.nl2br = opts.nl2br || defaults.nl2br; + this.tagName = opts.tagName || defaults.tagName; + this.target = opts.target || defaults.target; + this.validate = opts.validate || defaults.validate; + this.ignoreTags = []; + + // linkAttributes and linkClass is deprecated + this.attributes = opts.attributes || opts.linkAttributes || defaults.attributes; + this.className = opts.className || opts.linkClass || defaults.className; + + // Make all tags names upper case + + var ignoredTags = opts.ignoreTags || defaults.ignoreTags; + for (var i = 0; i < ignoredTags.length; i++) { + this.ignoreTags.push(ignoredTags[i].toUpperCase()); + } + } + + Options.prototype = { + /** + * Given the token, return all options for how it should be displayed + */ + resolve: function resolve(token) { + var href = token.toHref(this.defaultProtocol); + return { + formatted: this.get('format', token.toString(), token), + formattedHref: this.get('formatHref', href, token), + tagName: this.get('tagName', href, token), + className: this.get('className', href, token), + target: this.get('target', href, token), + events: this.getObject('events', href, token), + attributes: this.getObject('attributes', href, token) + }; + }, + + + /** + * Returns true or false based on whether a token should be displayed as a + * link based on the user options. By default, + */ + check: function check(token) { + return this.get('validate', token.toString(), token); + }, + + + // Private methods + + /** + * Resolve an option's value based on the value of the option and the given + * params. + * @param [String] key Name of option to use + * @param operator will be passed to the target option if it's method + * @param [MultiToken] token The token from linkify.tokenize + */ + get: function get(key, operator, token) { + var option = this[key]; + + if (!option) { + return option; + } + + switch (typeof option === 'undefined' ? 'undefined' : _typeof(option)) { + case 'function': + return option(operator, token.type); + case 'object': + var optionValue = option[token.type] || defaults[key]; + return typeof optionValue === 'function' ? optionValue(operator, token.type) : optionValue; + } + + return option; + }, + getObject: function getObject(key, operator, token) { + var option = this[key]; + return typeof option === 'function' ? option(operator, token.type) : option; + } + }; + + /** + * Quick indexOf replacement for checking the ignoreTags option + */ + function contains(arr, value) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] === value) { + return true; + } + } + return false; + } + + function noop(val) { + return val; + } + + function typeToTarget(href, type) { + return type === 'url' ? '_blank' : null; + } + + var options = Object.freeze({ + defaults: defaults, + Options: Options, + contains: contains + }); + + function createStateClass() { + return function (tClass) { + this.j = []; + this.T = tClass || null; + }; + } + + /** + A simple state machine that can emit token classes + + The `j` property in this class refers to state jumps. It's a + multidimensional array where for each element: + + * index [0] is a symbol or class of symbols to transition to. + * index [1] is a State instance which matches + + The type of symbol will depend on the target implementation for this class. + In Linkify, we have a two-stage scanner. Each stage uses this state machine + but with a slighly different (polymorphic) implementation. + + The `T` property refers to the token class. + + TODO: Can the `on` and `next` methods be combined? + + @class BaseState + */ + var BaseState = createStateClass(); + BaseState.prototype = { + defaultTransition: false, + + /** + @method constructor + @param {Class} tClass Pass in the kind of token to emit if there are + no jumps after this state and the state is accepting. + */ + + /** + On the given symbol(s), this machine should go to the given state + @method on + @param {Array|Mixed} symbol + @param {BaseState} state Note that the type of this state should be the + same as the current instance (i.e., don't pass in a different + subclass) + */ + on: function on(symbol, state) { + if (symbol instanceof Array) { + for (var i = 0; i < symbol.length; i++) { + this.j.push([symbol[i], state]); + } + return this; + } + this.j.push([symbol, state]); + return this; + }, + + + /** + Given the next item, returns next state for that item + @method next + @param {Mixed} item Should be an instance of the symbols handled by + this particular machine. + @return {State} state Returns false if no jumps are available + */ + next: function next(item) { + for (var i = 0; i < this.j.length; i++) { + var jump = this.j[i]; + var symbol = jump[0]; // Next item to check for + var state = jump[1]; // State to jump to if items match + + // compare item with symbol + if (this.test(item, symbol)) { + return state; + } + } + + // Nowhere left to jump! + return this.defaultTransition; + }, + + + /** + Does this state accept? + `true` only of `this.T` exists + @method accepts + @return {Boolean} + */ + accepts: function accepts() { + return !!this.T; + }, + + + /** + Determine whether a given item "symbolizes" the symbol, where symbol is + a class of items handled by this state machine. + This method should be overriden in extended classes. + @method test + @param {Mixed} item Does this item match the given symbol? + @param {Mixed} symbol + @return {Boolean} + */ + test: function test(item, symbol) { + return item === symbol; + }, + + + /** + Emit the token for this State (just return it in this case) + If this emits a token, this instance is an accepting state + @method emit + @return {Class} T + */ + emit: function emit() { + return this.T; + } + }; + + /** + State machine for string-based input + + @class CharacterState + @extends BaseState + */ + var CharacterState = inherits(BaseState, createStateClass(), { + /** + Does the given character match the given character or regular + expression? + @method test + @param {String} char + @param {String|RegExp} charOrRegExp + @return {Boolean} + */ + test: function test(character, charOrRegExp) { + return character === charOrRegExp || charOrRegExp instanceof RegExp && charOrRegExp.test(character); + } + }); + + /** + State machine for input in the form of TextTokens + + @class TokenState + @extends BaseState + */ + var State = inherits(BaseState, createStateClass(), { + + /** + * Similar to `on`, but returns the state the results in the transition from + * the given item + * @method jump + * @param {Mixed} item + * @param {Token} [token] + * @return state + */ + jump: function jump(token) { + var tClass = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1]; + + var state = this.next(new token('')); // dummy temp token + if (state === this.defaultTransition) { + // Make a new state! + state = new this.constructor(tClass); + this.on(token, state); + } else if (tClass) { + state.T = tClass; + } + return state; + }, + + + /** + Is the given token an instance of the given token class? + @method test + @param {TextToken} token + @param {Class} tokenClass + @return {Boolean} + */ + test: function test(token, tokenClass) { + return token instanceof tokenClass; + } + }); + + /** + Given a non-empty target string, generates states (if required) for each + consecutive substring of characters in str starting from the beginning of + the string. The final state will have a special value, as specified in + options. All other "in between" substrings will have a default end state. + + This turns the state machine into a Trie-like data structure (rather than a + intelligently-designed DFA). + + Note that I haven't really tried these with any strings other than + DOMAIN. + + @param {String} str + @param {CharacterState} start State to jump from the first character + @param {Class} endToken Token class to emit when the given string has been + matched and no more jumps exist. + @param {Class} defaultToken "Filler token", or which token type to emit when + we don't have a full match + @return {Array} list of newly-created states + */ + function stateify(str, start, endToken, defaultToken) { + var i = 0, + len = str.length, + state = start, + newStates = [], + nextState = void 0; + + // Find the next state without a jump to the next character + while (i < len && (nextState = state.next(str[i]))) { + state = nextState; + i++; + } + + if (i >= len) { + return []; + } // no new tokens were added + + while (i < len - 1) { + nextState = new CharacterState(defaultToken); + newStates.push(nextState); + state.on(str[i], nextState); + state = nextState; + i++; + } + + nextState = new CharacterState(endToken); + newStates.push(nextState); + state.on(str[len - 1], nextState); + + return newStates; + } + + function createTokenClass() { + return function (value) { + if (value) { + this.v = value; + } + }; + } + + /****************************************************************************** + Text Tokens + Tokens composed of strings + ******************************************************************************/ + + /** + Abstract class used for manufacturing text tokens. + Pass in the value this token represents + + @class TextToken + @abstract + */ + var TextToken = createTokenClass(); + TextToken.prototype = { + toString: function toString() { + return this.v + ''; + } + }; + + function inheritsToken(value) { + var props = value ? { v: value } : {}; + return inherits(TextToken, createTokenClass(), props); + } + + /** + A valid domain token + @class DOMAIN + @extends TextToken + */ + var DOMAIN = inheritsToken(); + + /** + @class AT + @extends TextToken + */ + var AT = inheritsToken('@'); + + /** + Represents a single colon `:` character + + @class COLON + @extends TextToken + */ + var COLON = inheritsToken(':'); + + /** + @class DOT + @extends TextToken + */ + var DOT = inheritsToken('.'); + + /** + A character class that can surround the URL, but which the URL cannot begin + or end with. Does not include certain English punctuation like parentheses. + + @class PUNCTUATION + @extends TextToken + */ + var PUNCTUATION = inheritsToken(); + + /** + The word localhost (by itself) + @class LOCALHOST + @extends TextToken + */ + var LOCALHOST = inheritsToken(); + + /** + Newline token + @class NL + @extends TextToken + */ + var TNL = inheritsToken('\n'); + + /** + @class NUM + @extends TextToken + */ + var NUM = inheritsToken(); + + /** + @class PLUS + @extends TextToken + */ + var PLUS = inheritsToken('+'); + + /** + @class POUND + @extends TextToken + */ + var POUND = inheritsToken('#'); + + /** + Represents a web URL protocol. Supported types include + + * `http:` + * `https:` + * `ftp:` + * `ftps:` + * There's Another super weird one + + @class PROTOCOL + @extends TextToken + */ + var PROTOCOL = inheritsToken(); + + /** + @class QUERY + @extends TextToken + */ + var QUERY = inheritsToken('?'); + + /** + @class SLASH + @extends TextToken + */ + var SLASH = inheritsToken('/'); + + /** + @class UNDERSCORE + @extends TextToken + */ + var UNDERSCORE = inheritsToken('_'); + + /** + One ore more non-whitespace symbol. + @class SYM + @extends TextToken + */ + var SYM = inheritsToken(); + + /** + @class TLD + @extends TextToken + */ + var TLD = inheritsToken(); + + /** + Represents a string of consecutive whitespace characters + + @class WS + @extends TextToken + */ + var WS = inheritsToken(); + + /** + Opening/closing bracket classes + */ + + var OPENBRACE = inheritsToken('{'); + var OPENBRACKET = inheritsToken('['); + var OPENANGLEBRACKET = inheritsToken('<'); + var OPENPAREN = inheritsToken('('); + var CLOSEBRACE = inheritsToken('}'); + var CLOSEBRACKET = inheritsToken(']'); + var CLOSEANGLEBRACKET = inheritsToken('>'); + var CLOSEPAREN = inheritsToken(')'); + + var TOKENS = Object.freeze({ + Base: TextToken, + DOMAIN: DOMAIN, + AT: AT, + COLON: COLON, + DOT: DOT, + PUNCTUATION: PUNCTUATION, + LOCALHOST: LOCALHOST, + NL: TNL, + NUM: NUM, + PLUS: PLUS, + POUND: POUND, + QUERY: QUERY, + PROTOCOL: PROTOCOL, + SLASH: SLASH, + UNDERSCORE: UNDERSCORE, + SYM: SYM, + TLD: TLD, + WS: WS, + OPENBRACE: OPENBRACE, + OPENBRACKET: OPENBRACKET, + OPENANGLEBRACKET: OPENANGLEBRACKET, + OPENPAREN: OPENPAREN, + CLOSEBRACE: CLOSEBRACE, + CLOSEBRACKET: CLOSEBRACKET, + CLOSEANGLEBRACKET: CLOSEANGLEBRACKET, + CLOSEPAREN: CLOSEPAREN + }); + + /** + The scanner provides an interface that takes a string of text as input, and + outputs an array of tokens instances that can be used for easy URL parsing. + + @module linkify + @submodule scanner + @main scanner + */ + + var tlds = 'aaa|aarp|abb|abbott|abogado|ac|academy|accenture|accountant|accountants|aco|active|actor|ad|adac|ads|adult|ae|aeg|aero|af|afl|ag|agency|ai|aig|airforce|airtel|al|alibaba|alipay|allfinanz|alsace|am|amica|amsterdam|an|analytics|android|ao|apartments|app|apple|aq|aquarelle|ar|aramco|archi|army|arpa|arte|as|asia|associates|at|attorney|au|auction|audi|audio|author|auto|autos|avianca|aw|ax|axa|az|azure|ba|baidu|band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bb|bbc|bbva|bcg|bcn|bd|be|beats|beer|bentley|berlin|best|bet|bf|bg|bh|bharti|bi|bible|bid|bike|bing|bingo|bio|biz|bj|black|blackfriday|bloomberg|blue|bm|bms|bmw|bn|bnl|bnpparibas|bo|boats|boehringer|bom|bond|boo|book|boots|bosch|bostik|bot|boutique|br|bradesco|bridgestone|broadway|broker|brother|brussels|bs|bt|budapest|bugatti|build|builders|business|buy|buzz|bv|bw|by|bz|bzh|ca|cab|cafe|cal|call|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|cc|cd|ceb|center|ceo|cern|cf|cfa|cfd|cg|ch|chanel|channel|chase|chat|cheap|chloe|christmas|chrome|church|ci|cipriani|circle|cisco|citic|city|cityeats|ck|cl|claims|cleaning|click|clinic|clinique|clothing|cloud|club|clubmed|cm|cn|co|coach|codes|coffee|college|cologne|com|commbank|community|company|compare|computer|comsec|condos|construction|consulting|contact|contractors|cooking|cool|coop|corsica|country|coupon|coupons|courses|cr|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cu|cuisinella|cv|cw|cx|cy|cymru|cyou|cz|dabur|dad|dance|date|dating|datsun|day|dclk|de|dealer|deals|degree|delivery|dell|deloitte|delta|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount|dj|dk|dm|dnp|do|docs|dog|doha|domains|download|drive|dubai|durban|dvag|dz|earth|eat|ec|edeka|edu|education|ee|eg|email|emerck|energy|engineer|engineering|enterprises|epson|equipment|er|erni|es|esq|estate|et|eu|eurovision|eus|events|everbank|exchange|expert|exposed|express|fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|fast|feedback|ferrero|fi|film|final|finance|financial|firestone|firmdale|fish|fishing|fit|fitness|fj|fk|flickr|flights|florist|flowers|flsmidth|fly|fm|fo|foo|football|ford|forex|forsale|forum|foundation|fox|fr|fresenius|frl|frogans|frontier|fund|furniture|futbol|fyi|ga|gal|gallery|gallup|game|garden|gb|gbiz|gd|gdn|ge|gea|gent|genting|gf|gg|ggee|gh|gi|gift|gifts|gives|giving|gl|glass|gle|global|globo|gm|gmail|gmbh|gmo|gmx|gn|gold|goldpoint|golf|goo|goog|google|gop|got|gov|gp|gq|gr|grainger|graphics|gratis|green|gripe|group|gs|gt|gu|gucci|guge|guide|guitars|guru|gw|gy|hamburg|hangout|haus|hdfcbank|health|healthcare|help|helsinki|here|hermes|hiphop|hitachi|hiv|hk|hm|hn|hockey|holdings|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hr|hsbc|ht|hu|hyundai|ibm|icbc|ice|icu|id|ie|ifm|iinet|il|im|immo|immobilien|in|industries|infiniti|info|ing|ink|institute|insurance|insure|int|international|investments|io|ipiranga|iq|ir|irish|is|iselect|ist|istanbul|it|itau|iwc|jaguar|java|jcb|je|jetzt|jewelry|jlc|jll|jm|jmp|jo|jobs|joburg|jot|joy|jp|jpmorgan|jprs|juegos|kaufen|kddi|ke|kerryhotels|kerrylogistics|kerryproperties|kfh|kg|kh|ki|kia|kim|kinder|kitchen|kiwi|km|kn|koeln|komatsu|kp|kpn|kr|krd|kred|kuokgroup|kw|ky|kyoto|kz|la|lacaixa|lamborghini|lamer|lancaster|land|landrover|lanxess|lasalle|lat|latrobe|law|lawyer|lb|lc|lds|lease|leclerc|legal|lexus|lgbt|li|liaison|lidl|life|lifeinsurance|lifestyle|lighting|like|limited|limo|lincoln|linde|link|live|living|lixil|lk|loan|loans|local|locus|lol|london|lotte|lotto|love|lr|ls|lt|ltd|ltda|lu|lupin|luxe|luxury|lv|ly|ma|madrid|maif|maison|makeup|man|management|mango|market|marketing|markets|marriott|mba|mc|md|me|med|media|meet|melbourne|meme|memorial|men|menu|meo|mg|mh|miami|microsoft|mil|mini|mk|ml|mm|mma|mn|mo|mobi|mobily|moda|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar|mp|mq|mr|ms|mt|mtn|mtpc|mtr|mu|museum|mutuelle|mv|mw|mx|my|mz|na|nadex|nagoya|name|natura|navy|nc|ne|nec|net|netbank|network|neustar|new|news|nexus|nf|ng|ngo|nhk|ni|nico|nikon|ninja|nissan|nl|no|nokia|norton|nowruz|np|nr|nra|nrw|ntt|nu|nyc|nz|obi|office|okinawa|om|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|origins|osaka|otsuka|ovh|pa|page|pamperedchef|panerai|paris|pars|partners|parts|party|passagens|pe|pet|pf|pg|ph|pharmacy|philips|photo|photography|photos|physio|piaget|pics|pictet|pictures|pid|pin|ping|pink|pizza|pk|pl|place|play|playstation|plumbing|plus|pm|pn|pohl|poker|porn|post|pr|praxi|press|pro|prod|productions|prof|promo|properties|property|protection|ps|pt|pub|pw|pwc|py|qa|qpon|quebec|quest|racing|re|read|realtor|realty|recipes|red|redstone|redumbrella|rehab|reise|reisen|reit|ren|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rexroth|rich|ricoh|rio|rip|ro|rocher|rocks|rodeo|room|rs|rsvp|ru|ruhr|run|rw|rwe|ryukyu|sa|saarland|safe|safety|sakura|sale|salon|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|sas|saxo|sb|sbs|sc|sca|scb|schaeffler|schmidt|scholarships|school|schule|schwarz|science|scor|scot|sd|se|seat|security|seek|select|sener|services|seven|sew|sex|sexy|sfr|sg|sh|sharp|shell|shia|shiksha|shoes|show|shriram|si|singles|site|sj|sk|ski|skin|sky|skype|sl|sm|smile|sn|sncf|so|soccer|social|softbank|software|sohu|solar|solutions|song|sony|soy|space|spiegel|spot|spreadbetting|sr|srl|st|stada|star|starhub|statefarm|statoil|stc|stcgroup|stockholm|storage|store|studio|study|style|su|sucks|supplies|supply|support|surf|surgery|suzuki|sv|swatch|swiss|sx|sy|sydney|symantec|systems|sz|tab|taipei|taobao|tatamotors|tatar|tattoo|tax|taxi|tc|tci|td|team|tech|technology|tel|telecity|telefonica|temasek|tennis|tf|tg|th|thd|theater|theatre|tickets|tienda|tiffany|tips|tires|tirol|tj|tk|tl|tm|tmall|tn|to|today|tokyo|tools|top|toray|toshiba|total|tours|town|toyota|toys|tp|tr|trade|trading|training|travel|travelers|travelersinsurance|trust|trv|tt|tube|tui|tunes|tushu|tv|tvs|tw|tz|ua|ubs|ug|uk|unicom|university|uno|uol|us|uy|uz|va|vacations|vana|vc|ve|vegas|ventures|verisign|versicherung|vet|vg|vi|viajes|video|viking|villas|vin|vip|virgin|vision|vista|vistaprint|viva|vlaanderen|vn|vodka|volkswagen|vote|voting|voto|voyage|vu|vuelos|wales|walter|wang|wanggou|watch|watches|weather|weatherchannel|webcam|weber|website|wed|wedding|weir|wf|whoswho|wien|wiki|williamhill|win|windows|wine|wme|wolterskluwer|work|works|world|ws|wtc|wtf|xbox|xerox|xin|xperia|xxx|xyz|yachts|yahoo|yamaxun|yandex|ye|yodobashi|yoga|yokohama|youtube|yt|za|zara|zero|zip|zm|zone|zuerich|zw'.split('|'); // macro, see gulpfile.js + + var NUMBERS = '0123456789'.split(''); + var ALPHANUM = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''); + var WHITESPACE = [' ', '\f', '\r', '\t', '\v', '�', '?', '?']; // excluding line breaks + + var domainStates = []; // states that jump to DOMAIN on /[a-z0-9]/ + var makeState = function makeState(tokenClass) { + return new CharacterState(tokenClass); + }; + + // Frequently used states + var S_START = makeState(); + var S_NUM = makeState(NUM); + var S_DOMAIN = makeState(DOMAIN); + var S_DOMAIN_HYPHEN = makeState(); // domain followed by 1 or more hyphen characters + var S_WS = makeState(WS); + + // States for special URL symbols + S_START.on('@', makeState(AT)).on('.', makeState(DOT)).on('+', makeState(PLUS)).on('#', makeState(POUND)).on('?', makeState(QUERY)).on('/', makeState(SLASH)).on('_', makeState(UNDERSCORE)).on(':', makeState(COLON)).on('{', makeState(OPENBRACE)).on('[', makeState(OPENBRACKET)).on('<', makeState(OPENANGLEBRACKET)).on('(', makeState(OPENPAREN)).on('}', makeState(CLOSEBRACE)).on(']', makeState(CLOSEBRACKET)).on('>', makeState(CLOSEANGLEBRACKET)).on(')', makeState(CLOSEPAREN)).on([',', ';', '!', '"', '\''], makeState(PUNCTUATION)); + + // Whitespace jumps + // Tokens of only non-newline whitespace are arbitrarily long + S_START.on('\n', makeState(TNL)).on(WHITESPACE, S_WS); + + // If any whitespace except newline, more whitespace! + S_WS.on(WHITESPACE, S_WS); + + // Generates states for top-level domains + // Note that this is most accurate when tlds are in alphabetical order + for (var i = 0; i < tlds.length; i++) { + var newStates = stateify(tlds[i], S_START, TLD, DOMAIN); + domainStates.push.apply(domainStates, newStates); + } + + // Collect the states generated by different protocls + var partialProtocolFileStates = stateify('file', S_START, DOMAIN, DOMAIN); + var partialProtocolFtpStates = stateify('ftp', S_START, DOMAIN, DOMAIN); + var partialProtocolHttpStates = stateify('http', S_START, DOMAIN, DOMAIN); + + // Add the states to the array of DOMAINeric states + domainStates.push.apply(domainStates, partialProtocolFileStates); + domainStates.push.apply(domainStates, partialProtocolFtpStates); + domainStates.push.apply(domainStates, partialProtocolHttpStates); + + // Protocol states + var S_PROTOCOL_FILE = partialProtocolFileStates.pop(); + var S_PROTOCOL_FTP = partialProtocolFtpStates.pop(); + var S_PROTOCOL_HTTP = partialProtocolHttpStates.pop(); + var S_PROTOCOL_SECURE = makeState(DOMAIN); + var S_FULL_PROTOCOL = makeState(PROTOCOL); // Full protocol ends with COLON + + // Secure protocols (end with 's') + S_PROTOCOL_FTP.on('s', S_PROTOCOL_SECURE).on(':', S_FULL_PROTOCOL); + + S_PROTOCOL_HTTP.on('s', S_PROTOCOL_SECURE).on(':', S_FULL_PROTOCOL); + + domainStates.push(S_PROTOCOL_SECURE); + + // Become protocol tokens after a COLON + S_PROTOCOL_FILE.on(':', S_FULL_PROTOCOL); + S_PROTOCOL_SECURE.on(':', S_FULL_PROTOCOL); + + // Localhost + var partialLocalhostStates = stateify('localhost', S_START, LOCALHOST, DOMAIN); + domainStates.push.apply(domainStates, partialLocalhostStates); + + // Everything else + // DOMAINs make more DOMAINs + // Number and character transitions + S_START.on(NUMBERS, S_NUM); + S_NUM.on('-', S_DOMAIN_HYPHEN).on(NUMBERS, S_NUM).on(ALPHANUM, S_DOMAIN); // number becomes DOMAIN + + S_DOMAIN.on('-', S_DOMAIN_HYPHEN).on(ALPHANUM, S_DOMAIN); + + // All the generated states should have a jump to DOMAIN + for (var _i = 0; _i < domainStates.length; _i++) { + domainStates[_i].on('-', S_DOMAIN_HYPHEN).on(ALPHANUM, S_DOMAIN); + } + + S_DOMAIN_HYPHEN.on('-', S_DOMAIN_HYPHEN).on(NUMBERS, S_DOMAIN).on(ALPHANUM, S_DOMAIN); + + // Set default transition + S_START.defaultTransition = makeState(SYM); + + /** + Given a string, returns an array of TOKEN instances representing the + composition of that string. + + @method run + @param {String} str Input string to scan + @return {Array} Array of TOKEN instances + */ + var run = function run(str) { + + // The state machine only looks at lowercase strings. + // This selective `toLowerCase` is used because lowercasing the entire + // string causes the length and character position to vary in some in some + // non-English strings. This happens only on V8-based runtimes. + var lowerStr = str.replace(/[A-Z]/g, function (c) { + return c.toLowerCase(); + }); + var len = str.length; + var tokens = []; // return value + + var cursor = 0; + + // Tokenize the string + while (cursor < len) { + var state = S_START; + var secondState = null; + var nextState = null; + var tokenLength = 0; + var latestAccepting = null; + var sinceAccepts = -1; + + while (cursor < len && (nextState = state.next(lowerStr[cursor]))) { + secondState = null; + state = nextState; + + // Keep track of the latest accepting state + if (state.accepts()) { + sinceAccepts = 0; + latestAccepting = state; + } else if (sinceAccepts >= 0) { + sinceAccepts++; + } + + tokenLength++; + cursor++; + } + + if (sinceAccepts < 0) { + continue; + } // Should never happen + + // Roll back to the latest accepting state + cursor -= sinceAccepts; + tokenLength -= sinceAccepts; + + // Get the class for the new token + var TOKEN = latestAccepting.emit(); // Current token class + + // No more jumps, just make a new token + tokens.push(new TOKEN(str.substr(cursor - tokenLength, tokenLength))); + } + + return tokens; + }; + + var start = S_START; + + var scanner = Object.freeze({ + State: CharacterState, + TOKENS: TOKENS, + run: run, + start: start + }); + + /****************************************************************************** + Multi-Tokens + Tokens composed of arrays of TextTokens + ******************************************************************************/ + + // Is the given token a valid domain token? + // Should nums be included here? + function isDomainToken(token) { + return token instanceof DOMAIN || token instanceof TLD; + } + + /** + Abstract class used for manufacturing tokens of text tokens. That is rather + than the value for a token being a small string of text, it's value an array + of text tokens. + + Used for grouping together URLs, emails, hashtags, and other potential + creations. + + @class MultiToken + @abstract + */ + var MultiToken = createTokenClass(); + + MultiToken.prototype = { + /** + String representing the type for this token + @property type + @default 'TOKEN' + */ + type: 'token', + + /** + Is this multitoken a link? + @property isLink + @default false + */ + isLink: false, + + /** + Return the string this token represents. + @method toString + @return {String} + */ + toString: function toString() { + var result = []; + for (var _i2 = 0; _i2 < this.v.length; _i2++) { + result.push(this.v[_i2].toString()); + } + return result.join(''); + }, + + + /** + What should the value for this token be in the `href` HTML attribute? + Returns the `.toString` value by default. + @method toHref + @return {String} + */ + toHref: function toHref() { + return this.toString(); + }, + + + /** + Returns a hash of relevant values for this token, which includes keys + * type - Kind of token ('url', 'email', etc.) + * value - Original text + * href - The value that should be added to the anchor tag's href + attribute + @method toObject + @param {String} [protocol] `'http'` by default + @return {Object} + */ + toObject: function toObject() { + var protocol = arguments.length <= 0 || arguments[0] === undefined ? 'http' : arguments[0]; + + return { + type: this.type, + value: this.toString(), + href: this.toHref(protocol) + }; + } + }; + + /** + Represents a list of tokens making up a valid email address + @class EMAIL + @extends MultiToken + */ + var EMAIL = inherits(MultiToken, createTokenClass(), { + type: 'email', + isLink: true, + toHref: function toHref() { + return 'mailto:' + this.toString(); + } + }); + + /** + Represents some plain text + @class TEXT + @extends MultiToken + */ + var TEXT = inherits(MultiToken, createTokenClass(), { type: 'text' }); + + /** + Multi-linebreak token - represents a line break + @class NL + @extends MultiToken + */ + var NL = inherits(MultiToken, createTokenClass(), { type: 'nl' }); + + /** + Represents a list of tokens making up a valid URL + @class URL + @extends MultiToken + */ + var URL = inherits(MultiToken, createTokenClass(), { + type: 'url', + isLink: true, + + /** + Lowercases relevant parts of the domain and adds the protocol if + required. Note that this will not escape unsafe HTML characters in the + URL. + @method href + @param {String} protocol + @return {String} + */ + toHref: function toHref() { + var protocol = arguments.length <= 0 || arguments[0] === undefined ? 'http' : arguments[0]; + + var hasProtocol = false; + var hasSlashSlash = false; + var tokens = this.v; + var result = []; + var i = 0; + + // Make the first part of the domain lowercase + // Lowercase protocol + while (tokens[i] instanceof PROTOCOL) { + hasProtocol = true; + result.push(tokens[i].toString().toLowerCase()); + i++; + } + + // Skip slash-slash + while (tokens[i] instanceof SLASH) { + hasSlashSlash = true; + result.push(tokens[i].toString()); + i++; + } + + // Lowercase all other characters in the domain + while (isDomainToken(tokens[i])) { + result.push(tokens[i].toString().toLowerCase()); + i++; + } + + // Leave all other characters as they were written + for (; i < tokens.length; i++) { + result.push(tokens[i].toString()); + } + + result = result.join(''); + + if (!(hasProtocol || hasSlashSlash)) { + result = protocol + '://' + result; + } + + return result; + }, + hasProtocol: function hasProtocol() { + return this.v[0] instanceof PROTOCOL; + } + }); + + var TOKENS$1 = Object.freeze({ + Base: MultiToken, + EMAIL: EMAIL, + NL: NL, + TEXT: TEXT, + URL: URL + }); + + /** + Not exactly parser, more like the second-stage scanner (although we can + theoretically hotswap the code here with a real parser in the future... but + for a little URL-finding utility abstract syntax trees may be a little + overkill). + + URL format: http://en.wikipedia.org/wiki/URI_scheme + Email format: http://en.wikipedia.org/wiki/Email_address (links to RFC in + reference) + + @module linkify + @submodule parser + @main parser + */ + + var makeState$1 = function makeState$1(tokenClass) { + return new State(tokenClass); + }; + + // The universal starting state. + var S_START$1 = makeState$1(); + + // Intermediate states for URLs. Note that domains that begin with a protocol + // are treated slighly differently from those that don't. + var S_PROTOCOL = makeState$1(); // e.g., 'http:' + var S_PROTOCOL_SLASH = makeState$1(); // e.g., '/', 'http:/'' + var S_PROTOCOL_SLASH_SLASH = makeState$1(); // e.g., '//', 'http://' + var S_DOMAIN$1 = makeState$1(); // parsed string ends with a potential domain name (A) + var S_DOMAIN_DOT = makeState$1(); // (A) domain followed by DOT + var S_TLD = makeState$1(URL); // (A) Simplest possible URL with no query string + var S_TLD_COLON = makeState$1(); // (A) URL followed by colon (potential port number here) + var S_TLD_PORT = makeState$1(URL); // TLD followed by a port number + var S_URL = makeState$1(URL); // Long URL with optional port and maybe query string + var S_URL_NON_ACCEPTING = makeState$1(); // URL followed by some symbols (will not be part of the final URL) + var S_URL_OPENBRACE = makeState$1(); // URL followed by { + var S_URL_OPENBRACKET = makeState$1(); // URL followed by [ + var S_URL_OPENANGLEBRACKET = makeState$1(); // URL followed by < + var S_URL_OPENPAREN = makeState$1(); // URL followed by ( + var S_URL_OPENBRACE_Q = makeState$1(URL); // URL followed by { and some symbols that the URL can end it + var S_URL_OPENBRACKET_Q = makeState$1(URL); // URL followed by [ and some symbols that the URL can end it + var S_URL_OPENANGLEBRACKET_Q = makeState$1(URL); // URL followed by < and some symbols that the URL can end it + var S_URL_OPENPAREN_Q = makeState$1(URL); // URL followed by ( and some symbols that the URL can end it + var S_URL_OPENBRACE_SYMS = makeState$1(); // S_URL_OPENBRACE_Q followed by some symbols it cannot end it + var S_URL_OPENBRACKET_SYMS = makeState$1(); // S_URL_OPENBRACKET_Q followed by some symbols it cannot end it + var S_URL_OPENANGLEBRACKET_SYMS = makeState$1(); // S_URL_OPENANGLEBRACKET_Q followed by some symbols it cannot end it + var S_URL_OPENPAREN_SYMS = makeState$1(); // S_URL_OPENPAREN_Q followed by some symbols it cannot end it + var S_EMAIL_DOMAIN = makeState$1(); // parsed string starts with local email info + @ with a potential domain name (C) + var S_EMAIL_DOMAIN_DOT = makeState$1(); // (C) domain followed by DOT + var S_EMAIL = makeState$1(EMAIL); // (C) Possible email address (could have more tlds) + var S_EMAIL_COLON = makeState$1(); // (C) URL followed by colon (potential port number here) + var S_EMAIL_PORT = makeState$1(EMAIL); // (C) Email address with a port + var S_LOCALPART = makeState$1(); // Local part of the email address + var S_LOCALPART_AT = makeState$1(); // Local part of the email address plus @ + var S_LOCALPART_DOT = makeState$1(); // Local part of the email address plus '.' (localpart cannot end in .) + var S_NL = makeState$1(NL); // single new line + + // Make path from start to protocol (with '//') + S_START$1.on(TNL, S_NL).on(PROTOCOL, S_PROTOCOL).on(SLASH, S_PROTOCOL_SLASH); + + S_PROTOCOL.on(SLASH, S_PROTOCOL_SLASH); + S_PROTOCOL_SLASH.on(SLASH, S_PROTOCOL_SLASH_SLASH); + + // The very first potential domain name + S_START$1.on(TLD, S_DOMAIN$1).on(DOMAIN, S_DOMAIN$1).on(LOCALHOST, S_TLD).on(NUM, S_DOMAIN$1); + + // Force URL for anything sane followed by protocol + S_PROTOCOL_SLASH_SLASH.on(TLD, S_URL).on(DOMAIN, S_URL).on(NUM, S_URL).on(LOCALHOST, S_URL); + + // Account for dots and hyphens + // hyphens are usually parts of domain names + S_DOMAIN$1.on(DOT, S_DOMAIN_DOT); + S_EMAIL_DOMAIN.on(DOT, S_EMAIL_DOMAIN_DOT); + + // Hyphen can jump back to a domain name + + // After the first domain and a dot, we can find either a URL or another domain + S_DOMAIN_DOT.on(TLD, S_TLD).on(DOMAIN, S_DOMAIN$1).on(NUM, S_DOMAIN$1).on(LOCALHOST, S_DOMAIN$1); + + S_EMAIL_DOMAIN_DOT.on(TLD, S_EMAIL).on(DOMAIN, S_EMAIL_DOMAIN).on(NUM, S_EMAIL_DOMAIN).on(LOCALHOST, S_EMAIL_DOMAIN); + + // S_TLD accepts! But the URL could be longer, try to find a match greedily + // The `run` function should be able to "rollback" to the accepting state + S_TLD.on(DOT, S_DOMAIN_DOT); + S_EMAIL.on(DOT, S_EMAIL_DOMAIN_DOT); + + // Become real URLs after `SLASH` or `COLON NUM SLASH` + // Here PSS and non-PSS converge + S_TLD.on(COLON, S_TLD_COLON).on(SLASH, S_URL); + S_TLD_COLON.on(NUM, S_TLD_PORT); + S_TLD_PORT.on(SLASH, S_URL); + S_EMAIL.on(COLON, S_EMAIL_COLON); + S_EMAIL_COLON.on(NUM, S_EMAIL_PORT); + + // Types of characters the URL can definitely end in + var qsAccepting = [DOMAIN, AT, LOCALHOST, NUM, PLUS, POUND, PROTOCOL, SLASH, TLD, UNDERSCORE, SYM]; + + // Types of tokens that can follow a URL and be part of the query string + // but cannot be the very last characters + // Characters that cannot appear in the URL at all should be excluded + var qsNonAccepting = [COLON, DOT, QUERY, PUNCTUATION, CLOSEBRACE, CLOSEBRACKET, CLOSEANGLEBRACKET, CLOSEPAREN, OPENBRACE, OPENBRACKET, OPENANGLEBRACKET, OPENPAREN]; + + // These states are responsible primarily for determining whether or not to + // include the final round bracket. + + // URL, followed by an opening bracket + S_URL.on(OPENBRACE, S_URL_OPENBRACE).on(OPENBRACKET, S_URL_OPENBRACKET).on(OPENANGLEBRACKET, S_URL_OPENANGLEBRACKET).on(OPENPAREN, S_URL_OPENPAREN); + + // URL with extra symbols at the end, followed by an opening bracket + S_URL_NON_ACCEPTING.on(OPENBRACE, S_URL_OPENBRACE).on(OPENBRACKET, S_URL_OPENBRACKET).on(OPENANGLEBRACKET, S_URL_OPENANGLEBRACKET).on(OPENPAREN, S_URL_OPENPAREN); + + // Closing bracket component. This character WILL be included in the URL + S_URL_OPENBRACE.on(CLOSEBRACE, S_URL); + S_URL_OPENBRACKET.on(CLOSEBRACKET, S_URL); + S_URL_OPENANGLEBRACKET.on(CLOSEANGLEBRACKET, S_URL); + S_URL_OPENPAREN.on(CLOSEPAREN, S_URL); + S_URL_OPENBRACE_Q.on(CLOSEBRACE, S_URL); + S_URL_OPENBRACKET_Q.on(CLOSEBRACKET, S_URL); + S_URL_OPENANGLEBRACKET_Q.on(CLOSEANGLEBRACKET, S_URL); + S_URL_OPENPAREN_Q.on(CLOSEPAREN, S_URL); + S_URL_OPENBRACE_SYMS.on(CLOSEBRACE, S_URL); + S_URL_OPENBRACKET_SYMS.on(CLOSEBRACKET, S_URL); + S_URL_OPENANGLEBRACKET_SYMS.on(CLOSEANGLEBRACKET, S_URL); + S_URL_OPENPAREN_SYMS.on(CLOSEPAREN, S_URL); + + // URL that beings with an opening bracket, followed by a symbols. + // Note that the final state can still be `S_URL_OPENBRACE_Q` (if the URL only + // has a single opening bracket for some reason). + S_URL_OPENBRACE.on(qsAccepting, S_URL_OPENBRACE_Q); + S_URL_OPENBRACKET.on(qsAccepting, S_URL_OPENBRACKET_Q); + S_URL_OPENANGLEBRACKET.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q); + S_URL_OPENPAREN.on(qsAccepting, S_URL_OPENPAREN_Q); + S_URL_OPENBRACE.on(qsNonAccepting, S_URL_OPENBRACE_SYMS); + S_URL_OPENBRACKET.on(qsNonAccepting, S_URL_OPENBRACKET_SYMS); + S_URL_OPENANGLEBRACKET.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_SYMS); + S_URL_OPENPAREN.on(qsNonAccepting, S_URL_OPENPAREN_SYMS); + + // URL that begins with an opening bracket, followed by some symbols + S_URL_OPENBRACE_Q.on(qsAccepting, S_URL_OPENBRACE_Q); + S_URL_OPENBRACKET_Q.on(qsAccepting, S_URL_OPENBRACKET_Q); + S_URL_OPENANGLEBRACKET_Q.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q); + S_URL_OPENPAREN_Q.on(qsAccepting, S_URL_OPENPAREN_Q); + S_URL_OPENBRACE_Q.on(qsNonAccepting, S_URL_OPENBRACE_Q); + S_URL_OPENBRACKET_Q.on(qsNonAccepting, S_URL_OPENBRACKET_Q); + S_URL_OPENANGLEBRACKET_Q.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_Q); + S_URL_OPENPAREN_Q.on(qsNonAccepting, S_URL_OPENPAREN_Q); + + S_URL_OPENBRACE_SYMS.on(qsAccepting, S_URL_OPENBRACE_Q); + S_URL_OPENBRACKET_SYMS.on(qsAccepting, S_URL_OPENBRACKET_Q); + S_URL_OPENANGLEBRACKET_SYMS.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q); + S_URL_OPENPAREN_SYMS.on(qsAccepting, S_URL_OPENPAREN_Q); + S_URL_OPENBRACE_SYMS.on(qsNonAccepting, S_URL_OPENBRACE_SYMS); + S_URL_OPENBRACKET_SYMS.on(qsNonAccepting, S_URL_OPENBRACKET_SYMS); + S_URL_OPENANGLEBRACKET_SYMS.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_SYMS); + S_URL_OPENPAREN_SYMS.on(qsNonAccepting, S_URL_OPENPAREN_SYMS); + + // Account for the query string + S_URL.on(qsAccepting, S_URL); + S_URL_NON_ACCEPTING.on(qsAccepting, S_URL); + + S_URL.on(qsNonAccepting, S_URL_NON_ACCEPTING); + S_URL_NON_ACCEPTING.on(qsNonAccepting, S_URL_NON_ACCEPTING); + + // Email address-specific state definitions + // Note: We are not allowing '/' in email addresses since this would interfere + // with real URLs + + // Tokens allowed in the localpart of the email + var localpartAccepting = [DOMAIN, NUM, PLUS, POUND, QUERY, UNDERSCORE, SYM, TLD]; + + // Some of the tokens in `localpartAccepting` are already accounted for here and + // will not be overwritten (don't worry) + S_DOMAIN$1.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT); + S_TLD.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT); + S_DOMAIN_DOT.on(localpartAccepting, S_LOCALPART); + + // Okay we're on a localpart. Now what? + // TODO: IP addresses and what if the email starts with numbers? + S_LOCALPART.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT) // close to an email address now + .on(DOT, S_LOCALPART_DOT); + S_LOCALPART_DOT.on(localpartAccepting, S_LOCALPART); + S_LOCALPART_AT.on(TLD, S_EMAIL_DOMAIN).on(DOMAIN, S_EMAIL_DOMAIN).on(LOCALHOST, S_EMAIL); + // States following `@` defined above + + var run$1 = function run$1(tokens) { + var len = tokens.length; + var cursor = 0; + var multis = []; + var textTokens = []; + + while (cursor < len) { + var state = S_START$1; + var secondState = null; + var nextState = null; + var multiLength = 0; + var latestAccepting = null; + var sinceAccepts = -1; + + while (cursor < len && !(secondState = state.next(tokens[cursor]))) { + // Starting tokens with nowhere to jump to. + // Consider these to be just plain text + textTokens.push(tokens[cursor++]); + } + + while (cursor < len && (nextState = secondState || state.next(tokens[cursor]))) { + + // Get the next state + secondState = null; + state = nextState; + + // Keep track of the latest accepting state + if (state.accepts()) { + sinceAccepts = 0; + latestAccepting = state; + } else if (sinceAccepts >= 0) { + sinceAccepts++; + } + + cursor++; + multiLength++; + } + + if (sinceAccepts < 0) { + + // No accepting state was found, part of a regular text token + // Add all the tokens we looked at to the text tokens array + for (var _i3 = cursor - multiLength; _i3 < cursor; _i3++) { + textTokens.push(tokens[_i3]); + } + } else { + + // Accepting state! + + // First close off the textTokens (if available) + if (textTokens.length > 0) { + multis.push(new TEXT(textTokens)); + textTokens = []; + } + + // Roll back to the latest accepting state + cursor -= sinceAccepts; + multiLength -= sinceAccepts; + + // Create a new multitoken + var MULTI = latestAccepting.emit(); + multis.push(new MULTI(tokens.slice(cursor - multiLength, cursor))); + } + } + + // Finally close off the textTokens (if available) + if (textTokens.length > 0) { + multis.push(new TEXT(textTokens)); + } + + return multis; + }; + + var parser = Object.freeze({ + State: State, + TOKENS: TOKENS$1, + run: run$1, + start: S_START$1 + }); + + if (!Array.isArray) { + Array.isArray = function (arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; + }; + } + + /** + Converts a string into tokens that represent linkable and non-linkable bits + @method tokenize + @param {String} str + @return {Array} tokens + */ + var tokenize = function tokenize(str) { + return run$1(run(str)); + }; + + /** + Returns a list of linkable items in the given string. + */ + var find = function find(str) { + var type = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1]; + + var tokens = tokenize(str); + var filtered = []; + + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (token.isLink && (!type || token.type === type)) { + filtered.push(token.toObject()); + } + } + + return filtered; + }; + + /** + Is the given string valid linkable text of some sort + Note that this does not trim the text for you. + + Optionally pass in a second `type` param, which is the type of link to test + for. + + For example, + + test(str, 'email'); + + Will return `true` if str is a valid email. + */ + var test = function test(str) { + var type = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1]; + + var tokens = tokenize(str); + return tokens.length === 1 && tokens[0].isLink && (!type || tokens[0].type === type); + }; + + exports.find = find; + exports.inherits = inherits; + exports.options = options; + exports.parser = parser; + exports.scanner = scanner; + exports.test = test; + exports.tokenize = tokenize; +})(window.linkify = window.linkify || {}); +})(); diff --git a/lrcinstance.h b/lrcinstance.h index 517eaae588b594c5386cfbac144861b68687b26b..1377dd6c9ed5d032e116b4e6fb5ef1da15dec1a1 100644 --- a/lrcinstance.h +++ b/lrcinstance.h @@ -93,6 +93,14 @@ public: instance().selectedConvUid = convUid; }; + static void reset(bool newInstance = false) { + if (newInstance) { + instance().lrc_.reset(new lrc::api::Lrc()); + } else { + instance().lrc_.reset(); + } + }; + private: std::unique_ptr<lrc::api::Lrc> lrc_; diff --git a/main.cpp b/main.cpp index 5e3d70f7eafa0f4ce4a1f03006c30ca7cb98b3f6..469351c8a8f052dd7b40b0ad181e46c246097e3f 100644 --- a/main.cpp +++ b/main.cpp @@ -17,6 +17,7 @@ **************************************************************************/ #include "mainwindow.h" + #include <QApplication> #include <QFile> @@ -35,11 +36,13 @@ #include <ciso646> +#include "utils.h" + #ifdef Q_OS_WIN #include <windows.h> #endif -#ifdef _MSC_VER +#if defined _MSC_VER && !COMPILE_ONLY #include <gnutls/gnutls.h> #endif @@ -88,7 +91,7 @@ main(int argc, char *argv[]) auto startMinimized = false; QString uri = ""; -#ifdef _MSC_VER +#if defined _MSC_VER && !COMPILE_ONLY gnutls_global_init(); #endif diff --git a/mainwindow.cpp b/mainwindow.cpp index efaa47540549a1520727320cd03af73a0d9bf4bc..176853cb0cb259c77bc1edfd94728205e3694562 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -38,6 +38,7 @@ #include "callwidget.h" #include "utils.h" #include "wizarddialog.h" +#include "version.h" MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), @@ -102,7 +103,7 @@ MainWindow::MainWindow(QWidget* parent) : readSettingsFromRegistry(); win_sparkle_set_appcast_url("http://dl.ring.cx/windows/winsparkle-ring.xml"); - win_sparkle_set_app_details(L"Savoir-faire Linux", L"Ring", QString(NIGHTLY_VERSION).toStdWString().c_str()); + win_sparkle_set_app_details(L"Savoir-faire Linux", L"Ring", QString(VERSION_STRING).toStdWString().c_str()); win_sparkle_set_shutdown_request_callback([]() {QCoreApplication::exit();}); win_sparkle_set_did_find_update_callback([]() {MainWindow::instance().showNormal();}); win_sparkle_init(); @@ -243,6 +244,7 @@ MainWindow::closeEvent(QCloseEvent* event) settings.setValue(SettingsKey::geometry, saveGeometry()); settings.setValue(SettingsKey::windowState, saveState()); } + LRCInstance::reset(); QMainWindow::closeEvent(event); } diff --git a/messagemodel.cpp b/messagemodel.cpp deleted file mode 100644 index 998c97f7eb5113f42577ed840831a5f530a46891..0000000000000000000000000000000000000000 --- a/messagemodel.cpp +++ /dev/null @@ -1,123 +0,0 @@ -/*************************************************************************** -* Copyright (C) 2017 by Savoir-faire Linux * -* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * -* * -* 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/>. * -***************************************************************************/ - -#include "messagemodel.h" - -// Qt -#include <QDateTime> - -// LRC -#include "globalinstances.h" -#include "api/contactmodel.h" -#include "api/conversationmodel.h" - -// Client -#include "pixbufmanipulator.h" -#include "utils.h" - -MessageModel::MessageModel(const ConversationInfo& conv, const AccountInfo& acc, QObject *parent) - : QAbstractItemModel(parent), - conv_(conv), - acc_(acc) -{ -} - -int MessageModel::rowCount(const QModelIndex &parent) const -{ - if (!parent.isValid()) { - return conv_.interactions.size(); - } - return 0; // A valid QModelIndex returns 0 as no entry has sub-elements -} - -int MessageModel::columnCount(const QModelIndex &parent) const -{ - Q_UNUSED(parent); - return 1; -} - -QVariant MessageModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid() || conv_.interactions.size() == 0) { - return QVariant(); - } - - auto it = conv_.interactions.begin(); - std::advance(it, index.row()); - const auto& item = (*it).second; - switch (role) { - case Role::Body: - return QVariant(QString::fromStdString(item.body)); - case Role::Picture: - case Qt::DecorationRole: - return GlobalInstances::pixmapManipulator().decorationRole(conv_, acc_); - case Role::DisplayName: - case Qt::DisplayRole: - { - auto& contact = acc_.contactModel->getContact(conv_.participants[0]); - return QVariant(QString::fromStdString(Utils::bestNameForContact(contact))); - } - case Role::Presence: - { - auto& contact = acc_.contactModel->getContact(conv_.participants[0]); - return QVariant(contact.isPresent); - } - case Role::InteractionDate: - { - auto& date = item.timestamp; - return QVariant(QDateTime::fromTime_t(date)); - } - case Role::Status: - return QVariant::fromValue(static_cast<int>(item.status)); - case Role::Direction: - return QVariant::fromValue(lrc::api::interaction::isOutgoing(item)); - case Role::Type: - return QVariant::fromValue(static_cast<int>(item.type)); - } - return QVariant(); -} - -QModelIndex MessageModel::index(int row, int column, const QModelIndex &parent) const -{ - Q_UNUSED(parent); - if (column != 0) { - return QModelIndex(); - } - - if (row >= 0 && row < rowCount()) { - return createIndex(row, column); - } - return QModelIndex(); -} - -QModelIndex MessageModel::parent(const QModelIndex &child) const -{ - Q_UNUSED(child); - return QModelIndex(); -} - -Qt::ItemFlags MessageModel::flags(const QModelIndex &index) const -{ - auto flags = QAbstractItemModel::flags(index) | Qt::ItemNeverHasChildren | Qt::ItemIsSelectable; - if (!index.isValid()) { - return QAbstractItemModel::flags(index); - } else { - flags &= ~(Qt::ItemIsSelectable); - } - return flags; -} diff --git a/messagemodel.h b/messagemodel.h deleted file mode 100644 index b8411c37c60501a9af56c05dbfedf557ac69ceb7..0000000000000000000000000000000000000000 --- a/messagemodel.h +++ /dev/null @@ -1,61 +0,0 @@ -/*************************************************************************** -* Copyright (C) 2017 by Savoir-faire Linux * -* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * -* * -* 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/>. * -***************************************************************************/ -#pragma once - -// Qt include -#include <QAbstractItemModel> - -// LRC -#include "api/account.h" -#include "api/conversation.h" -#include "api/contact.h" - -class MessageModel : public QAbstractItemModel -{ - Q_OBJECT -public: - using AccountInfo = lrc::api::account::Info; - using ConversationInfo = lrc::api::conversation::Info; - using ContactInfo = lrc::api::contact::Info; - - enum Role { - Body = Qt::UserRole + 1, - DisplayName, - Picture, - Presence, - Status, - DataTransferStatus, - InteractionDate, - Direction, - Type - }; - - explicit MessageModel(const ConversationInfo& conv, const AccountInfo& acc, QObject *parent = 0); - - // QAbstractItemModel - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - int columnCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const; - QModelIndex parent(const QModelIndex &child) const; - Qt::ItemFlags flags(const QModelIndex &index) const; - -private: - ConversationInfo conv_; - const AccountInfo& acc_; -}; diff --git a/conversationfilterbutton.cpp b/messagewebpage.cpp similarity index 52% rename from conversationfilterbutton.cpp rename to messagewebpage.cpp index 7bba253372cd6e642f30952d83ce06ed4433d31a..441fc27fdaa8304835d9e6064d30a15b61d86447 100644 --- a/conversationfilterbutton.cpp +++ b/messagewebpage.cpp @@ -1,65 +1,42 @@ -/*************************************************************************** - * Copyright (C) 2018 by Savoir-faire Linux * - * Author: Isa Nanic <isa.nanic@savoirfairelinux.com> * - * * - * 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/>. * - **************************************************************************/ - -#include "conversationfilterbutton.h" -#include <QPainter> - -ConversationFilterButton::ConversationFilterButton() -{ - setCheckable(false); - connect(this, &ConversationFilterButton::pressed, this, &ConversationFilterButton::setSelected); -} - -ConversationFilterButton::ConversationFilterButton(QWidget* widget) -{ - Q_UNUSED(widget) - setCheckable(false); - connect(this, &ConversationFilterButton::pressed, this, &ConversationFilterButton::setSelected); -} - -ConversationFilterButton::~ConversationFilterButton() -{ -} - -void -ConversationFilterButton::paintEvent(QPaintEvent * e) -{ - QPushButton::paintEvent(e); - QPainter painter(this); - - if (selected_) { - this->setStyleSheet("background-color: rgb(250, 250, 250)"); - } else { - this->setStyleSheet("background-color: rgb(237, 237, 237)"); - } - painter.end(); -} - -void -ConversationFilterButton::setSelected() -{ - selected_ = true; -} - -void -ConversationFilterButton::setUnselected() -{ - selected_ = false; - update(); -} - +/*************************************************************************** + * Copyright (C) 2018 by Savoir-faire Linux * + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * + * * + * 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/>. * + **************************************************************************/ + +#include <QDesktopServices> + +#include "messagewebpage.h" + +MessageWebPage::MessageWebPage(QWidget *parent) + : QWebEnginePage(parent) +{ +} + +MessageWebPage::~MessageWebPage() +{ +} + +bool +MessageWebPage::acceptNavigationRequest(const QUrl & url, QWebEnginePage::NavigationType type, bool isMainFrame) +{ + qDebug() << "acceptNavigationRequest(" << url << "," << type << "," << isMainFrame << ")"; + + if (type == QWebEnginePage::NavigationTypeLinkClicked) { + QDesktopServices::openUrl(url); + return false; + } + return true; +} diff --git a/conversationfilterbutton.h b/messagewebpage.h similarity index 68% rename from conversationfilterbutton.h rename to messagewebpage.h index 4877227b5f03ca62ff75eb5e25ce3e05dbd1ec28..e8c4cea164d5462deddb79d02bf2cd4e1c4f6e28 100644 --- a/conversationfilterbutton.h +++ b/messagewebpage.h @@ -1,45 +1,35 @@ -/*************************************************************************** - * Copyright (C) 2018 by Savoir-faire Linux * - * Author: Isa Nanic <isa.nanic@savoirfairelinux.com> * - * * - * 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/>. * - **************************************************************************/ - -#pragma once - -#include <QPushButton> - -namespace Ui{ - class ConvesationFilterButton; -} - -class ConversationFilterButton : public QPushButton -{ - Q_OBJECT - ConversationFilterButton(const ConversationFilterButton& cpy); -public: - ConversationFilterButton(); - ConversationFilterButton(QWidget * widget); - ~ConversationFilterButton(); - -public slots: - void setSelected(); - void setUnselected(); - - -private: - void paintEvent(QPaintEvent* e); - bool selected_ = false; -}; - +/*************************************************************************** + * Copyright (C) 2018 by Savoir-faire Linux * + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * + * * + * 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/>. * + **************************************************************************/ + +#pragma once + +#include <QtWebEngineWidgets/QWebEngineView> + +class MessageWebPage : public QWebEnginePage +{ + Q_OBJECT +public: + explicit MessageWebPage(QWidget* parent = nullptr); + ~MessageWebPage(); + +protected: + virtual bool acceptNavigationRequest(const QUrl &url, + QWebEnginePage::NavigationType type, + bool isMainFrame); + +}; \ No newline at end of file diff --git a/messagewebview.cpp b/messagewebview.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5262edd9eb45336f372e0014d7f64c015b1175f8 --- /dev/null +++ b/messagewebview.cpp @@ -0,0 +1,348 @@ +/*************************************************************************** + * Copyright (C) 2017-2018 by Savoir-faire Linux * + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * + * Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com> * + * Author: S�bastien Blin <sebastien.blin@savoirfairelinux.com> * + * Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com> * + * * + * 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/>. * + **************************************************************************/ + +#include "messagewebview.h" + +#include <QScrollBar> +#include <QMouseEvent> +#include <QDebug> +#include <QMenu> +#include <QDesktopServices> +#include <QFileDialog> +#include <QWebEnginePage> +#include <QWebEngineScript> +#include <QWebEngineScriptCollection> +#include <QWebEngineSettings> +#include <QWebChannel> +#include <QTimer> + +#include <ciso646> +#include <fstream> + +#include "utils.h" +#include "webchathelpers.h" +#include "messagewebpage.h" +#include "lrcinstance.h" + +MessageWebView::MessageWebView(QWidget *parent) + : QWebEngineView(parent) +{ + setPage(new MessageWebPage()); + + settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true); + settings()->setAttribute(QWebEngineSettings::JavascriptCanOpenWindows, true); + settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, false); + settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false); + settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false); + settings()->setAttribute(QWebEngineSettings::FullScreenSupportEnabled, false); + settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false); + settings()->setAttribute(QWebEngineSettings::LinksIncludedInFocusChain, false); + settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false); + + setContextMenuPolicy(Qt::ContextMenuPolicy::NoContextMenu); + + jsBridge_ = new PrivateBridging(this); + webChannel_ = new QWebChannel(page()); + webChannel_->registerObject(QStringLiteral("jsbridge"), jsBridge_); + page()->setWebChannel(webChannel_); + + connect(this, &QWebEngineView::renderProcessTerminated, + [this](QWebEnginePage::RenderProcessTerminationStatus termStatus, int statusCode) { + Q_UNUSED(statusCode); + QString status; + switch (termStatus) { + case QWebEnginePage::NormalTerminationStatus: + qDebug() << "MessageWebView - Render process normal exit"; + break; + case QWebEnginePage::AbnormalTerminationStatus: + qDebug() << "MessageWebView - Render process abnormal exit"; + break; + case QWebEnginePage::CrashedTerminationStatus: + qDebug() << "MessageWebView - Render process crashed"; + break; + case QWebEnginePage::KilledTerminationStatus: + qDebug() << "MessageWebView - Render process killed"; + break; + } + }); +} + +MessageWebView::~MessageWebView() +{ +} + +void MessageWebView::buildView() +{ + auto html = Utils::QByteArrayFromFile(":/web/chatview.html"); + page()->setHtml(html, QUrl(":/web/chatview.html")); + connect(this, &QWebEngineView::loadFinished, this, &MessageWebView::slotLoadFinished); +} + +void +MessageWebView::slotLoadFinished() +{ + insertStyleSheet("chatcss", Utils::QByteArrayFromFile(":/web/chatview.css")); + page()->runJavaScript(Utils::QByteArrayFromFile(":/web/linkify.js"), QWebEngineScript::MainWorld); + page()->runJavaScript(Utils::QByteArrayFromFile(":/web/linkify-html.js"), QWebEngineScript::MainWorld); + page()->runJavaScript(Utils::QByteArrayFromFile(":/web/linkify-string.js"), QWebEngineScript::MainWorld); + page()->runJavaScript(Utils::QByteArrayFromFile(":/web/qwebchannel.js"), QWebEngineScript::MainWorld); + page()->runJavaScript(Utils::QByteArrayFromFile(":/web/chatview.js"), QWebEngineScript::MainWorld); +} + +void +MessageWebView::insertStyleSheet(const QString &name, const QString &source) +{ + QWebEngineScript script; + auto simplifiedCSS = source.simplified().replace("'", "\""); + QString s = QString::fromLatin1("(function() {"\ + " var node = document.createElement('style');"\ + " node.id = '%1';"\ + " node.innerHTML = '%2';"\ + " document.head.appendChild(node);"\ + "})()").arg(name).arg(simplifiedCSS); + page()->runJavaScript(s); + + script.setName(name); + script.setSourceCode(s); + script.setInjectionPoint(QWebEngineScript::DocumentReady); + script.setRunsOnSubFrames(true); + script.setWorldId(QWebEngineScript::MainWorld); + page()->scripts().insert(script); +} + +void +MessageWebView::removeStyleSheet(const QString &name) +{ + QString s = QString::fromLatin1("(function() {"\ + " var element = document.getElementById('%1');"\ + " element.outerHTML = '';"\ + " delete element;"\ + "})()").arg(name); + + page()->runJavaScript(s); + + QWebEngineScript script = page()->scripts().findScript(name); + page()->scripts().remove(script); +} + +void MessageWebView::clear() +{ + QString s = QString::fromLatin1("clearMessages();"); + page()->runJavaScript(s, QWebEngineScript::MainWorld); +} + +void +MessageWebView::setDisplayLinks(bool display) +{ + QString s = QString::fromLatin1("setDisplayLinks('%1');") + .arg(display ? "true" : "false"); + page()->runJavaScript(s, QWebEngineScript::MainWorld); +} + +void +MessageWebView::printNewInteraction(lrc::api::ConversationModel& conversationModel, + uint64_t msgId, + const lrc::api::interaction::Info& interaction) +{ + auto interactionObject = interactionToJsonInteractionObject(conversationModel, msgId, interaction).toUtf8(); + QString s = QString::fromLatin1("addMessage(%1);") + .arg(interactionObject.constData()); + page()->runJavaScript(s, QWebEngineScript::MainWorld); +} + +void +MessageWebView::updateInteraction(lrc::api::ConversationModel& conversationModel, + uint64_t msgId, + const lrc::api::interaction::Info& interaction) +{ + auto interactionObject = interactionToJsonInteractionObject(conversationModel, msgId, interaction).toUtf8(); + QString s = QString::fromLatin1("updateMessage(%1);") + .arg(interactionObject.constData()); + page()->runJavaScript(s, QWebEngineScript::MainWorld); +} + +void +MessageWebView::removeInteraction(uint64_t interactionId) +{ + QString s = QString::fromLatin1("removeInteraction(%1);") + .arg(QString::number(interactionId)); + page()->runJavaScript(s, QWebEngineScript::MainWorld); +} + +void +MessageWebView::printHistory(lrc::api::ConversationModel& conversationModel, + const std::map<uint64_t, + lrc::api::interaction::Info> interactions) +{ + auto interactionsStr = interactionsToJsonArrayObject(conversationModel, interactions).toUtf8(); + QString s = QString::fromLatin1("printHistory(%1);") + .arg(interactionsStr.constData()); + page()->runJavaScript(s, QWebEngineScript::MainWorld); +} + +void +MessageWebView::setSenderImage(const std::string& sender, + const std::string& senderImage) +{ + QJsonObject setSenderImageObject = QJsonObject(); + setSenderImageObject.insert("sender_contact_method", QJsonValue(QString(sender.c_str()))); + setSenderImageObject.insert("sender_image", QJsonValue(QString(senderImage.c_str()))); + + auto setSenderImageObjectString = QString(QJsonDocument(setSenderImageObject).toJson(QJsonDocument::Compact)); + QString s = QString::fromLatin1("setSenderImage(%1);") + .arg(setSenderImageObjectString.toUtf8().constData()); + page()->runJavaScript(s, QWebEngineScript::MainWorld); +} + +void +MessageWebView::setInvitation(bool show, const std::string& contactUri) +{ + QString s = QString::fromLatin1(show ? "showInvitation(\"%1\")" : "showInvitation()") + .arg(QString(contactUri.c_str())); + page()->runJavaScript(s, QWebEngineScript::MainWorld); +} + +void +MessageWebView::hideMessages() +{ + QString s = QString::fromLatin1("hideBody();"); + page()->runJavaScript(s, QWebEngineScript::MainWorld); +} + +// JS bridging incoming +Q_INVOKABLE int +PrivateBridging::deleteInteraction(const QString& arg) +{ + bool ok; + uint64_t interactionUid = arg.toULongLong(&ok); + if (ok) { + LRCInstance::getCurrentConversationModel()->clearInteractionFromConversation( + LRCInstance::getSelectedConvUid(), + interactionUid + ); + } else { + qDebug() << "deleteInteraction - invalid arg" << arg; + } + return 0; +} + +Q_INVOKABLE int +PrivateBridging::retryInteraction(const QString& arg) +{ + bool ok; + uint64_t interactionUid = arg.toULongLong(&ok); + if (ok) { + LRCInstance::getCurrentConversationModel()->retryInteraction( + LRCInstance::getSelectedConvUid(), + interactionUid + ); + } else { + qDebug() << "retryInteraction - invalid arg" << arg; + } + return 0; +} + +Q_INVOKABLE int +PrivateBridging::openFile(const QString& arg) +{ + QDesktopServices::openUrl(arg); + return 0; +} + +Q_INVOKABLE int +PrivateBridging::acceptFile(const QString& arg) +{ + try { + auto interactionUid = std::stoull(arg.toStdString()); + + lrc::api::datatransfer::Info info = {}; + auto convUid = LRCInstance::getSelectedConvUid(); + LRCInstance::getCurrentConversationModel()->getTransferInfo(interactionUid, info); + + auto downloadsFolder = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation).toStdString(); + // get full path + std::string filename = downloadsFolder.c_str(); + if (!filename.empty() && filename.back() != '/') + filename += "/"; + auto wantedFilename = filename + info.displayName; + auto duplicate = 0; + while (std::ifstream(wantedFilename).good()) { + ++duplicate; + auto extensionIdx = info.displayName.find_last_of("."); + if (extensionIdx == std::string::npos) + wantedFilename = filename + info.displayName + " (" + std::to_string(duplicate) + ")"; + else + wantedFilename = filename + info.displayName.substr(0, extensionIdx) + " (" + std::to_string(duplicate) + ")" + info.displayName.substr(extensionIdx); + } + LRCInstance::getCurrentConversationModel()->acceptTransfer(convUid, interactionUid, wantedFilename); + } catch (...) { + qDebug() << "JS bridging - exception during acceptFile: " << arg; + } + return 0; +} + +Q_INVOKABLE int +PrivateBridging::refuseFile(const QString& arg) +{ + try { + auto interactionUid = std::stoull(arg.toStdString()); + auto convUid = LRCInstance::getSelectedConvUid(); + LRCInstance::getCurrentConversationModel()->cancelTransfer(convUid, interactionUid); + } catch (...) { + qDebug() << "JS bridging - exception during refuseFile:" << arg; + } + return 0; +} + +Q_INVOKABLE int +PrivateBridging::sendMessage(const QString& arg) +{ + try { + auto convUid = LRCInstance::getSelectedConvUid(); + LRCInstance::getCurrentConversationModel()->sendMessage(convUid, arg.toStdString()); + } catch (...) { + qDebug() << "JS bridging - exception during sendMessage:" << arg; + } + return 0; +} + +Q_INVOKABLE int +PrivateBridging::sendFile() +{ + qDebug() << "JS bridging - MessageWebView::sendFile"; + QString filePath = QFileDialog::getOpenFileName((QWidget*)this->parent(), tr("Choose File"), "", tr("Files (*)")); + QFileInfo fi(filePath); + QString fileName = fi.fileName(); + try { + auto convUid = LRCInstance::getSelectedConvUid(); + LRCInstance::getCurrentConversationModel()->sendFile(convUid, filePath.toStdString(), fileName.toStdString()); + } catch (...) { + qDebug() << "JS bridging - exception during sendFile"; + } + return 0; +} + +Q_INVOKABLE int +PrivateBridging::log(const QString& arg) +{ + qDebug() << "JS log: " << arg; + return 0; +} \ No newline at end of file diff --git a/messagewebview.h b/messagewebview.h new file mode 100644 index 0000000000000000000000000000000000000000..72b15e783bc03718b208177d470ba482176a9079 --- /dev/null +++ b/messagewebview.h @@ -0,0 +1,81 @@ +/*************************************************************************** + * Copyright (C) 2018 by Savoir-faire Linux * + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * + * * + * 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/>. * + **************************************************************************/ + +#pragma once + +#include <QDebug> +#include <QWebEngineView> + +#include "api/conversationmodel.h" + +class PrivateBridging : public QObject +{ + Q_OBJECT +public: + explicit PrivateBridging(QObject* parent = nullptr) : QObject(parent) {}; + ~PrivateBridging() {}; + + // exposed to JS through QWebChannel + Q_INVOKABLE int deleteInteraction(const QString& arg); + Q_INVOKABLE int retryInteraction(const QString& arg); + Q_INVOKABLE int openFile(const QString& arg); + Q_INVOKABLE int acceptFile(const QString& arg); + Q_INVOKABLE int refuseFile(const QString& arg); + Q_INVOKABLE int sendMessage(const QString & arg); + Q_INVOKABLE int sendFile(); + Q_INVOKABLE int log(const QString& arg); + +}; + +class MessageWebView : public QWebEngineView +{ + Q_OBJECT +public: + explicit MessageWebView(QWidget* parent = nullptr); + ~MessageWebView(); + + void buildView(); + + void insertStyleSheet(const QString &name, const QString &source); + void removeStyleSheet(const QString &name); + + void clear(); + void setDisplayLinks(bool display); + void printNewInteraction(lrc::api::ConversationModel& conversationModel, + uint64_t msgId, + const lrc::api::interaction::Info& interaction); + void updateInteraction(lrc::api::ConversationModel& conversationModel, + uint64_t msgId, + const lrc::api::interaction::Info& interaction); + void removeInteraction(uint64_t interactionId); + void printHistory(lrc::api::ConversationModel& conversationModel, + const std::map<uint64_t, + lrc::api::interaction::Info> interactions); + void setSenderImage(const std::string& sender, + const std::string& senderImage); + void setInvitation(bool show, const std::string& contactUri); + void hideMessages(); + +private slots: + void slotLoadFinished(); + +private: + QWebChannel* webChannel_; + PrivateBridging* jsBridge_; + +}; diff --git a/ressources.qrc b/ressources.qrc index 5cf0ca66456f6cc353262276d4a05791ff08c4bb..51b40be02b54c44a411fd93267a4293792243bfa 100644 --- a/ressources.qrc +++ b/ressources.qrc @@ -1,55 +1,62 @@ <RCC> - <qresource prefix="/"> - <file>stylesheet.css</file> - <file>images/ring.png</file> - <file>images/logo-ring-standard-coul.png</file> - <file>images/icons/ic_arrow_back_white_24dp.png</file> - <file>images/icons/ic_mic_off_white_24dp.png</file> - <file>images/icons/ic_person_add_white_24dp.png</file> - <file>images/icons/ic_search_black_18dp_2x.png</file> - <file>images/icons/ic_content_copy_white_24dp.png</file> - <file>images/icons/ic_send_white_24dp.png</file> - <file>images/icons/ic_group_add_white_24dp.png</file> - <file>images/icons/ic_videocam_off_white_24dp.png</file> - <file>images/icons/ic_high_quality_white_24dp.png</file> - <file>images/icons/ic_pause_white_24dp.png</file> - <file>images/icons/ic_chat_white_24dp.png</file> - <file>images/icons/ic_done_white_24dp.png</file> - <file>images/icons/ic_close_white_24dp.png</file> - <file>images/icons/ic_call_transfer_white_24px.png</file> - <file>images/icons/ic_pause_white_100px.png</file> - <file>images/icons/ic_videocam_white.png</file> - <file>images/icons/ic_add_black_18dp_2x.png</file> - <file>images/icons/ic_delete_black_18dp_2x.png</file> - <file>images/icons/ic_arrow_drop_down_black_18dp_2x.png</file> - <file>images/icons/ic_arrow_drop_up_black_18dp_2x.png</file> - <file>images/icons/ic_check_white_18dp_2x.png</file> - <file>images/icons/ic_folder_black_18dp_2x.png</file> - <file>images/icons/ic_arrow_forward_white_48dp_2x.png</file> - <file>images/icons/ic_settings_white_48dp_2x.png</file> - <file>images/icons/ic_voicemail_white_24dp_2x.png</file> - <file>images/background-light.png</file> - <file>images/background-dark.png</file> - <file>images/icons/ic_arrow_drop_down_black_9dp_2x.png</file> - <file>images/icons/ic_arrow_drop_up_black_9dp_2x.png</file> - <file>images/icons/ic_arrow_tab_next_black_9dp_2x.png</file> - <file>images/icons/ic_arrow_tab_previous_black_9dp_2x.png</file> - <file>images/spikeMask.png</file> - <file>images/icons/ic_photo_camera_white_24dp_2x.png</file> - <file>images/qrcode.png</file> - <file>images/icons/ic_share_black_48dp_2x.png</file> - <file>images/loading.gif</file> - <file>images/FontAwesome.otf</file> - <file>images/icons/ic_chat_black_24dp_2x.png</file> - <file>images/icons/ic_person_add_black_24dp_2x.png</file> - <file>images/waiting.gif</file> - <file>images/default_avatar_overlay.svg</file> - <file>images/icons/ic_clear_24px.svg</file> - <file>images/icons/ic_block_24px.svg</file> - <file>images/icons/ic_phone_24px.svg</file> - <file>images/icons/ic_video_call_24px.svg</file> - <file>images/icons/ic_arrow_back_24px.svg</file> - <file>images/icons/ic_send_24px.svg</file> - <file>images/icons/round-settings-24px.svg</file> - </qresource> + <qresource prefix="/"> + <file>stylesheet.css</file> + <file>images/ring.png</file> + <file>images/logo-ring-standard-coul.png</file> + <file>images/icons/ic_arrow_back_white_24dp.png</file> + <file>images/icons/ic_mic_off_white_24dp.png</file> + <file>images/icons/ic_person_add_white_24dp.png</file> + <file>images/icons/ic_search_black_18dp_2x.png</file> + <file>images/icons/ic_content_copy_white_24dp.png</file> + <file>images/icons/ic_send_white_24dp.png</file> + <file>images/icons/ic_group_add_white_24dp.png</file> + <file>images/icons/ic_videocam_off_white_24dp.png</file> + <file>images/icons/ic_high_quality_white_24dp.png</file> + <file>images/icons/ic_pause_white_24dp.png</file> + <file>images/icons/ic_chat_white_24dp.png</file> + <file>images/icons/ic_done_white_24dp.png</file> + <file>images/icons/ic_close_white_24dp.png</file> + <file>images/icons/ic_call_transfer_white_24px.png</file> + <file>images/icons/ic_pause_white_100px.png</file> + <file>images/icons/ic_videocam_white.png</file> + <file>images/icons/ic_add_black_18dp_2x.png</file> + <file>images/icons/ic_delete_black_18dp_2x.png</file> + <file>images/icons/ic_arrow_drop_down_black_18dp_2x.png</file> + <file>images/icons/ic_arrow_drop_up_black_18dp_2x.png</file> + <file>images/icons/ic_check_white_18dp_2x.png</file> + <file>images/icons/ic_folder_black_18dp_2x.png</file> + <file>images/icons/ic_arrow_forward_white_48dp_2x.png</file> + <file>images/icons/ic_settings_white_48dp_2x.png</file> + <file>images/icons/ic_voicemail_white_24dp_2x.png</file> + <file>images/background-light.png</file> + <file>images/background-dark.png</file> + <file>images/icons/ic_arrow_drop_down_black_9dp_2x.png</file> + <file>images/icons/ic_arrow_drop_up_black_9dp_2x.png</file> + <file>images/icons/ic_arrow_tab_next_black_9dp_2x.png</file> + <file>images/icons/ic_arrow_tab_previous_black_9dp_2x.png</file> + <file>images/spikeMask.png</file> + <file>images/icons/ic_photo_camera_white_24dp_2x.png</file> + <file>images/qrcode.png</file> + <file>images/icons/ic_share_black_48dp_2x.png</file> + <file>images/loading.gif</file> + <file>images/FontAwesome.otf</file> + <file>images/icons/ic_chat_black_24dp_2x.png</file> + <file>images/icons/ic_person_add_black_24dp_2x.png</file> + <file>images/waiting.gif</file> + <file>images/default_avatar_overlay.svg</file> + <file>images/icons/ic_clear_24px.svg</file> + <file>images/icons/ic_block_24px.svg</file> + <file>images/icons/ic_phone_24px.svg</file> + <file>images/icons/ic_video_call_24px.svg</file> + <file>images/icons/ic_arrow_back_24px.svg</file> + <file>images/icons/ic_send_24px.svg</file> + <file>images/icons/round-settings-24px.svg</file> + <file>web/chatview.css</file> + <file>web/chatview.html</file> + <file>web/chatview.js</file> + <file>web/linkify-html.js</file> + <file>web/linkify-string.js</file> + <file>web/linkify.js</file> + <file>web/qwebchannel.js</file> + </qresource> </RCC> diff --git a/ring-client-windows.sln b/ring-client-windows.sln index 0eb69bd003b0ee6d425a5fb2e5527fb61ccc529a..9106f8c44781633dfd5b0878fc1dba413182a8ef 100644 --- a/ring-client-windows.sln +++ b/ring-client-windows.sln @@ -5,14 +5,14 @@ VisualStudioVersion = 15.0.27703.2026 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ring-client-windows", "ring-client-windows.vcxproj", "{0F6318E4-4C06-384E-BCA8-F344DA187957}" ProjectSection(ProjectDependencies) = postProject - {103832FC-21E8-3D6C-8C85-21D927092562} = {103832FC-21E8-3D6C-8C85-21D927092562} + {A604BA33-C1DB-34F6-8584-C429857717A8} = {A604BA33-C1DB-34F6-8584-C429857717A8} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ring-daemon", "..\daemon\MSVC\ring-daemon.vcxproj", "{79F8DE42-595D-49D9-A66F-55244FD9DCC3}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ringclient_static", "..\lrc\msvc\ringclient_static.vcxproj", "{103832FC-21E8-3D6C-8C85-21D927092562}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ringclient_static", "..\lrc\msvc\ringclient_static.vcxproj", "{A604BA33-C1DB-34F6-8584-C429857717A8}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "qtwrapper", "..\lrc\msvc\src\qtwrapper\qtwrapper.vcxproj", "{75CF859A-E546-32BB-8806-448BDB78A8C4}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "qtwrapper", "..\lrc\msvc\src\qtwrapper\qtwrapper.vcxproj", "{8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}" ProjectSection(ProjectDependencies) = postProject {79F8DE42-595D-49D9-A66F-55244FD9DCC3} = {79F8DE42-595D-49D9-A66F-55244FD9DCC3} EndProjectSection @@ -99,60 +99,60 @@ Global {79F8DE42-595D-49D9-A66F-55244FD9DCC3}.RelWithDebInfo|x64.Build.0 = ReleaseLib|x64 {79F8DE42-595D-49D9-A66F-55244FD9DCC3}.RelWithDebInfo|x86.ActiveCfg = ReleaseLib|Win32 {79F8DE42-595D-49D9-A66F-55244FD9DCC3}.RelWithDebInfo|x86.Build.0 = ReleaseLib|Win32 - {103832FC-21E8-3D6C-8C85-21D927092562}.Debug|x64.ActiveCfg = Debug|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.Debug|x64.Build.0 = Debug|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.Debug|x86.ActiveCfg = Debug|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib_win32|x64.ActiveCfg = Debug|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib_win32|x64.Build.0 = Debug|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib_win32|x86.ActiveCfg = MinSizeRel|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib_win32|x86.Build.0 = MinSizeRel|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib|x64.ActiveCfg = Debug|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib|x64.Build.0 = Debug|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib|x86.ActiveCfg = MinSizeRel|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.DebugLib|x86.Build.0 = MinSizeRel|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.MinSizeRel|x64.Build.0 = MinSizeRel|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.Release|x64.ActiveCfg = Release|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.Release|x86.ActiveCfg = Release|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib_win32|x64.ActiveCfg = Release|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib_win32|x64.Build.0 = Release|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib_win32|x86.ActiveCfg = MinSizeRel|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib_win32|x86.Build.0 = MinSizeRel|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib|x64.ActiveCfg = Release|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib|x64.Build.0 = Release|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib|x86.ActiveCfg = MinSizeRel|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.ReleaseLib|x86.Build.0 = MinSizeRel|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64 - {103832FC-21E8-3D6C-8C85-21D927092562}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.Debug|x64.ActiveCfg = Debug|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.Debug|x64.Build.0 = Debug|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.Debug|x86.ActiveCfg = Debug|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib_win32|x64.ActiveCfg = Debug|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib_win32|x64.Build.0 = Debug|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib_win32|x86.ActiveCfg = MinSizeRel|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib_win32|x86.Build.0 = MinSizeRel|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib|x64.ActiveCfg = Debug|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib|x64.Build.0 = Debug|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib|x86.ActiveCfg = MinSizeRel|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.DebugLib|x86.Build.0 = MinSizeRel|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.MinSizeRel|x64.Build.0 = MinSizeRel|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.Release|x64.ActiveCfg = Release|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.Release|x86.ActiveCfg = Release|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib_win32|x64.ActiveCfg = Release|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib_win32|x64.Build.0 = Release|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib_win32|x86.ActiveCfg = MinSizeRel|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib_win32|x86.Build.0 = MinSizeRel|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib|x64.ActiveCfg = Release|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib|x64.Build.0 = Release|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib|x86.ActiveCfg = MinSizeRel|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.ReleaseLib|x86.Build.0 = MinSizeRel|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64 - {75CF859A-E546-32BB-8806-448BDB78A8C4}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.Debug|x64.ActiveCfg = Debug|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.Debug|x64.Build.0 = Debug|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.Debug|x86.ActiveCfg = Debug|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib_win32|x64.ActiveCfg = Debug|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib_win32|x64.Build.0 = Debug|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib_win32|x86.ActiveCfg = MinSizeRel|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib_win32|x86.Build.0 = MinSizeRel|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib|x64.ActiveCfg = Debug|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib|x64.Build.0 = Debug|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib|x86.ActiveCfg = MinSizeRel|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.DebugLib|x86.Build.0 = MinSizeRel|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.MinSizeRel|x64.Build.0 = MinSizeRel|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.Release|x64.ActiveCfg = Release|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.Release|x86.ActiveCfg = Release|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib_win32|x64.ActiveCfg = Release|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib_win32|x64.Build.0 = Release|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib_win32|x86.ActiveCfg = MinSizeRel|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib_win32|x86.Build.0 = MinSizeRel|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib|x64.ActiveCfg = Release|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib|x64.Build.0 = Release|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib|x86.ActiveCfg = MinSizeRel|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.ReleaseLib|x86.Build.0 = MinSizeRel|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64 + {A604BA33-C1DB-34F6-8584-C429857717A8}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.Debug|x64.ActiveCfg = Debug|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.Debug|x64.Build.0 = Debug|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.Debug|x86.ActiveCfg = Debug|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib_win32|x64.ActiveCfg = Debug|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib_win32|x64.Build.0 = Debug|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib_win32|x86.ActiveCfg = MinSizeRel|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib_win32|x86.Build.0 = MinSizeRel|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib|x64.ActiveCfg = Debug|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib|x64.Build.0 = Debug|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib|x86.ActiveCfg = MinSizeRel|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.DebugLib|x86.Build.0 = MinSizeRel|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.MinSizeRel|x64.Build.0 = MinSizeRel|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.Release|x64.ActiveCfg = Release|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.Release|x86.ActiveCfg = Release|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib_win32|x64.ActiveCfg = Release|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib_win32|x64.Build.0 = Release|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib_win32|x86.ActiveCfg = MinSizeRel|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib_win32|x86.Build.0 = MinSizeRel|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib|x64.ActiveCfg = Release|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib|x64.Build.0 = Release|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib|x86.ActiveCfg = MinSizeRel|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.ReleaseLib|x86.Build.0 = MinSizeRel|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64 + {8C3AD0D4-A52C-3EFB-A2C6-04EA3AB5F00E}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ring-client-windows.vcxproj b/ring-client-windows.vcxproj index 7217d113df0983be88fff1c1be4aa6ad208eb7c6..6019a7f479dda3b86cf1fa1ba23213c0b3057682 100644 --- a/ring-client-windows.vcxproj +++ b/ring-client-windows.vcxproj @@ -1,6 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup Label="ProjectConfigurations"> + <ProjectConfiguration Include="ReleaseCompile|x64"> + <Configuration>ReleaseCompile</Configuration> + <Platform>x64</Platform> + </ProjectConfiguration> <ProjectConfiguration Include="Release|x64"> <Configuration>Release</Configuration> <Platform>x64</Platform> @@ -22,6 +26,15 @@ <IntermediateDirectory>release\</IntermediateDirectory> <PrimaryOutput>Ring</PrimaryOutput> </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'" Label="Configuration"> + <PlatformToolset>v141</PlatformToolset> + <OutputDirectory>release\</OutputDirectory> + <ATLMinimizesCRunTimeLibraryUsage>false</ATLMinimizesCRunTimeLibraryUsage> + <CharacterSet>NotSet</CharacterSet> + <ConfigurationType>StaticLibrary</ConfigurationType> + <IntermediateDirectory>release\</IntermediateDirectory> + <PrimaryOutput>Ring</PrimaryOutput> + </PropertyGroup> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> <PropertyGroup Condition="'$(QtMsBuild)'=='' or !Exists('$(QtMsBuild)\qt.targets')"> <QtMsBuild>$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild> @@ -36,17 +49,28 @@ <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets"> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" /> </ImportGroup> + <ImportGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'" Label="PropertySheets"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" /> + </ImportGroup> <PropertyGroup Label="UserMacros" /> <PropertyGroup> <OutDir Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(Platform)\$(Configuration)\</OutDir> + <OutDir Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">$(Platform)\$(Configuration)\</OutDir> <IntDir Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(Platform)\$(Configuration)\</IntDir> + <IntDir Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">$(Platform)\$(Configuration)\</IntDir> <TargetName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Ring</TargetName> + <TargetName Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">Ring</TargetName> <IgnoreImportLibrary Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</IgnoreImportLibrary> + <IgnoreImportLibrary Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">true</IgnoreImportLibrary> <LinkIncremental Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</LinkIncremental> + <LinkIncremental Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">false</LinkIncremental> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'"> + <TargetExt>.lib</TargetExt> </PropertyGroup> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <ClCompile> - <AdditionalIncludeDirectories>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalIncludeDirectories>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)..\ring-daemon\contrib\msvc\include;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\ring-lrc\src;$(ProjectDir)..\lrc\src;$(ProjectDir)winsparkle\include;$(ProjectDir)qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <AdditionalOptions>-Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions)</AdditionalOptions> <AssemblerListingLocation>release\</AssemblerListingLocation> <BrowseInformation>false</BrowseInformation> @@ -55,7 +79,7 @@ <ExceptionHandling>Sync</ExceptionHandling> <ObjectFileName>$(IntDir)</ObjectFileName> <Optimization>MaxSpeed</Optimization> - <PreprocessorDefinitions>_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <PreprocessorDefinitions>_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessToFile>false</PreprocessToFile> <ProgramDataBaseFileName>$(IntDir)vc$(PlatformToolsetVersion).pdb</ProgramDataBaseFileName> <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary> @@ -68,7 +92,82 @@ <FunctionLevelLinking>true</FunctionLevelLinking> </ClCompile> <Link> - <AdditionalDependencies>..\daemon\MSVC\x64\ReleaseLib_win32\bin\dring.lib;..\lrc\msvc\src\qtwrapper\Release\qtwrapper.lib;..\lrc\msvc\release\ringclient_static.lib;.\winsparkle\x64\release\WinSparkle.lib;.\qrencode-win32\qrencode-win32\vc8\qrcodelib\x64\Release-Lib\qrcodelib.lib;shell32.lib;Ole32.lib;Advapi32.lib;Shlwapi.lib;User32.lib;Gdi32.lib;Crypt32.lib;Strmiids.lib;$(QTDIR)\lib\qtmain.lib;$(QTDIR)\lib\Qt5Svg.lib;$(QTDIR)\lib\Qt5Widgets.lib;$(QTDIR)\lib\Qt5WinExtras.lib;$(QTDIR)\lib\Qt5Gui.lib;$(QTDIR)\lib\Qt5Xml.lib;$(QTDIR)\lib\Qt5Network.lib;$(QTDIR)\lib\Qt5Core.lib;$(QTDIR)\lib\Qt5Sql.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies>dring.lib;qtwrapper.lib;ringclient_static.lib;WinSparkle.lib;qrcodelib.lib;shell32.lib;Ole32.lib;Advapi32.lib;Shlwapi.lib;User32.lib;Gdi32.lib;Crypt32.lib;Strmiids.lib;qtmain.lib;Qt5Svg.lib;Qt5Widgets.lib;Qt5WinExtras.lib;Qt5Gui.lib;Qt5Xml.lib;Qt5Network.lib;Qt5Core.lib;$(QTDIR)\lib\Qt5Sql.lib;$(QTDIR)\lib\Qt5WebEngineWidgets.lib;$(QTDIR)\lib\Qt5WebChannel.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalLibraryDirectories>$(QTDIR)\lib;..\ring-daemon\contrib\msvc\lib\x64;..\daemon\contrib\msvc\lib\x64;..\ring-daemon\MSVC\x64\ReleaseLib_win32\bin;..\daemon\MSVC\x64\ReleaseLib_win32\bin;..\ring-lrc\msvc\release;..\lrc\msvc\release;..\ring-lrc\msvc\src\qtwrapper\Release;..\lrc\msvc\src\qtwrapper\Release;.\winsparkle\x64\release;.\qrencode-win32\qrencode-win32\vc8\qrcodelib\x64\Release-Lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> + <AdditionalOptions>"/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /ignore:4006,4049,4078,4098 /LTCG /NODEFAULTLIB:LIBCMT %(AdditionalOptions)</AdditionalOptions> + <DataExecutionPrevention>true</DataExecutionPrevention> + <GenerateDebugInformation>true</GenerateDebugInformation> + <IgnoreImportLibrary>true</IgnoreImportLibrary> + <LinkIncremental>false</LinkIncremental> + <OutputFile>$(OutDir)\Ring.exe</OutputFile> + <RandomizedBaseAddress>true</RandomizedBaseAddress> + <SubSystem>Windows</SubSystem> + <SuppressStartupBanner>true</SuppressStartupBanner> + <Version>2.0</Version> + <ForceFileOutput>MultiplyDefinedSymbolOnly</ForceFileOutput> + </Link> + <Midl> + <DefaultCharType>Unsigned</DefaultCharType> + <EnableErrorChecks>None</EnableErrorChecks> + <WarningLevel>0</WarningLevel> + </Midl> + <ResourceCompile> + <PreprocessorDefinitions>_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions> + </ResourceCompile> + <QtMoc> + <QTDIR>$(QTDIR)</QTDIR> + <OutputFile>.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</OutputFile> + <Define>_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</Define> + <CompilerFlavor>msvc</CompilerFlavor> + <Include>$(Configuration)/moc_predefs.h</Include> + <ExecutionDescription>Moc'ing %(Identity)...</ExecutionDescription> + <InputFile>%(FullPath)</InputFile> + <DynamicSource>output</DynamicSource> + <IncludePath>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)..\ring-daemon\contrib\msvc\include;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\ring-lrc\src;$(ProjectDir)..\lrc\src;$(ProjectDir)winsparkle\include;$(ProjectDir)qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release;%(AdditionalIncludeDirectories)</IncludePath> + </QtMoc> + <QtRcc> + <InitFuncName>ressources</InitFuncName> + <OutputFile>.\GeneratedFiles\qrc_%(Filename).cpp</OutputFile> + <QTDIR>$(QTDIR)</QTDIR> + <ExecutionDescription>Rcc'ing %(Identity)...</ExecutionDescription> + <Compression>default</Compression> + <InputFile>%(FullPath)</InputFile> + </QtRcc> + <QtUic> + <InputFile>%(FullPath)</InputFile> + <ExecutionDescription>Uic'ing %(Identity)...</ExecutionDescription> + <OutputFile>.\GeneratedFiles\ui_%(Filename).h</OutputFile> + <QTDIR>$(QTDIR)</QTDIR> + </QtUic> + <PreBuildEvent> + <Command>call touch_res.bat</Command> + </PreBuildEvent> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'"> + <ClCompile> + <AdditionalIncludeDirectories>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)..\ring-daemon\contrib\msvc\include;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\ring-lrc\src;$(ProjectDir)..\lrc\src;$(ProjectDir)winsparkle\include;$(ProjectDir)qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <AdditionalOptions>-Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 %(AdditionalOptions)</AdditionalOptions> + <AssemblerListingLocation>release\</AssemblerListingLocation> + <BrowseInformation>false</BrowseInformation> + <DebugInformationFormat>ProgramDatabase</DebugInformationFormat> + <DisableSpecificWarnings>4068;4099;4189;4267;4577;4467;4715;%(DisableSpecificWarnings)</DisableSpecificWarnings> + <ExceptionHandling>Sync</ExceptionHandling> + <ObjectFileName>$(IntDir)</ObjectFileName> + <Optimization>MaxSpeed</Optimization> + <PreprocessorDefinitions>COMPILE_ONLY;_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <PreprocessToFile>false</PreprocessToFile> + <ProgramDataBaseFileName>$(IntDir)vc$(PlatformToolsetVersion).pdb</ProgramDataBaseFileName> + <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary> + <SuppressStartupBanner>true</SuppressStartupBanner> + <TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType> + <WarningLevel>Level3</WarningLevel> + <MultiProcessorCompilation>true</MultiProcessorCompilation> + <SDLCheck>true</SDLCheck> + <IntrinsicFunctions>true</IntrinsicFunctions> + <FunctionLevelLinking>true</FunctionLevelLinking> + </ClCompile> + <Link> + <AdditionalDependencies>..\daemon\MSVC\x64\ReleaseLib_win32\bin\dring.lib;..\lrc\msvc\src\qtwrapper\Release\qtwrapper.lib;..\lrc\msvc\release\ringclient_static.lib;.\winsparkle\x64\release\WinSparkle.lib;.\qrencode-win32\qrencode-win32\vc8\qrcodelib\x64\Release-Lib\qrcodelib.lib;shell32.lib;Ole32.lib;Advapi32.lib;Shlwapi.lib;User32.lib;Gdi32.lib;Crypt32.lib;Strmiids.lib;$(QTDIR)\lib\qtmain.lib;$(QTDIR)\lib\Qt5Svg.lib;$(QTDIR)\lib\Qt5Widgets.lib;$(QTDIR)\lib\Qt5WinExtras.lib;$(QTDIR)\lib\Qt5Gui.lib;$(QTDIR)\lib\Qt5Xml.lib;$(QTDIR)\lib\Qt5Network.lib;$(QTDIR)\lib\Qt5Core.lib;$(QTDIR)\lib\Qt5Sql.lib;$(QTDIR)\lib\Qt5WebEngineWidgets.lib;$(QTDIR)\lib\Qt5WebChannel.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalLibraryDirectories>$(QTDIR)\lib;..\daemon\contrib\msvc\lib\x64;..\daemon\MSVC\x64\ReleaseLib_win32\bin;..\lrc\msvc\release;.\winsparkle\x64\release;.\qrencode-win32\qrencode-win32\vc8\qrcodelib\x64\Release-Lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> <AdditionalOptions>"/MANIFESTDEPENDENCY:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' publicKeyToken='6595b64144ccf1df' language='*' processorArchitecture='*'" /ignore:4006,4049,4078,4098 /LTCG /NODEFAULTLIB:LIBCMT %(AdditionalOptions)</AdditionalOptions> <DataExecutionPrevention>true</DataExecutionPrevention> @@ -93,13 +192,13 @@ <QtMoc> <QTDIR>$(QTDIR)</QTDIR> <OutputFile>.\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp</OutputFile> - <Define>_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</Define> + <Define>COMPILE_ONLY;_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;%(PreprocessorDefinitions)</Define> <CompilerFlavor>msvc</CompilerFlavor> <Include>$(Configuration)/moc_predefs.h</Include> <ExecutionDescription>Moc'ing %(Identity)...</ExecutionDescription> <InputFile>%(FullPath)</InputFile> <DynamicSource>output</DynamicSource> - <IncludePath>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)../daemon/contrib/msvc/include;$(ProjectDir)../lrc/src;$(ProjectDir)../client-windows/winsparkle/include;$(ProjectDir)../client-windows/qrencode-win32/qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;release;%(AdditionalIncludeDirectories)</IncludePath> + <IncludePath>.\GeneratedFiles\$(ConfigurationName);.\GeneratedFiles;.;$(ProjectDir)..\ring-daemon\contrib\msvc\include;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\ring-lrc\src;$(ProjectDir)..\lrc\src;$(ProjectDir)winsparkle\include;$(ProjectDir)qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release;%(AdditionalIncludeDirectories)</IncludePath> </QtMoc> <QtRcc> <InitFuncName>ressources</InitFuncName> @@ -115,9 +214,12 @@ <OutputFile>.\GeneratedFiles\ui_%(Filename).h</OutputFile> <QTDIR>$(QTDIR)</QTDIR> </QtUic> + <PreBuildEvent> + <Command>call touch_res.bat</Command> + </PreBuildEvent> </ItemDefinitionGroup> <ItemGroup> - <ClCompile Include="conversationfilterbutton.cpp" /> + <ClCompile Include="animationhelpers.cpp" /> <ClCompile Include="currentaccountcombobox.cpp" /> <ClCompile Include="aboutdialog.cpp" /> <ClCompile Include="accountdetails.cpp" /> @@ -129,9 +231,13 @@ <ClCompile Include="callutilsdialog.cpp" /> <ClCompile Include="callwidget.cpp"> <OutputFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\%(Filename).moc</OutputFile> + <OutputFile Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\%(Filename).moc</OutputFile> <DynamicSource Condition="'$(Configuration)|$(Platform)'=='Release|x64'">input</DynamicSource> + <DynamicSource Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">input</DynamicSource> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> </ClCompile> <ClCompile Include="configurationwidget.cpp" /> <ClCompile Include="contactpicker.cpp" /> @@ -139,12 +245,11 @@ <ClCompile Include="deleteaccountdialog.cpp" /> <ClCompile Include="globalsystemtray.cpp" /> <ClCompile Include="idlabel.cpp" /> - <ClCompile Include="imdelegate.cpp" /> - <ClCompile Include="instantmessagingwidget.cpp" /> <ClCompile Include="invitebuttonswidget.cpp" /> <ClCompile Include="main.cpp" /> <ClCompile Include="mainwindow.cpp" /> - <ClCompile Include="messagemodel.cpp" /> + <ClCompile Include="messagewebpage.cpp" /> + <ClCompile Include="messagewebview.cpp" /> <ClCompile Include="navwidget.cpp" /> <ClCompile Include="photoboothdialog.cpp" /> <ClCompile Include="photoboothwidget.cpp" /> @@ -156,9 +261,13 @@ <ClCompile Include="sendcontactrequestwidget.cpp" /> <ClCompile Include="conversationsfilterwidget.cpp"> <OutputFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\%(Filename).moc</OutputFile> + <OutputFile Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\%(Filename).moc</OutputFile> <DynamicSource Condition="'$(Configuration)|$(Platform)'=='Release|x64'">input</DynamicSource> + <DynamicSource Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">input</DynamicSource> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> </ClCompile> <ClCompile Include="smartlistselectorbuttonnotifier.cpp" /> <ClCompile Include="smartlistview.cpp" /> @@ -168,6 +277,7 @@ <ClCompile Include="videooverlay.cpp" /> <ClCompile Include="videoview.cpp" /> <ClCompile Include="videowidget.cpp" /> + <ClCompile Include="webchathelpers.cpp" /> <ClCompile Include="windowscontactbackend.cpp" /> <ClCompile Include="wizarddialog.cpp" /> <ClCompile Include="wizardwidget.cpp" /> @@ -175,7 +285,9 @@ <ItemGroup> <QtMoc Include="wizardwidget.h"> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> </QtMoc> <QtMoc Include="aboutdialog.h"> </QtMoc> @@ -198,11 +310,15 @@ </QtMoc> <QtMoc Include="accountlistmodel.h"> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define> </QtMoc> <QtMoc Include="accountitemdelegate.h"> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define> </QtMoc> <QtMoc Include="contactrequestwidget.h"> </QtMoc> @@ -212,10 +328,6 @@ </QtMoc> <QtMoc Include="idlabel.h"> </QtMoc> - <QtMoc Include="imdelegate.h"> - </QtMoc> - <QtMoc Include="instantmessagingwidget.h"> - </QtMoc> <QtMoc Include="mainwindow.h"> </QtMoc> <QtMoc Include="navwidget.h"> @@ -226,20 +338,34 @@ </QtMoc> <QtMoc Include="invitebuttonswidget.h"> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> </QtMoc> <QtMoc Include="currentaccountcombobox.h"> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> </QtMoc> - <QtMoc Include="conversationfilterbutton.h"> - <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <QtMoc Include="animationhelpers.h"> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtWebChannel;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> </QtMoc> <ClInclude Include="lrcinstance.h" /> - <QtMoc Include="messagemodel.h"> - <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> - <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define> + <QtMoc Include="messagewebview.h"> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + </QtMoc> + <QtMoc Include="messagewebpage.h"> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtWebEngineWidgets;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> </QtMoc> <ClInclude Include="pixbufmanipulator.h" /> <QtMoc Include="qualitydialog.h"> @@ -256,23 +382,33 @@ <ClInclude Include="settingskey.h" /> <QtMoc Include="smartlistview.h"> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define> </QtMoc> <QtMoc Include="conversationitemdelegate.h"> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> </QtMoc> <QtMoc Include="smartlistmodel.h"> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG;QT_SVG_LIB;QT_WIDGETS_LIB;QT_WINEXTRAS_LIB;QT_GUI_LIB;QT_XML_LIB;QT_NETWORK_LIB;QT_CORE_LIB</Define> </QtMoc> <QtMoc Include="conversationsfilterwidget.h"> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> </QtMoc> <QtMoc Include="smartlistselectorbuttonnotifier.h"> <IncludePath Condition="'$(Configuration)|$(Platform)'=='Release|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> + <IncludePath Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">.\GeneratedFiles\$(ConfigurationName)\.;.\GeneratedFiles;.;$(ProjectDir)..\daemon\contrib\msvc\include;$(ProjectDir)..\lrc\src;$(ProjectDir)..\client-windows\winsparkle\include;$(ProjectDir)..\client-windows\qrencode-win32\qrencode-win32;$(QTDIR)\include;$(QTDIR)\include\QtSvg;$(QTDIR)\include\QtWidgets;$(QTDIR)\include\QtWinExtras;$(QTDIR)\include\QtGui;$(QTDIR)\include\QtANGLE;$(QTDIR)\include\QtXml;$(QTDIR)\include\QtNetwork;$(QTDIR)\include\QtCore;$(QTDIR)\mkspecs\win32-msvc;.\release</IncludePath> <Define Condition="'$(Configuration)|$(Platform)'=='Release|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> + <Define Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">_WINDOWS;UNICODE;_UNICODE;WIN32;WIN64;NIGHTLY_VERSION=20180706;ENABLE_AUTOUPDATE;QT_NO_DEBUG;NDEBUG</Define> </QtMoc> <ClInclude Include="utils.h" /> <QtMoc Include="videooverlay.h"> @@ -281,6 +417,8 @@ </QtMoc> <QtMoc Include="videowidget.h"> </QtMoc> + <ClInclude Include="version.h" /> + <ClInclude Include="webchathelpers.h" /> <ClInclude Include="windowscontactbackend.h" /> <QtMoc Include="wizarddialog.h"> </QtMoc> @@ -289,13 +427,18 @@ <CustomBuild Include="debug\moc_predefs.h.cbt"> <FileType>Document</FileType> <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='Release|x64'">true</ExcludedFromBuild> + <ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">true</ExcludedFromBuild> </CustomBuild> <CustomBuild Include="release\moc_predefs.h.cbt"> <FileType>Document</FileType> <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs)</AdditionalInputs> + <AdditionalInputs Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">$(QTDIR)\mkspecs\features\data\dummy.cpp;%(AdditionalInputs)</AdditionalInputs> <Command Condition="'$(Configuration)|$(Platform)'=='Release|x64'">cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -O2 -MD -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E $(QTDIR)\mkspecs\features\data\dummy.cpp 2>NUL >release\moc_predefs.h</Command> + <Command Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">cl -Bx"$(QTDIR)\bin\qmake.exe" -nologo -Zc:wchar_t -FS -Zc:rvalueCast -Zc:inline -Zc:strictStrings -Zc:throwingNew -Zc:referenceBinding -O2 -MD -W3 -w34100 -w34189 -w44996 -w44456 -w44457 -w44458 -wd4577 -wd4467 -E $(QTDIR)\mkspecs\features\data\dummy.cpp 2>NUL >release\moc_predefs.h</Command> <Message Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Generate moc_predefs.h</Message> + <Message Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">Generate moc_predefs.h</Message> <Outputs Condition="'$(Configuration)|$(Platform)'=='Release|x64'">release\moc_predefs.h;%(Outputs)</Outputs> + <Outputs Condition="'$(Configuration)|$(Platform)'=='ReleaseCompile|x64'">release\moc_predefs.h;%(Outputs)</Outputs> </CustomBuild> <ClInclude Include="ui_aboutdialog.h" /> <ClInclude Include="ui_accountdetails.h" /> @@ -306,7 +449,6 @@ <ClInclude Include="ui_contactpicker.h" /> <ClInclude Include="ui_contactrequestwidget.h" /> <ClInclude Include="ui_deleteaccountdialog.h" /> - <ClInclude Include="ui_instantmessagingwidget.h" /> <ClInclude Include="ui_mainwindow.h" /> <ClInclude Include="ui_photoboothdialog.h" /> <ClInclude Include="ui_photoboothwidget.h" /> @@ -371,12 +513,22 @@ <None Include="translations\ring_client_windows_zh.ts" /> <None Include="translations\ring_client_windows_zh_CN.ts" /> <None Include="translations\ring_client_windows_zh_TW.ts" /> + <None Include="web\chatview.css" /> + <None Include="web\chatview.html" /> + <None Include="web\chatview.js" /> + <None Include="web\linkify-html.js" /> + <None Include="web\linkify-string.js" /> + <None Include="web\linkify.js" /> + <None Include="web\qwebchannel.js" /> </ItemGroup> <ItemGroup> <QtUic Include="aboutdialog.ui"> </QtUic> <QtUic Include="accountdetails.ui"> </QtUic> + <QtUic Include="animatedoverlay.ui"> + <SubType>Designer</SubType> + </QtUic> <QtUic Include="bannedcontactswidget.ui"> </QtUic> <QtUic Include="callutilsdialog.ui"> @@ -392,8 +544,6 @@ </QtUic> <QtUic Include="deleteaccountdialog.ui"> </QtUic> - <QtUic Include="instantmessagingwidget.ui"> - </QtUic> <QtUic Include="invitebuttonswidget.ui" /> <QtUic Include="mainwindow.ui"> <SubType>Designer</SubType> @@ -462,6 +612,7 @@ <None Include="images\logo-ring-standard-coul.png" /> <None Include="images\qrcode.png" /> <QtRcc Include="ressources.qrc"> + <SubType>Designer</SubType> </QtRcc> <None Include="images\ring.png" /> <None Include="images\spikeMask.png" /> diff --git a/ring-client-windows.vcxproj.filters b/ring-client-windows.vcxproj.filters index 951dfa2f12799d333615a0784a620e0b78e51106..d2e77d3928e780532f3f0543a1332841b13b511c 100644 --- a/ring-client-windows.vcxproj.filters +++ b/ring-client-windows.vcxproj.filters @@ -55,6 +55,9 @@ <Extensions>ts;xlf</Extensions> <ParseFiles>false</ParseFiles> </Filter> + <Filter Include="Resource Files\web"> + <UniqueIdentifier>{faf8f8a4-eb67-49df-a082-2fd64189e62c}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClCompile Include="aboutdialog.cpp"> @@ -93,12 +96,6 @@ <ClCompile Include="idlabel.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClCompile Include="imdelegate.cpp"> - <Filter>Source Files</Filter> - </ClCompile> - <ClCompile Include="instantmessagingwidget.cpp"> - <Filter>Source Files</Filter> - </ClCompile> <ClCompile Include="main.cpp"> <Filter>Source Files</Filter> </ClCompile> @@ -156,9 +153,6 @@ <ClCompile Include="smartlistmodel.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClCompile Include="messagemodel.cpp"> - <Filter>Source Files</Filter> - </ClCompile> <ClCompile Include="accountlistmodel.cpp"> <Filter>Source Files</Filter> </ClCompile> @@ -186,7 +180,16 @@ <ClCompile Include="currentaccountcombobox.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClCompile Include="conversationfilterbutton.cpp"> + <ClCompile Include="messagewebview.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="messagewebpage.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="webchathelpers.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="animationhelpers.cpp"> <Filter>Source Files</Filter> </ClCompile> </ItemGroup> @@ -230,12 +233,6 @@ <QtMoc Include="idlabel.h"> <Filter>Header Files</Filter> </QtMoc> - <QtMoc Include="imdelegate.h"> - <Filter>Header Files</Filter> - </QtMoc> - <QtMoc Include="instantmessagingwidget.h"> - <Filter>Header Files</Filter> - </QtMoc> <QtMoc Include="mainwindow.h"> <Filter>Header Files</Filter> </QtMoc> @@ -296,9 +293,6 @@ <QtMoc Include="smartlistview.h"> <Filter>Header Files</Filter> </QtMoc> - <QtMoc Include="messagemodel.h"> - <Filter>Header Files</Filter> - </QtMoc> <QtMoc Include="accountlistmodel.h"> <Filter>Header Files</Filter> </QtMoc> @@ -323,7 +317,13 @@ <QtMoc Include="currentaccountcombobox.h"> <Filter>Header Files</Filter> </QtMoc> - <QtMoc Include="conversationfilterbutton.h"> + <QtMoc Include="messagewebview.h"> + <Filter>Header Files</Filter> + </QtMoc> + <QtMoc Include="messagewebpage.h"> + <Filter>Header Files</Filter> + </QtMoc> + <QtMoc Include="animationhelpers.h"> <Filter>Header Files</Filter> </QtMoc> </ItemGroup> @@ -361,9 +361,6 @@ <ClInclude Include="ui_deleteaccountdialog.h"> <Filter>Generated Files</Filter> </ClInclude> - <ClInclude Include="ui_instantmessagingwidget.h"> - <Filter>Generated Files</Filter> - </ClInclude> <ClInclude Include="ui_mainwindow.h"> <Filter>Generated Files</Filter> </ClInclude> @@ -552,6 +549,27 @@ <None Include="translations\ring_client_windows_zh_TW.ts"> <Filter>Translation Files</Filter> </None> + <None Include="web\chatview.css"> + <Filter>Resource Files\web</Filter> + </None> + <None Include="web\chatview.html"> + <Filter>Resource Files\web</Filter> + </None> + <None Include="web\linkify.js"> + <Filter>Resource Files\web</Filter> + </None> + <None Include="web\linkify-html.js"> + <Filter>Resource Files\web</Filter> + </None> + <None Include="web\linkify-string.js"> + <Filter>Resource Files\web</Filter> + </None> + <None Include="web\chatview.js"> + <Filter>Resource Files\web</Filter> + </None> + <None Include="web\qwebchannel.js"> + <Filter>Resource Files\web</Filter> + </None> </ItemGroup> <ItemGroup> <QtUic Include="aboutdialog.ui"> @@ -581,9 +599,6 @@ <QtUic Include="deleteaccountdialog.ui"> <Filter>Form Files</Filter> </QtUic> - <QtUic Include="instantmessagingwidget.ui"> - <Filter>Form Files</Filter> - </QtUic> <QtUic Include="mainwindow.ui"> <Filter>Form Files</Filter> </QtUic> @@ -620,6 +635,9 @@ <QtUic Include="wizardwidget.ui"> <Filter>Form Files</Filter> </QtUic> + <QtUic Include="animatedoverlay.ui"> + <Filter>Form Files</Filter> + </QtUic> </ItemGroup> <ItemGroup> <None Include="images\FontAwesome.otf"> @@ -765,5 +783,11 @@ <ClInclude Include="lrcinstance.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="webchathelpers.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="version.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> </Project> \ No newline at end of file diff --git a/ringthemeutils.h b/ringthemeutils.h index 8b77e733b3f4f7d7d34e7a31e4e7217e16d039db..4536685069ab45269013e4bad6704ff520d47079 100644 --- a/ringthemeutils.h +++ b/ringthemeutils.h @@ -31,6 +31,7 @@ static const QColor red_ {251, 72, 71}; static const QColor lightRed_ {252, 91, 90}; static const QColor darkRed_ {219, 55, 54}; static const QColor notificationRed_{ 255, 59, 48 }; +static const QColor urgentOrange_{ 255, 165, 0 }; static const QColor green_ {127, 255, 0}; static const QColor presenceGreen_{ 76, 217, 100 }; static const QColor smartlistSelection_ { 237, 237, 237 }; diff --git a/settingskey.h b/settingskey.h index 8a2453ab43d2bdf50fceb8ac25bdc2fc2ff87844..429003133bef5b8a9f7a825df2c094e217f9e171 100644 --- a/settingskey.h +++ b/settingskey.h @@ -26,6 +26,7 @@ constexpr static char geometry[] = "geometry"; constexpr static char windowState[] = "windowState"; constexpr static char enableNotifications[] = "enableNotifications"; constexpr static char selectedAccount[] = "selectedAccount"; +constexpr static char mainSplitterState[] = "mainSplitterState"; } #define accountAutoAnswer(A) (A+SettingsKey::autoAnswer) diff --git a/smartlistmodel.cpp b/smartlistmodel.cpp index d767a0360e4e037aa52e86dfb552a61ad511a0a8..be10454b28c79c5bfc1e0b85fefe75962432c05e 100644 --- a/smartlistmodel.cpp +++ b/smartlistmodel.cpp @@ -29,7 +29,6 @@ // Client #include "pixbufmanipulator.h" -#include "messagemodel.h" #include "utils.h" SmartListModel::SmartListModel(const lrc::api::account::Info &acc, QObject *parent) @@ -95,6 +94,8 @@ QVariant SmartListModel::data(const QModelIndex &index, int role) const } case Role::LastInteraction: return QVariant(QString::fromStdString(item.interactions.at(item.lastMessageUid).body)); + case Role::LastInteractionType: + return QVariant(Utils::toUnderlyingValue(item.interactions.at(item.lastMessageUid).type)); case Role::ContactType: { auto& contact = acc_.contactModel->getContact(item.participants[0]); @@ -103,7 +104,7 @@ QVariant SmartListModel::data(const QModelIndex &index, int role) const case Role::UID: return QVariant(QString::fromStdString(item.uid)); case Role::ContextMenuOpen: - return QVariant(isContextMenuOpen_); + return QVariant(isContextMenuOpen); } } catch (...) {} } diff --git a/smartlistmodel.h b/smartlistmodel.h index 907159993ebdecbbad6594825133208d077d3387..1da0a655d5da9837c7fe1e05aaf85127e55d1b5d 100644 --- a/smartlistmodel.h +++ b/smartlistmodel.h @@ -25,8 +25,6 @@ #include "api/conversation.h" #include "api/contact.h" -#include "messagemodel.h" - namespace lrc { namespace api { class ConversationModel; } } class SmartListModel : public QAbstractItemModel @@ -47,6 +45,7 @@ public: UnreadMessagesCount, LastInteractionDate, LastInteraction, + LastInteractionType, ContactType, UID, ContextMenuOpen @@ -62,7 +61,8 @@ public: QModelIndex parent(const QModelIndex &child) const; Qt::ItemFlags flags(const QModelIndex &index) const; - bool isContextMenuOpen_{false}; + // hack for context menu highlight retention + bool isContextMenuOpen{ false }; private: const AccountInfo& acc_; diff --git a/smartlistview.cpp b/smartlistview.cpp index ae44423122c58667de061ecf65c92ccc22dcd330..4accf0504d6c7bb2d1b7dd648f0235c472863ca2 100644 --- a/smartlistview.cpp +++ b/smartlistview.cpp @@ -127,30 +127,18 @@ SmartListView::eventFilter(QObject* watched, QEvent* event) return QObject::eventFilter(watched, event); } -void -SmartListView::hideButtonsWidgets() -{ - auto model = this->model(); - if (!model) { - return; - } - for (int i = 0; i < model->rowCount(); ++i) { - auto index = model->index(i, 0); - if (index.isValid() && indexWidget(index)) { - qDebug() << "hide a ButtonsWidgets"; - indexWidget(index)->setVisible(false); - } - } -} - void SmartListView::drawRow(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { if (index == hoveredRow_ && indexWidget(hoveredRow_)) { indexWidget(index)->setVisible(true); - } - else if (indexWidget(index)) { - indexWidget(index)->setVisible(false); + } else if (indexWidget(index)) { + auto type = Utils::toEnum<lrc::api::profile::Type>( + index.data(static_cast<int>(SmartListModel::Role::ContactType)).value<int>() + ); + if (type == lrc::api::profile::Type::PENDING) { + indexWidget(index)->setVisible(false); + } } QTreeView::drawRow(painter, option, index); } \ No newline at end of file diff --git a/smartlistview.h b/smartlistview.h index 480109ccdf42a05c9870af09486ebbe4e8dcf270..126c1a23b41e181bb0ccdcd41b212d7bd05dbe40 100644 --- a/smartlistview.h +++ b/smartlistview.h @@ -21,8 +21,6 @@ #include <QTreeView> -class ConversationItemDelegate; - class SmartListView : public QTreeView { Q_OBJECT @@ -38,7 +36,6 @@ protected: void drawRow(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const; private: - void hideButtonsWidgets(); QModelIndex hoveredRow_; signals: diff --git a/stylesheet.css b/stylesheet.css index a1ef20a2649ff66feafc48c8add2e659ef9a706c..63150a8306c184d522c0887e00069a153b1e2879 100644 --- a/stylesheet.css +++ b/stylesheet.css @@ -26,7 +26,7 @@ QPushButton#refuseButton:pressed, QPushButton#cancelButton:pressed{ background-color: #db3c30; } -QPushButton#buttonConversations, QPushButton#buttonInvites { +QPushButton#btnConversations, QPushButton#btnInvites { background-color: rgb(242, 242, 242); border-style: solid; border-width: 0px; @@ -35,14 +35,18 @@ QPushButton#buttonConversations, QPushButton#buttonInvites { color: rgb(32, 32, 32); } -QPushButton#buttonConversations:hover, QPushButton#buttonInvites:hover { +QPushButton#btnConversations:hover, QPushButton#btnInvites:hover { background-color: rgb(237, 237, 237); } -QPushButton#buttonConversations:pressed, QPushButton#buttonInvites:pressed { +QPushButton#btnConversations:pressed, QPushButton#btnInvites:pressed { background-color: rgb(212, 212, 212); } +QPushButton#btnConversations:checked, QPushButton#btnInvites:checked { + background-color: rgb(237, 237, 237); +} + QPushButton#imBackButton, QPushButton#btnAcceptInvite, QPushButton#btnIgnoreInvite, QPushButton#btnBlockInvite, QPushButton#btnAudioCall, QPushButton#btnVideoCall, QPushButton#sendContactRequestButton, QPushButton#sendButton, QPushButton#sendIMButton { @@ -79,12 +83,6 @@ QWidget#callInvitePage, QWidget#outboundCallPage{ background-position: bottom; } -IdLabel{ - border-style: solid; - border-width: 1px; - border-color: rgb(0, 192, 212); -} - RingContactLineEdit{ border-color: rgb(242, 242, 242); border-radius: 5px; @@ -163,9 +161,8 @@ QListView#contactRequestList::item:hover, QListView#BannedList::item:hover { background-color: rgba(242, 242, 242, 255); } -QListView#listMessageView{ - background: rgb(255, 255, 255); - border-bottom: 2px solid rgb(240, 240, 240); +QWidget#sendIMWidget { + border-top: 2px solid rgb(240, 240, 240); } QWidget#messagingHeaderWidget{ @@ -175,6 +172,7 @@ QWidget#messagingHeaderWidget{ QLineEdit#messageEdit, QLineEdit#imMessageEdit{ border: none; background-color: rgb(255, 255, 255); + padding: 0px; } QLineEdit#numberBar{ @@ -248,21 +246,24 @@ QPushButton#videoCfgBtn:pressed{ } QToolButton#qrButton, QToolButton#shareButton{ - background-color: #3AC0D2; - border-radius: 15px; - border:solid 1px; + background-color: rgb(242, 242, 242); + border-style: solid; + border-width: 0px; + border-radius: 5px; + padding: 8px; + color: rgb(32, 32, 32); } QToolButton#qrButton:hover, QToolButton#shareButton:hover{ - background-color: #4dc6d6; + background-color: rgb(237, 237, 237); } QToolButton#qrButton:pressed, QToolButton#shareButton:pressed{ - background-color: #34acbd; + background-color: rgb(212, 212, 212); } QToolButton#qrButton:checked { - background-color: #34acbd; + background-color: rgb(237, 237, 237); } QPushButton#deleteAccountBtn, QToolButton#addAccountButton{ @@ -286,10 +287,7 @@ QScrollBar::handle:vertical{ QDialog#WizardDialog, QWidget#welcomePage, QWidget#sendContactRequestPage, QDialog#DeleteAccountDialog, QDialog#DeleteContactDialog{ - background: rgb(242, 242, 242); - background-image : url(:/images/background-light.png); - background-repeat : repeat-x; - background-position: bottom; + background: rgb(255, 255, 255); } QDialog#CallUtilsDialog, QDialog#QualityDialog{ diff --git a/touch_res.bat b/touch_res.bat new file mode 100644 index 0000000000000000000000000000000000000000..6b58825a7689e62f712603b9195cc74164253942 --- /dev/null +++ b/touch_res.bat @@ -0,0 +1 @@ +copy ressources.qrc /B+ ,,/Y \ No newline at end of file diff --git a/utils.cpp b/utils.cpp index 56ffb9cb0cb8a040c8931ce110e263a0515af488..870da114db25ddfc3e1344fbfe291c75122d9cbb 100644 --- a/utils.cpp +++ b/utils.cpp @@ -36,6 +36,7 @@ #include <QStackedWidget> #include <QPropertyAnimation> #include <QApplication> +#include <QFile> #include "globalinstances.h" #include "pixbufmanipulator.h" @@ -403,3 +404,15 @@ Utils::conversationPhoto(const std::string & convUid, const lrc::api::account::I QVariant var = GlobalInstances::pixmapManipulator().decorationRole(*conversation, accountInfo); return var.value<QImage>(); } + +QByteArray +Utils::QByteArrayFromFile(const QString& filename) +{ + QFile file(filename); + if (file.open(QIODevice::ReadOnly)) { + return file.readAll(); + } else { + qDebug() << "can't open file"; + return QByteArray(); + } +} \ No newline at end of file diff --git a/utils.h b/utils.h index ffa29226c420eeb4a583a1187fc27806c7cd22ca..c13abde9fdb75ed14fa5eab39aaf7476e1c38a23 100644 --- a/utils.h +++ b/utils.h @@ -37,6 +37,8 @@ #include <QString> #include <QImage> #include <QStackedWidget> +#include <QTextDocument> +#include <QItemDelegate> #include "api/conversationmodel.h" #include "api/account.h" @@ -74,8 +76,8 @@ namespace Utils bool isInteractionGenerated(const lrc::api::interaction::Type& interaction); bool isContactValid(const std::string& contactUid, const lrc::api::ConversationModel& model); QImage conversationPhoto(const std::string& convUid, const lrc::api::account::Info& accountInfo); + QByteArray QByteArrayFromFile(const QString& filename); - // helpers template<typename E> constexpr inline typename std::enable_if< std::is_enum<E>::value, typename std::underlying_type<E>::type @@ -104,4 +106,4 @@ namespace Utils } return std::distance(vec.begin(), it); } -} +} \ No newline at end of file diff --git a/version.h b/version.h new file mode 100644 index 0000000000000000000000000000000000000000..1ff8dd5c37ddd9bc18ebad90edf0192c4aaeea9e --- /dev/null +++ b/version.h @@ -0,0 +1,52 @@ +#pragma once + +#define BUILD_YEAR_CH0 (__DATE__[ 7]) +#define BUILD_YEAR_CH1 (__DATE__[ 8]) +#define BUILD_YEAR_CH2 (__DATE__[ 9]) +#define BUILD_YEAR_CH3 (__DATE__[10]) + + +#define BUILD_MONTH_IS_JAN (__DATE__[0] == 'J' && __DATE__[1] == 'a' && __DATE__[2] == 'n') +#define BUILD_MONTH_IS_FEB (__DATE__[0] == 'F') +#define BUILD_MONTH_IS_MAR (__DATE__[0] == 'M' && __DATE__[1] == 'a' && __DATE__[2] == 'r') +#define BUILD_MONTH_IS_APR (__DATE__[0] == 'A' && __DATE__[1] == 'p') +#define BUILD_MONTH_IS_MAY (__DATE__[0] == 'M' && __DATE__[1] == 'a' && __DATE__[2] == 'y') +#define BUILD_MONTH_IS_JUN (__DATE__[0] == 'J' && __DATE__[1] == 'u' && __DATE__[2] == 'n') +#define BUILD_MONTH_IS_JUL (__DATE__[0] == 'J' && __DATE__[1] == 'u' && __DATE__[2] == 'l') +#define BUILD_MONTH_IS_AUG (__DATE__[0] == 'A' && __DATE__[1] == 'u') +#define BUILD_MONTH_IS_SEP (__DATE__[0] == 'S') +#define BUILD_MONTH_IS_OCT (__DATE__[0] == 'O') +#define BUILD_MONTH_IS_NOV (__DATE__[0] == 'N') +#define BUILD_MONTH_IS_DEC (__DATE__[0] == 'D') + + +#define BUILD_MONTH_CH0 \ + ((BUILD_MONTH_IS_OCT || BUILD_MONTH_IS_NOV || BUILD_MONTH_IS_DEC) ? '1' : '0') + +#define BUILD_MONTH_CH1 \ + ( \ + (BUILD_MONTH_IS_JAN) ? '1' : \ + (BUILD_MONTH_IS_FEB) ? '2' : \ + (BUILD_MONTH_IS_MAR) ? '3' : \ + (BUILD_MONTH_IS_APR) ? '4' : \ + (BUILD_MONTH_IS_MAY) ? '5' : \ + (BUILD_MONTH_IS_JUN) ? '6' : \ + (BUILD_MONTH_IS_JUL) ? '7' : \ + (BUILD_MONTH_IS_AUG) ? '8' : \ + (BUILD_MONTH_IS_SEP) ? '9' : \ + (BUILD_MONTH_IS_OCT) ? '0' : \ + (BUILD_MONTH_IS_NOV) ? '1' : \ + (BUILD_MONTH_IS_DEC) ? '2' : \ + /* error default */ '?' \ + ) + +#define BUILD_DAY_CH0 ((__DATE__[4] >= '0') ? (__DATE__[4]) : '0') +#define BUILD_DAY_CH1 (__DATE__[ 5]) + +const char VERSION_STRING[] = +{ + BUILD_YEAR_CH0, BUILD_YEAR_CH1, BUILD_YEAR_CH2, BUILD_YEAR_CH3, + BUILD_MONTH_CH0, BUILD_MONTH_CH1, + BUILD_DAY_CH0, BUILD_DAY_CH1, + '\0' +}; \ No newline at end of file diff --git a/videoview.cpp b/videoview.cpp index 5a9c70a78649789059985d4b82fe03798b577004..36c0a3073b1b1b39cd786fd6cac435f93e151fe4 100644 --- a/videoview.cpp +++ b/videoview.cpp @@ -217,7 +217,8 @@ VideoView::dropEvent(QDropEvent* event) void VideoView::toggleFullScreen() { - overlay_->toggleContextButtons(isFullScreen()); + qDebug() << "toggle FS"; + /*overlay_->toggleContextButtons(isFullScreen()); if(isFullScreen()) { dynamic_cast<QSplitter*>(oldParent_)->insertWidget(0,this); this->resize(oldSize_.width(), oldSize_.height()); @@ -228,7 +229,7 @@ VideoView::toggleFullScreen() this->setParent(0); this->showFullScreen(); } - ui->videoWidget->setResetPreview(true); + ui->videoWidget->setResetPreview(true);*/ } void diff --git a/web/chatview.css b/web/chatview.css new file mode 100644 index 0000000000000000000000000000000000000000..5fa4156e23a1fff54083d5675d05819f664a3dce --- /dev/null +++ b/web/chatview.css @@ -0,0 +1,921 @@ +/** Variable and font definitions */ + +:root { + /* color definitions */ + --ring-light-blue: rgba(59, 193, 211, 0.3); + /* main properties */ + /* --bg-color: #f2f2f2; same as macOS client */ + --bg-color: #ffffff; /* same as macOS client */ + /* navbar properties */ + --navbar-height: 40px; + --navbar-padding-top: 8px; + --navbar-padding-bottom: var(--navbar-padding-top); + /* message bar properties */ + --textarea-max-height: 150px; + --placeholder-text-color: #d3d3d3; + /* button properties */ + --action-icon-color: #00; + --deactivated-icon-color: #bebebe; + --action-icon-hover-color: #ededed; + --action-critical-icon-hover-color: rgba(211, 77, 59, 0.3); /* complementary color of ring light blue */ + --action-critical-icon-press-color: rgba(211, 77, 59, 0.5); + --action-critical-icon-color: #4E1300; + --non-action-icon-color: #212121; + --action-icon-press-color: rgba(212, 212, 212, 1.0); + /* hairline properties */ + --hairline-color: #f0f0f0; + --hairline-thickness: 2px; +} + +@font-face { + font-family: emoji; + /* Fonts for text outside emoji blocks */ + src: local('Open sans'), local('Helvetica'), local('Segoe UI'), local('sans-serif'); +} + +@font-face { + font-family: emoji; + src: local('Noto Color Emoji'), local('Android Emoji'), local('Twitter Color Emoji'); + /* Emoji unicode blocks */ + unicode-range: U+1F300-1F5FF, U+1F600-1F64F, U+1F680-1F6FF, U+2600-26FF; +} + +/** Body */ + +body { + --messagebar-size: 57px; + margin: 0; + overflow: hidden; + background-color: var(--bg-color); + padding-bottom: var(--messagebar-size); + /* disable selection highlight because it looks very bad */ + -webkit-user-select: none; + opacity: 1; + transition: 0.5s opacity; +} + + body.fade { + opacity: 0; + transition: none; + } + +::-webkit-scrollbar-track { + background-color: var(--bg-color); +} + +::-webkit-scrollbar { + width: 8px; + background-color: var(--bg-color); +} + +::-webkit-scrollbar-thumb { + background-color: #f0f0f0; +} + + +/** Navbar */ + +.navbar-wrapper { + /* on top, over everything and full width */ + position: fixed; + left: 0; + right: 0; + z-index: 500; + top: 0; +} + +#navbar { + background-color: var(--bg-color); + padding-right: 8px; + padding-left: 8px; + padding-top: var(--navbar-padding-top); + padding-bottom: var(--navbar-padding-bottom); + height: var(--navbar-height); + overflow: hidden; + align-items: center; + /* takes whole width */ + left: 0; + right: 0; + /* hairline */ + border-bottom: var(--hairline-thickness) solid var(--hairline-color); + display: flex; +} + +.hiddenState { + /* Used to hide navbar and message bar */ + display: none !important; +} + +.svgicon { + display: block; + margin: auto; + height: 70%; +} + +.nav-button { + width: 30px; + height: 30px; + margin: 8px; + padding: 2px; + display: flex; + cursor: pointer; + align-self: center; + border-radius: 9px; +} + + .nav-button.deactivated { + width: 30px; + height: 30px; + margin: 8px; + padding: 2px; + align-self: center; + display: flex; + border-radius: 9px; + cursor: auto; + } + +.action-button svg { + fill: var(--action-icon-color); +} + +.action-button.deactivated svg { + fill: var(--deactivated-icon-color); +} + +.non-action-button svg { + fill: var(--non-action-icon-color); +} + +.non-action-button:hover, .action-button:hover { + background: var(--action-icon-hover-color); +} + +.non-action-button:active, .action-button:active { + background: var(--action-icon-press-color); +} + +.action-button.deactivated:hover, .action-button.deactivated:active { + background: none; +} + +.action-critical-button svg { + fill: var(--action-critical-icon-color); +} + +.action-critical-button:hover { + background: var(--action-critical-icon-hover-color); +} + +.action-critical-button:active { + background: var(--action-critical-icon-press-color); +} + +#callButtons { + display: flex; +} + +#navbar #addBannedContactButton, #navbar #addToConversationsButton { + display: none; +} + +#navbar.onBannedState #addToConvButton, #navbar.onBannedState #callButtons, #navbar.onBannedState #addToConversationsButton { + display: none; +} + +#navbar.onBannedState #addBannedContactButton { + display: flex; +} + +/** Invitation bar */ + +#invitation { + visibility: hidden; + background: var(--ring-light-blue); + position: absolute; + width: 100%; + /* hairline */ + border-bottom: var(--hairline-thickness) solid var(--hairline-color); +} + + #invitation #actions { + margin: 10px; + list-style: none; + display: flex; + align-items: center; + justify-content: center; + /* enable selection (it is globally disabled in the body) */ + -webkit-user-select: auto; + } + + #invitation #text h1 { + font-size: 1.5em; + } + + #invitation #text { + text-align: center; + width: 90%; + margin: auto; + margin-top: 10px; + font-size: 0.8em; + /* enable selection (it is globally disabled in the body) */ + -webkit-user-select: auto; + } + +.invitation-button, +.flat-button { + margin: 5px; + border: 0; + border-radius: 5px; + transition: all 0.3s ease; + color: #f9f9f9; + padding: 10px 20px 10px 20px; + vertical-align: middle; + cursor: pointer; +} + +.button-green { + background: #27ae60; +} + + .button-green:hover { + background: #1f8b4c; + } + +.button-red { + background: #dc3a37; +} + + .button-red:hover { + background: #b02e2c; + } + +/** Messaging bar */ + +#sendMessage { + background-color: var(--bg-color); + display: flex; + overflow: hidden; + padding: 4px; + align-items: center; + position: fixed; + left: 0; + right: 0; + z-index: 500; + bottom: 0; + /* hairline */ + border-top: var(--hairline-thickness) solid var(--hairline-color); +} + +#message { + font-family: emoji; + flex: 1; + background-color: var(--bg-color); + border: 0; + overflow-y: scroll; + color: black; + max-height: var(--textarea-max-height); + margin-right: 10px; + resize: none; + /* enable selection (it is globally disabled in the body) */ + -webkit-user-select: auto; +} + + #message:focus, + #message.focus { + outline: none; + } + +#container[disabled] { + background-color: #ccc; +} + +input[placeholder], [placeholder], *[placeholder] { + color: var(--placeholder-text-color); +} + +/** Main chat view */ + +#lazyloading-icon { + margin: auto; + margin-bottom: 10px; + margin-top: 5px; + vertical-align: center; + width: 30px; + display: flex; +} + +#container { + position: relative; + height: 100%; + width: 100%; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + /* When there are not enough messages to occupy full height of the + container, make sure they are displayed at the bottom, not at the + top. */ + justify-content: flex-end; +} + +a:link { + text-decoration: none; + color: #0e649b; + transition: all 0.2s ease-in-out; + border-bottom: dotted 1px; +} + +a:hover { + border: 0; +} + +#messages { + position: relative; + z-index: 0; + width: 100%; + overflow: hidden; + height: auto; + padding-top: 0.5em; +} + + #messages:hover { + overflow-y: overlay; + } + +.last_message { + /* The last message gets a bigger bottom padding so that it is not + "glued" to the message bar. */ + padding-bottom: 1.5em !important; +} + +/* General messages */ + +.internal_mes_wrapper { + max-width: 70%; + margin: 8px 0 0 0; + display: flex; + flex-direction: column; + /* If a message is smaller (in width) than the timestamp, do not fill + full width and pack message at the left. */ + align-items: flex-start; + align-content: flex-start; +} + +.message_out > .internal_mes_wrapper { + /* If message is in the outgoing direction, pack at the right. */ + align-items: flex-end; + align-content: flex-end; +} + +.message_wrapper { + max-width: calc(100% - 2em); + border-radius: 10px; + padding: 0.5em 1em 0.5em 1em; + position: relative; + display: flex; + flex-direction: row; +} + +.message_type_data_transfer .message_wrapper { + display: flex; + flex-direction: column; + padding: 0; + width: 450px; + max-width: none; +} + +.transfer_info_wrapper { + display: flex; + flex-direction: row; +} + +.message { + font: 0.875em emoji; + margin: 0; + display: flex; + justify-content: flex-start; + align-items: top; + overflow: hidden; + /* enable selection (it is globally disabled in the body) */ + -webkit-user-select: auto; +} + +.message_in { + padding-left: 25%; +} + +.message_out { + padding-right: 25%; + /* Message sent by the user should be displayed at the right side of + the screen. */ + flex-direction: row-reverse; +} + +.message_delivery_status { + margin: 10px 10px; + color: #A0A0A0; +} + +.message_sender { + display: none; +} + +.sender_image { + border-radius: 50%; + margin: 8px 10px 0px 10px; +} + +.message_out .message_wrapper { + border-top-right-radius: 0; + transform-origin: top right; +} + +.message_in .message_wrapper { + border-top-left-radius: 0; + transform-origin: top left; +} + +.message_out .message_wrapper { + background-color: #cfd8dc; +} + +.message_in .message_wrapper { + background-color: #cfebf5; +} + +.message_in .sender_image, +.message_out .sender_image { + visibility: hidden; +} + +.message_in.last_of_sequence .sender_image, +.message_in.single_message .sender_image { + visibility: visible; +} + +.message_in.last_of_sequence .sender_image { + margin-top: 2px; +} + +.message_in.middle_of_sequence .sender_image { + margin-top: 0px; +} + +.generated_message.message_in .message_wrapper, +.generated_message.message_out .message_wrapper { + background-color: transparent !important; + border-radius: 0px !important; +} + +.single_message.message_in .message_wrapper, +.single_message.message_out .message_wrapper { + border-radius: 20px 20px 20px 20px !important; +} + +.last_of_sequence.message_in .message_wrapper { + border-radius: 4px 20px 20px 20px; +} + +.first_of_sequence.message_in .message_wrapper { + border-radius: 20px 20px 20px 4px; +} + +.middle_of_sequence.message_in .message_wrapper { + border-radius: 4px 20px 20px 5px; +} + +.last_of_sequence.message_out .message_wrapper { + border-radius: 20px 4px 20px 20px; +} + + +.first_of_sequence.message_out .message_wrapper { + border-radius: 20px 20px 4px 20px; +} + +.middle_of_sequence.message_out .message_wrapper { + border-radius: 20px 5px 4px 20px; +} + +.middle_of_sequence .internal_mes_wrapper, +.last_of_sequence .internal_mes_wrapper, +.last_message .internal_mes_wrapper { + margin: 3px 0 0 0 !important; +} + +.message_out .sender_image { + margin: 8px; +} + +.first_of_sequence.message_out .internal_mes_wrapper, +.single_message.message_out .internal_mes_wrapper { + margin: 4px 0 0 0 !important; +} + +@-webkit-keyframes fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.timestamp { + display: inline-flex; + justify-content: flex-start; + align-self: stretch; + color: #333; + font-size: 10px; + padding: 5px; +} + +.timestamp_out { + flex-direction: row-reverse; +} + +.timestamp_action { + margin: auto; + padding: 0; + vertical-align: center; + opacity: 0; + transition: visibility 0.3s linear, opacity 0.3s linear; +} + +.message_type_contact .message_wrapper:hover .timestamp_action, +.message_type_call .message_wrapper:hover .timestamp_action { + opacity: 1; +} + +/* Ellipsis - dropdown menu */ + +input[type=checkbox] { + display: none; +} + +.menu_interaction { + margin: 5px; + padding: 10px; + padding-top: 0; + opacity: 0; + height: 20px; + transition: visibility 0.3s linear, opacity 0.3s linear; +} + +.message_type_contact .menu_interaction { + display: none; + visibility: hidden; +} + +.message_type_call .menu_interaction { + margin: auto; + padding: 0; + vertical-align: center; +} + + .message_type_call .menu_interaction .dropdown { + margin-top: -17px; + } + +.message:hover:not(.message_type_contact) .menu_interaction { + display: block; + opacity: 1; +} + +.dropdown { + display: none; + z-index: 1; + position: absolute; + background-color: #fff; + padding-top: 3px; + padding-bottom: 3px; +} + + .dropdown div { + color: #111; + padding: 10px; + } + + .dropdown div:hover { + background-color: #ddd; + } + +.showmenu:checked ~ .dropdown { + display: block; +} + +.menuoption { + user-select: none; + cursor: pointer; +} + +/* Buttons */ + +.flat-button { + flex: 1; + padding: 0; +} + +.left_buttons { + align-self: center; + max-width: 90px; + padding-left: 1em; +} + +/* Status */ + +.status_circle { + fill: #A0A0A0; + -webkit-animation: circle-dance; + -webkit-animation-duration: 0.8s; + -webkit-animation-iteration-count: infinite; + -webkit-animation-direction: alternate; + -webkit-animation-timing-function: ease-in-out; +} + +.anim-first { + -webkit-animation-delay: 0.7s; +} + +.anim-second { + -webkit-animation-delay: 0.9s; +} + +.anim-third { + -webkit-animation-delay: 1.1s; +} + +@-webkit-keyframes circle-dance { + 0%,50% { + -webkit-transform: translateY(0); + fill: #A0A0A0; + } + + 100% { + -webkit-transform: translateY(-8px); + fill: #000; + } +} + +.status-x { + stroke-dasharray: 12; +} + +/* Contact + Call interactions */ +.message_type_contact .message_wrapper, +.message_type_call .message_wrapper { + width: auto; + margin: auto; + display: flex; + flex-wrap: wrap; + background-color: var(--bg-color); + padding: 0; +} + + .message_type_contact .message_wrapper:before, + .message_type_call .message_wrapper:before { + display: none; + } + +.message_type_contact .text, +.message_type_call .text { + align-self: center; + font-size: 1.2em; + padding: 1em; +} + +/* file interactions */ + +.message_type_data_transfer .internal_mes_wrapper { + padding: 0; + display: flex; + flex-wrap: wrap; +} + +.accept, .refuse { + border-radius: 50%; + cursor: pointer; +} + + .accept svg, + .refuse svg { + padding: 8px; + width: 24px; + height: 24px; + } + +.accept { + fill: #219d55; +} + + .accept:hover { + fill: white; + background: #219d55; + } + +.refuse { + fill: #dc2719; +} + + .refuse:hover { + fill: white; + background: #dc2719; + } + +.message_type_data_transfer .text { + text-align: left; + align-self: left; + padding: 1em; +} + +.truncate-ellipsis { + display: table; + table-layout: fixed; + width: 100%; + white-space: nowrap; +} + + .truncate-ellipsis > * { + display: table-cell; + overflow: hidden; + text-overflow: ellipsis; + } + +.message_type_data_transfer .filename { + cursor: pointer; + font-size: 1.1em; +} + +.message_type_data_transfer .informations { + color: #555; + font-size: 0.8em; +} + +.message_progress_bar { + width: 100%; + height: 1em; + position: relative; + overflow: hidden; + background-color: #eee; + border-radius: 0; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset; +} + + .message_progress_bar > span { + display: inline; + height: 100%; + background-color: #01a2b8; + position: absolute; + overflow: hidden; + } + +/* text interactions */ + +.message_type_text .internal_mes_wrapper { + padding: 0px; +} + +.message_text { + word-break: break-all; + word-wrap: hyphenate; + max-width: 100%; +} + + .message_text pre { + display: inline; + } + +pre { + font: inherit; + font-family: inherit; + font-size: inherit; + font-style: inherit; + font-variant: inherit; + font-weight: inherit; + margin: 0; + padding: 0; + white-space: pre-wrap; +} + +/* Media interactions */ +.media_wrapper img { + max-width: 800px; + max-height: 700px; + margin: 5px 0 0 0; + border-radius: 10px; +} + +.playVideo { + background-color: rgba(0, 0, 0, 0.6); + height: 50px; + width: 50px; + border-radius: 5px; + float: right; + position: absolute; + top: calc(50% - 25px); + left: calc(50% - 25px); + z-index: 3; +} + +.containerVideo { + width: 100%; + position: relative; +} + +.playVideo svg { + height: 40px; + width: 40px; + margin: 5px; +} + +/* Text interaction */ +.failure, +.sending { + margin: 10px 10px; + color: #A0A0A0; +} + +/* classic screens */ +@media screen and (max-width: 1920px), screen and (max-height: 1080px) { + .message_in { + padding-left: 15%; + } + + .message_out { + padding-right: 15%; + } + + .internal_mes_wrapper { + max-width: 60%; + } + + .media_wrapper img { + /* It is perfectly fine to specify max-widths in px when the + wrapper's max-width is in %, as long as the max-width in px + doesn't exceed the one in %. */ + max-width: 450px; + max-height: 450px; + } + + .menu_interaction { + margin: 5px; + padding: 2px; + height: 10px; + font-size: 0.7em; + transition: visibility 0.3s linear,opacity 0.3s linear; + } +} + +/* lower resolutions */ +@media screen and (max-width: 1000px), screen and (max-height: 480px) { + .message_in { + padding-left: 0; + } + + .message_out { + padding-right: 0; + } + + .message_type_contact, + .message_type_call { + max-width: 100%; + } + + .internal_mes_wrapper { + max-width: 90%; + } + + /* Media interactions */ + .media_wrapper img { + max-width: 200px; + max-height: 200px; + } +} + +@media screen and (max-width: 550px) { + .message_type_data_transfer .message_wrapper { + width: 250px; + } +} + +/* Special case */ +@media screen and (max-width: 350px) { + .sender_image { + display: none; + } + + /* File interactions */ + .message_type_data_transfer .left_buttons { + max-width: 100%; + } + + .message_type_data_transfer .text { + max-width: 100%; + padding-left: 0; + } + + .message_type_data_transfer .message_wrapper { + width: 200px; + } +} diff --git a/web/chatview.html b/web/chatview.html new file mode 100644 index 0000000000000000000000000000000000000000..43fbb18f3892d871fb1da5f7274ffc6b44ace583 --- /dev/null +++ b/web/chatview.html @@ -0,0 +1,43 @@ +<html> +<!-- Empty head might be needed for setSenderImage --> +<head> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta charset="utf-8"> + <!--<link rel="stylesheet" href="chatview.css"> + <script type="text/javascript" src="linkify.js"></script> + <script type="text/javascript" src="linkify-html.js"></script> + <script type="text/javascript" src="linkify-string.js"></script> + <script type="text/javascript" src="qwebchannel.js"></script>--> +</head> +<body> + <div class="navbar-wrapper"> + <div id="invitation"> + <div id="text"> + </div> + <div id="actions"> + <div id="accept-btn" class="invitation-button button-green" onclick="acceptInvitation()">Accept</div> + <div id="refuse-btn" class="invitation-button button-red" onclick="refuseInvitation()">Refuse</div> + <div id="block-btn" class="invitation-button button-red" onclick="blockConversation()">Block</div> + </div> + </div> + </div> + <div id="container"> + <div id="messages" onscroll="onScrolled()"></div> + <div id="sendMessage"> + <div class="nav-button action-button" onclick="sendFile()" title="Send File"> + <svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" /> + <path d="M0 0h24v24H0z" fill="none" /> + </svg> + </div> + <textarea id="message" autofocus placeholder="Type a message" onkeyup="grow_text_area()" onkeydown="process_messagebar_keydown()" rows="1"></textarea> + <div class="nav-button action-button" onclick="sendMessage(); grow_text_area()" title="Send"> + <svg class="svgicon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" /> + <path d="M0 0h24v24H0z" fill="none" /> + </svg> + </div> + </div> + </div> +</body> +</html> diff --git a/web/chatview.js b/web/chatview.js new file mode 100644 index 0000000000000000000000000000000000000000..54994fbb5a6d2e5cd5a794a560fc607529c3458f --- /dev/null +++ b/web/chatview.js @@ -0,0 +1,1504 @@ +"use strict" + +/* Constants used at several places*/ +const messageBarPlaceHolder = "Type a message" +/* Constants used at several places*/ +// scrollDetectionThresh represents the number of pixels a user can scroll +// without disabling the automatic go-back-to-bottom when a new message is +// received +const scrollDetectionThresh = 200 +// printHistoryPart loads blocks of messages. Each block contains +// scrollBuffer messages +const scrollBuffer = 20 +// The first time a conversation is loaded, the lazy loading system makes +// sure at least initialScrollBufferFactor screens of messages are loaded +const initialScrollBufferFactor = 3 +// Some signal like the onscrolled signals are debounced so that the their +// assigned function isn't fired too often +const debounceTime = 200 + +/* Buffers */ +// current index in the history buffer +var historyBufferIndex = 0 +// buffer containing the conversation's messages +var historyBuffer = [] + +/* We retrieve refs to the most used navbar and message bar elements for efficiency purposes */ +/* NOTE: always use getElementById when possible, way more efficient */ +const messageBar = document.getElementById("sendMessage") +const messageBarInput = document.getElementById("message") +const invitation = document.getElementById("invitation") +const invitationText = document.getElementById("text") +var messages = document.getElementById("messages") + +/* States: allows us to avoid re-doing something if it isn't meaningful */ +var displayLinksEnabled = true +var hoverBackButtonAllowed = true +var hasInvitation = false +var isTemporary = false +var isBanned = false +var isAccountEnabled = true +var isInitialLoading = false +var imagesLoadingCounter = 0 + +/* Set the default target to _self and handle with QWebEnginePage::acceptNavigationRequest */ +var linkifyOptions = { + attributes: null, + className: 'linkified', + defaultProtocol: 'http', + events: null, + format: function (value, type) { + return value; + }, + formatHref: function (href, type) { + return href; + }, + ignoreTags: [], + nl2br: false, + tagName: 'a', + target: { + url: '_self' + }, + validate: true +}; + +new QWebChannel(qt.webChannelTransport, function(channel) { + window.jsbridge = channel.objects.jsbridge; +}); + +function onScrolled_() { + if (messages.scrollTop == 0 && historyBufferIndex != historyBuffer.length) { + /* At the top and there's something to print */ + printHistoryPart(messages, messages.scrollHeight) + } +} + +const debounce = (fn, time) => { + let timeout + + return function() { + const functionCall = () => fn.apply(this, arguments) + + clearTimeout(timeout) + timeout = setTimeout(functionCall, time) + } +} + +/* exported onScrolled */ +var onScrolled = debounce(onScrolled_, debounceTime) + +/** + * Generic wrapper. Execute passed function keeping scroll position identical. + * + * @param func function to execute + * @param args parameters as array + */ +function exec_keeping_scroll_position(func, args) { + var atEnd = messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh + func(...args) + if (atEnd) { + messages.scrollTop = messages.scrollHeight + } +} + +/** + * Reset scrollbar at a given position. + * @param scroll position at which the scrollbar should be set. + * Here position means the number of pixels scrolled, + * i.e. scroll = 0 resets the scrollbar at the bottom. + */ +function back_to_scroll(scroll) { + messages.scrollTop = messages.scrollHeight - scroll +} + +/** + * Reset scrollbar at bottom. + */ +function back_to_bottom() { + back_to_scroll(0) +} + +/** + * Hide or show invitation. + * + * Invitation is hidden if no contactAlias/invalid alias is passed. + * Otherwise, invitation div is updated. + * + * @param contactAlias + */ +/* exported showInvitation */ +function showInvitation(contactAlias) { + if (!contactAlias) { + if (hasInvitation) { + hasInvitation = false + invitation.style.visibility = "" + } + } else { + hasInvitation = true + invitationText.innerHTML = "<h1>" + contactAlias + " sends you an invitation</h1>" + + "Do you want to add them to the conversations list?<br>" + + "Note: you can automatically accept this invitation by sending a message." + invitation.style.visibility = "visible" + } +} + +/* exported setDisplayLinks */ +function setDisplayLinks(display) { + displayLinksEnabled = display +} + +/** + * This event handler dynamically resizes the message bar depending on the amount of + * text entered, while adjusting the body paddings so that that the message bar doesn't + * overlap messages when it grows. + */ +/* exported grow_text_area */ +function grow_text_area() { + exec_keeping_scroll_position(function(){ + var old_height = window.getComputedStyle(messageBar).height + messageBarInput.style.height = "auto" /* <-- necessary, no clue why */ + messageBarInput.style.height = messageBarInput.scrollHeight + "px" + var new_height = window.getComputedStyle(messageBar).height + + var msgbar_size = window.getComputedStyle(document.body).getPropertyValue("--messagebar-size") + var total_size = parseInt(msgbar_size) + parseInt(new_height) - parseInt(old_height) + + document.body.style.setProperty("--messagebar-size", total_size.toString() + "px") + }, []) +} + +/** + * This event handler processes keydown events from the message bar. When pressed key is + * the enter key, send the message unless shift or control was pressed too. + * + * @param key the pressed key + */ +/* exported process_messagebar_keydown */ +function process_messagebar_keydown(key) { + key = key || event + var map = {} + map[key.keyCode] = key.type == "keydown" + if (key.ctrlKey && map[13]) { + messageBarInput.value += "\n" + } + if (key.ctrlKey || key.shiftKey) { + return true + } + if (map[13]) { + sendMessage() + key.preventDefault() + } + return true +} + + +/** + * Disable or enable textarea. + * + * @param isDisabled whether message bar should be enabled or disabled + */ +/* exported disableSendMessage */ +function disableSendMessage(isDisabled) +{ + messageBarInput.disabled = isDisabled +} + +/* + * Update timestamps messages. + */ +function updateView() { + updateTimestamps(messages) +} + +setInterval(updateView, 60000) + +/** + * Transform a date to a string group like "1 hour ago". + * + * @param date + */ +function formatDate(date) { + const seconds = Math.floor((new Date() - date) / 1000) + var interval = Math.floor(seconds / (3600 * 24)) + if (interval > 5) { + return date.toLocaleDateString() + } + if (interval > 1) { + return interval + " days ago" + } + if (interval === 1) { + return interval + " day ago" + } + interval = Math.floor(seconds / 3600) + if (interval > 1) { + return interval + " hours ago" + } + if (interval === 1) { + return interval + " hour ago" + } + interval = Math.floor(seconds / 60) + if (interval > 1) { + return interval + " minutes ago" + } + return "just now" +} + +/** + * Send content of message bar + */ +function sendMessage() +{ + var message = messageBarInput.value + if (message.length > 0) { + messageBarInput.value = "" + window.jsbridge.sendMessage(message) + } +} + +/* exported sendFile */ +function sendFile() +{ + window.jsbridge.sendFile(); +} + +/** + * Clear all messages. + */ +/* exported clearMessages */ +function clearMessages() +{ + while (messages.firstChild) { + messages.removeChild(messages.firstChild) + } +} + +/** + * Convert text to HTML. + */ +function escapeHtml(html) +{ + var text = document.createTextNode(html) + var div = document.createElement("div") + div.appendChild(text) + return div.innerHTML +} + + +/** + * Get the youtube video id from a URL. + * @param url + */ +function youtube_id(url) { + const regExp = /^.*(youtu\.be\/|v\/|\/u\/w|embed\/|watch\?v=|&v=)([^#&?]*).*/ + const match = url.match(regExp) + return (match && match[2].length == 11) ? match[2] : null +} + +/** + * Returns HTML message from the message text, cleaned and linkified. + * @param message_text + */ +function getMessageHtml(message_text) +{ + const escaped_message = escapeHtml(message_text) + + var linkified_message = linkifyHtml(escaped_message, linkifyOptions) // eslint-disable-line no-undef + + const textPart = document.createElement("pre") + textPart.innerHTML = linkified_message + + return textPart.outerHTML +} + +/** + * Returns the message status, formatted for display + * @param message_delivery_status + */ +/* exported getMessageDeliveryStatusText */ +function getMessageDeliveryStatusText(message_delivery_status) +{ + var formatted_delivery_status = message_delivery_status + + switch(message_delivery_status) + { + case "sending": + case "ongoing": + formatted_delivery_status = "Sending<svg overflow='visible' viewBox='0 -2 16 14' height='16px' width='16px'><circle class='status_circle anim-first' cx='4' cy='12' r='1'/><circle class='status_circle anim-second' cx='8' cy='12' r='1'/><circle class='status_circle anim-third' cx='12' cy='12' r='1'/></svg>" + break + case "failure": + formatted_delivery_status = "Failure <svg overflow='visible' viewBox='0 -2 16 14' height='16px' width='16px'><path class='status-x x-first' stroke='#AA0000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' fill='none' d='M4,4 L12,12'/><path class='status-x x-second' stroke='#AA0000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' fill='none' d='M12,4 L4,12'/></svg>" + break + case "sent": + case "finished": + case "unknown": + case "read": + formatted_delivery_status = "" + break + default: + break + } + + return formatted_delivery_status +} + +/** + * Returns the message date, formatted for display + */ +function getMessageTimestampText(message_timestamp, custom_format) +{ + const date = new Date(1000 * message_timestamp) + if(custom_format) { + return formatDate(date) + } else { + return date.toLocaleString() + } +} + +/** + * Update timestamps. + * @param message_div + */ +function updateTimestamps(messages_div) { + const timestamps = messages_div.getElementsByClassName("timestamp") + for (var c = timestamps.length - 1; c >= 0 ; --c) { + var timestamp = timestamps.item(c) + timestamp.innerHTML = getMessageTimestampText(timestamp.getAttribute("message_timestamp"), true) + } +} + +/** + * Convert a value in filesize + */ +function humanFileSize(bytes) { + var thresh = 1024 + if(Math.abs(bytes) < thresh) { + return bytes + " B" + } + var units = ["kB","MB","GB","TB","PB","EB","ZB","YB"] + var u = -1 + do { + bytes /= thresh + ++u + } while(Math.abs(bytes) >= thresh && u < units.length - 1) + return bytes.toFixed(1)+" "+units[u] +} + +/** + * Hide or show add to conversations/calls whether the account is enabled + * @param accountEnabled true if account is enabled + */ +function hideControls(accountEnabled) { + if (!accountEnabled) { + callButtons.display = "none" + } else { + callButtons.display = "" + } +} + +/** + * Change the value of the progress bar. + * + * @param progress_bar + * @param message_object + */ +function updateProgressBar(progress_bar, message_object) { + var delivery_status = message_object["delivery_status"] + if ("progress" in message_object && !isErrorStatus(delivery_status) && message_object["progress"] !== 100) { + var progress_percent = (100 * message_object["progress"] / message_object["totalSize"]) + if (progress_percent !== 100) + progress_bar.childNodes[0].setAttribute("style", "width: " + progress_percent + "%") + else + progress_bar.setAttribute("style", "display: none") + } else + progress_bar.setAttribute("style", "display: none") +} + +/** + * Check if a status is an error status + * @param + */ +function isErrorStatus(status) { + return (status === "failure" + || status === "awaiting peer timeout" + || status === "canceled" + || status === "unjoinable peer") +} + +/** + * Build a new file interaction + * @param message_id + */ +function fileInteraction(message_id) { + var message_wrapper = document.createElement("div") + message_wrapper.setAttribute("class", "message_wrapper") + + var transfer_info_wrapper = document.createElement("div") + transfer_info_wrapper.setAttribute("class", "transfer_info_wrapper") + message_wrapper.appendChild(transfer_info_wrapper) + + /* Buttons at the left for status information or accept/refuse actions. + The text is bold and clickable. */ + var left_buttons = document.createElement("div") + left_buttons.setAttribute("class", "left_buttons") + transfer_info_wrapper.appendChild(left_buttons) + + var full_div = document.createElement("div") + full_div.setAttribute("class", "full") + full_div.style.visibility = "hidden" + full_div.style.display = "none" + + var filename_wrapper = document.createElement("div") + filename_wrapper.setAttribute("class", "truncate-ellipsis") + + var message_text = document.createElement("span") + message_text.setAttribute("class", "filename") + filename_wrapper.appendChild(message_text) + + // And information like size or error message. + var informations_div = document.createElement("div") + informations_div.setAttribute("class", "informations") + + var text_div = document.createElement("div") + text_div.setAttribute("class", "text") + text_div.addEventListener("click", function () { + // ask ring to open the file + const filename = document.querySelector("#message_" + message_id + " .full").innerText + window.jsbridge.openFile(filename); + }) + + text_div.appendChild(filename_wrapper) + text_div.appendChild(full_div) + text_div.appendChild(informations_div) + transfer_info_wrapper.appendChild(text_div) + + // And finally, a progress bar + var message_transfer_progress_bar = document.createElement("span") + message_transfer_progress_bar.setAttribute("class", "message_progress_bar") + + var message_transfer_progress_completion = document.createElement("span") + message_transfer_progress_bar.appendChild(message_transfer_progress_completion) + message_wrapper.appendChild(message_transfer_progress_bar) + + const internal_mes_wrapper = document.createElement("div") + internal_mes_wrapper.setAttribute("class", "internal_mes_wrapper") + internal_mes_wrapper.appendChild(message_wrapper) + + return internal_mes_wrapper +} + +/** + * Build information text for passed file interaction message object + * + * @param message_object message object containing file interaction info + */ +function buildFileInformationText(message_object) { + var informations_txt = getMessageTimestampText(message_object["timestamp"], true) + if (message_object["totalSize"] && message_object["progress"]) { + if (message_object["delivery_status"] === "finished") { + informations_txt += " - " + humanFileSize(message_object["totalSize"]) + } else { + informations_txt += " - " + humanFileSize(message_object["progress"]) + + " / " + humanFileSize(message_object["totalSize"]) + } + } + + return informations_txt + " - " + message_object["delivery_status"] +} + +/** + * Update a file interaction (icons + filename + status + progress bar) + * + * @param message_div the message to update + * @param message_object new informations + * @param forceTypeToFile + */ +function updateFileInteraction(message_div, message_object, forceTypeToFile = false) { + if (!message_div.querySelector(".informations")) return // media + + var acceptSvg = "<svg height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z\"/></svg>", + refuseSvg = "<svg height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z\"/><path d=\"M0 0h24v24H0z\" fill=\"none\"/></svg>", + fileSvg = "<svg fill=\"#000000\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z\"/><path d=\"M0 0h24v24H0z\" fill=\"none\"/></svg>", + warningSvg = "<svg fill=\"#000000\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z\"/></svg>" + var message_delivery_status = message_object["delivery_status"] + var message_direction = message_object["direction"] + var message_id = message_object["id"] + var message_text = message_object["text"] + + + if (isImage(message_text) && message_delivery_status === "finished" && displayLinksEnabled && !forceTypeToFile) { + // Replace the old wrapper by the downloaded image + var old_wrapper = message_div.querySelector(".internal_mes_wrapper") + if (old_wrapper) { + old_wrapper.parentNode.removeChild(old_wrapper) + } + + var errorHandler = function() { + var wrapper = message_div.querySelector(".internal_mes_wrapper") + var message_wrapper = message_div.querySelector(".message_wrapper") + if (message_wrapper) { + message_wrapper.parentNode.removeChild(message_wrapper) + } + + var media_wrapper = message_div.querySelector(".media_wrapper") + if (media_wrapper) { + media_wrapper.parentNode.removeChild(media_wrapper) + } + + var new_interaction = fileInteraction(message_id) + var new_message_wrapper = new_interaction.querySelector(".message_wrapper") + wrapper.prepend(new_message_wrapper) + updateFileInteraction(message_div, message_object, true) + } + + var new_wrapper = mediaInteraction(message_id, message_text, null, errorHandler) + message_div.insertBefore(new_wrapper, message_div.querySelector(".menu_interaction")) + message_div.querySelector("img").id = message_id + message_div.querySelector("img").msg_obj = message_object + return + } + + // Set informations text + var informations_div = message_div.querySelector(".informations") + informations_div.innerText = buildFileInformationText(message_object) + + // Update flat buttons + var left_buttons = message_div.querySelector(".left_buttons") + left_buttons.innerHTML = "" + if (message_delivery_status === "awaiting peer" || + message_delivery_status === "awaiting host" || + message_delivery_status.indexOf("ongoing") === 0) { + + if (message_direction === "in" && message_delivery_status.indexOf("ongoing") !== 0) { + // add buttons to accept or refuse a call. + var accept_button = document.createElement("div") + accept_button.innerHTML = acceptSvg + accept_button.setAttribute("title", "Accept") + accept_button.setAttribute("class", "flat-button accept") + accept_button.onclick = function() { + window.jsbridge.acceptFile(message_id); + } + left_buttons.appendChild(accept_button) + } + + var refuse_button = document.createElement("div") + refuse_button.innerHTML = refuseSvg + refuse_button.setAttribute("title", "Refuse") + refuse_button.setAttribute("class", "flat-button refuse") + refuse_button.onclick = function() { + window.jsbridge.refuseFile(message_id); + } + left_buttons.appendChild(refuse_button) + } else { + var status_button = document.createElement("div") + var statusFile = fileSvg + if (isErrorStatus(message_delivery_status)) + statusFile = warningSvg + status_button.innerHTML = statusFile + status_button.setAttribute("class", "flat-button") + left_buttons.appendChild(status_button) + } + + message_div.querySelector(".full").innerText = message_text + message_div.querySelector(".filename").innerText = message_text.split("/").pop() + updateProgressBar(message_div.querySelector(".message_progress_bar"), message_object) +} + +/** + * Return if a file is an image + * @param file + */ +function isImage(file) { + return file.toLowerCase().match(/\.(jpeg|jpg|gif|png)$/) !== null +} + +/** + * Return if a file is a youtube video + * @param file + */ +function isVideo(file) { + const availableProtocols = ["http:", "https:"] + const videoHostname = ["youtube.com", "www.youtube.com", "youtu.be"] + const urlParser = document.createElement("a") + urlParser.href = file + return (availableProtocols.includes(urlParser.protocol) && videoHostname.includes(urlParser.hostname)) +} + +/** + * Build a container for passed video thumbnail + * @param linkElt video thumbnail div + */ +function buildVideoContainer(linkElt) { + const containerElt = document.createElement("div") + containerElt.setAttribute("class", "containerVideo") + const playDiv = document.createElement("div") + playDiv.setAttribute("class", "playVideo") + playDiv.innerHTML = "<svg fill=\"#ffffff\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\ + <path d=\"M8 5v14l11-7z\"/>\ + <path d=\"M0 0h24v24H0z\" fill=\"none\"/>\ + </svg>" + linkElt.appendChild(playDiv) + containerElt.appendChild(linkElt) + + return containerElt +} + +/** + * Try to show an image or a video link (youtube for now) + * @param message_id + * @param link to show + * @param ytid if it's a youtube video + * @param errorHandler the new media's onerror field will be set to this function + */ +function mediaInteraction(message_id, link, ytid, errorHandler) { + /* TODO promise? + Try to display images. */ + const media_wrapper = document.createElement("div") + media_wrapper.setAttribute("class", "media_wrapper") + const linkElt = document.createElement("a") + linkElt.href = link + linkElt.style.textDecoration = "none" + linkElt.style.border = "none" + const imageElt = document.createElement("img") + + imageElt.src = ytid ? `http://img.youtube.com/vi/${ytid}/0.jpg` : link + + /* Note, here, we don't check the size of the image. + in the future, we can check the content-type and content-length with a request + and maybe disable svg */ + + if (isInitialLoading) { + /* During initial load, make sure the scrollbar stays at the bottom. + Also, the final scrollHeight is only known after the last image was + loaded. We want to display a specific number of messages screens so + we have to set up a callback (on_image_load_finished) which will + check on that and reschedule a new display batch if not enough + messages have been loaded in the DOM. */ + imagesLoadingCounter++ + imageElt.onload = function() { + back_to_bottom() + on_image_load_finished() + } + + if (errorHandler) { + imageElt.onerror = function() { + errorHandler() + back_to_bottom() + on_image_load_finished() + } + } + } else if (messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh) { + /* Keep the scrollbar at the bottom. Images are loaded asynchronously and + the scrollbar position is changed each time an image is loaded and displayed. + In order to make sure the scrollbar stays at the bottom, reset scrollbar + position each time an image was loaded. */ + imageElt.onload = back_to_bottom + + if (errorHandler) { + imageElt.onerror = function() { + errorHandler() + back_to_bottom() + } + } + } else if (errorHandler) { + imageElt.onerror = errorHandler + } + + linkElt.appendChild(imageElt) + + if (ytid) { + media_wrapper.appendChild(buildVideoContainer(linkElt)) + } else { + media_wrapper.appendChild(linkElt) + } + + const internal_mes_wrapper = document.createElement("div") + internal_mes_wrapper.setAttribute("class", "internal_mes_wrapper") + internal_mes_wrapper.appendChild(media_wrapper) + + return internal_mes_wrapper +} + +/** + * Build a new text interaction + * @param message_id + * @param htmlText the DOM to show + */ +function textInteraction(message_id, htmlText) { + const message_wrapper = document.createElement("div") + message_wrapper.setAttribute("class", "message_wrapper") + var message_text = document.createElement("div") + message_text.setAttribute("class", "message_text") + message_text.innerHTML = htmlText + message_wrapper.appendChild(message_text) + // TODO STATUS + + const internal_mes_wrapper = document.createElement("div") + internal_mes_wrapper.setAttribute("class", "internal_mes_wrapper") + internal_mes_wrapper.appendChild(message_wrapper) + + return internal_mes_wrapper +} + +/** + * Update a text interaction (text) + * @param message_div the message to update + * @param delivery_status the status of the message + */ +function updateTextInteraction(message_div, delivery_status) { + if (!message_div.querySelector(".message_text")) return // media + var sending = message_div.querySelector(".sending") + switch(delivery_status) + { + case "ongoing": + case "sending": + if (!sending) { + sending = document.createElement("div") + sending.setAttribute("class", "sending") + sending.innerHTML = "<svg overflow=\"hidden\" viewBox=\"0 -2 16 14\" height=\"16px\" width=\"16px\"><circle class=\"status_circle anim-first\" cx=\"4\" cy=\"12\" r=\"1\"/><circle class=\"status_circle anim-second\" cx=\"8\" cy=\"12\" r=\"1\"/><circle class=\"status_circle anim-third\" cx=\"12\" cy=\"12\" r=\"1\"/></svg>" + // add sending animation to message; + message_div.insertBefore(sending, message_div.querySelector(".menu_interaction")) + } + message_div.querySelector(".message_text").style.color = "#888" + break + case "failure": + // change text color to red + message_div.querySelector(".message_text").color = "#000" + var failure_div = message_div.querySelector(".failure") + if (!failure_div) { + failure_div = document.createElement("div") + failure_div.setAttribute("class", "failure") + failure_div.innerHTML = "<svg overflow=\"visible\" viewBox=\"0 -2 16 14\" height=\"16px\" width=\"16px\"><path class=\"status-x x-first\" stroke=\"#AA0000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"3\" fill=\"none\" d=\"M4,4 L12,12\"/><path class=\"status-x x-second\" stroke=\"#AA0000\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"3\" fill=\"none\" d=\"M12,4 L4,12\"/></svg>" + // add failure animation to message + message_div.insertBefore(failure_div, message_div.querySelector(".menu_interaction")) + } + message_div.querySelector(".message_text").style.color = "#000" + if (sending) sending.style.display = "none" + break + case "sent": + case "finished": + case "unknown": + case "read": + // change text color to black + message_div.querySelector(".message_text").style.color = "#000" + if (sending) sending.style.display = "none" + break + default: + break + } +} + +/** + * Build a new interaction (call or contact) + */ +function actionInteraction() { + var message_wrapper = document.createElement("div") + message_wrapper.setAttribute("class", "message_wrapper") + + // A file interaction contains buttons at the left of the interaction + // for the status or accept/refuse buttons + var left_buttons = document.createElement("div") + left_buttons.setAttribute("class", "left_buttons") + message_wrapper.appendChild(left_buttons) + // Also contains a bold clickable text + var text_div = document.createElement("div") + text_div.setAttribute("class", "text") + message_wrapper.appendChild(text_div) + return message_wrapper +} + +/** + * Update a call interaction (icon + text) + * @param message_div the message to update + * @param message_object new informations + */ +function updateCallInteraction(message_div, message_object) { + const outgoingCall = "<svg fill=\"#219d55\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5z\"/></svg>" + const callMissed = "<svg fill=\"#dc2719\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M19.59 7L12 14.59 6.41 9H11V7H3v8h2v-4.59l7 7 9-9z\"/></svg>" + const outgoingMissed = "<svg fill=\"#dc2719\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"><defs><path d=\"M24 24H0V0h24v24z\" id=\"a\"/></defs><clipPath id=\"b\"><use overflow=\"visible\" xlink:href=\"#a\"/></clipPath><path clip-path=\"url(#b)\" d=\"M3 8.41l9 9 7-7V15h2V7h-8v2h4.59L12 14.59 4.41 7 3 8.41z\"/></svg>" + const callReceived = "<svg fill=\"#219d55\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M20 5.41L18.59 4 7 15.59V9H5v10h10v-2H8.41z\"/></svg>" + + const message_text = message_object["text"] + const message_direction = (message_text.toLowerCase().indexOf("incoming") !== -1) ? "in" : "out" + const missed = message_text.indexOf("Missed") !== -1 + + message_div.querySelector(".text").innerText = message_text.substring(2) + + var left_buttons = message_div.querySelector(".left_buttons") + left_buttons.innerHTML = "" + var status_button = document.createElement("div") + var statusFile = "" + if (missed) + statusFile = (message_direction === "in") ? callMissed : outgoingMissed + else + statusFile = (message_direction === "in") ? callReceived : outgoingCall + status_button.innerHTML = statusFile + status_button.setAttribute("class", "flat-button") + left_buttons.appendChild(status_button) +} + +/** + * Update a contact interaction (icon + text) + * @param message_div the message to update + * @param message_object new informations + */ +function updateContactInteraction(message_div, message_object) { + const message_text = message_object["text"] + + message_div.querySelector(".text").innerText = message_text + + var left_buttons = message_div.querySelector(".left_buttons") + left_buttons.innerHTML = "" + var status_button = document.createElement("div") + status_button.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\ +<path d=\"M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z\"/>\ +<path d=\"M0 0h24v24H0z\" fill=\"none\"/></svg>" + status_button.setAttribute("class", "flat-button") + left_buttons.appendChild(status_button) +} + +/** + * Remove an interaction from the conversation + * @param interaction_id + */ +/* exported removeInteraction */ +function removeInteraction(interaction_id) { + var interaction = document.getElementById(`message_${interaction_id}`) + if (!interaction) { + return + } + + if (interaction.previousSibling) { + /* if element was the most recently received message, make sure the + last_message property is given away to the previous sibling */ + if (interaction.classList.contains("last_message")) { + interaction.previousSibling.classList.add("last_message") + } + + /* same for timestamp */ + var timestamp = interaction.querySelector(".timestamp") + var previousTimeStamp = interaction.previousSibling.querySelector(".timestamp") + if (timestamp && !previousTimeStamp) { + interaction.previousSibling.querySelector(".internal_mes_wrapper").appendChild(timestamp) + } + } + + var firstMessage = getPreviousInteraction(interaction) + var secondMessage = getNextInteraction(interaction) + + updateSequencing(firstMessage, secondMessage) + + interaction.parentNode.removeChild(interaction) +} + +/** + * Build message dropdown + * @return a message dropdown for passed message id + */ +function buildMessageDropdown(message_id) { + const menu_element = document.createElement("div") + menu_element.setAttribute("class", "menu_interaction") + menu_element.innerHTML = + `<input type="checkbox" id="showmenu${message_id}" class="showmenu"> + <label for="showmenu${message_id}"> + <svg fill="#888888" height="12" viewBox="0 0 24 24" width="12" xmlns="http://www.w3.org/2000/svg"> + <path d="M0 0h24v24H0z" fill="none"/> + <path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/> + </svg> + </label>` + menu_element.onclick = function() { + const button = this.querySelector(".showmenu") + button.checked = !button.checked + } + menu_element.onmouseleave = function() { + const button = this.querySelector(".showmenu") + button.checked = false + } + const dropdown = document.createElement("div") + const dropdown_classes = [ + "dropdown", + `dropdown_${message_id}` + ] + dropdown.setAttribute("class", dropdown_classes.join(" ")) + + const remove = document.createElement("div") + remove.setAttribute("class", "menuoption") + remove.innerHTML = "Delete" + remove.msg_id = message_id + remove.onclick = function() { + window.jsbridge.deleteInteraction(`${this.msg_id}`); + } + dropdown.appendChild(remove) + menu_element.appendChild(dropdown) + + return menu_element +} + +/** + * Build a message div for passed message object + * @param message_object to treat + */ +function buildNewMessage(message_object) { + const message_id = message_object["id"] + const message_type = message_object["type"] + const message_text = message_object["text"] + const message_direction = message_object["direction"] + const delivery_status = message_object["delivery_status"] + const message_sender_contact_method = message_object["sender_contact_method"] + + var classes = [ + "message", + `message_${message_direction}`, + `message_type_${message_type}` + ] + + var type = "" + var message_div = document.createElement("div") + message_div.setAttribute("id", `message_${message_id}`) + message_div.setAttribute("class", classes.join(" ")) + + // Build message for each types. + // Add sender images if necessary (like if the interaction doesn't take the whole width) + const need_sender = (message_type === "data_transfer" || message_type === "text") + if (need_sender) { + var message_sender_image = document.createElement("span") + message_sender_image.setAttribute("class", `sender_image sender_image_${message_sender_contact_method}`) + message_div.appendChild(message_sender_image) + } + + // Build main content + if (message_type === "data_transfer") { + if (isImage(message_text) && delivery_status === "finished" && displayLinksEnabled) { + var errorHandler = function() { + var wrapper = message_div.querySelector(".internal_mes_wrapper") + var message_wrapper = message_div.querySelector(".message_wrapper") + if (message_wrapper) { + message_wrapper.parentNode.removeChild(message_wrapper) + } + + var media_wrapper = message_div.querySelector(".media_wrapper") + if (media_wrapper) { + media_wrapper.parentNode.removeChild(media_wrapper) + } + + var new_interaction = fileInteraction(message_id) + var new_message_wrapper = new_interaction.querySelector(".message_wrapper") + wrapper.prepend(new_message_wrapper) + updateFileInteraction(message_div, message_object, true) + } + message_div.append(mediaInteraction(message_id, message_text, null, errorHandler)) + message_div.querySelector("img").id = message_id + message_div.querySelector("img").msg_obj = message_object + } else { + message_div.append(fileInteraction(message_id)) + } + } else if (message_type === "text") { + // TODO add the possibility to update messages (remove and rebuild) + const htmlText = getMessageHtml(message_text) + if (displayLinksEnabled) { + const parser = new DOMParser() + const DOMMsg = parser.parseFromString(htmlText, "text/xml") + const links = DOMMsg.querySelectorAll("a") + if (DOMMsg.childNodes.length && links.length) { + var isTextToShow = (DOMMsg.childNodes[0].childNodes.length > 1) + const ytid = (isVideo(message_text))? youtube_id(message_text) : "" + if (!isTextToShow && (ytid || isImage(message_text))) { + type = "media" + message_div.append(mediaInteraction(message_id, message_text, ytid)) + } + } + } + if (type !== "media") { + type = "text" + message_div.append(textInteraction(message_id, htmlText)) + } + } else if (message_type === "call" || message_type === "contact") { + message_div.append(actionInteraction()) + } else { + const temp = document.createElement("div") + temp.innerText = message_type + message_div.appendChild(temp) + } + + var message_dropdown = buildMessageDropdown(message_id) + if (message_type !== "call") { + message_div.appendChild(message_dropdown) + } else { + var wrapper = message_div.querySelector(".message_wrapper") + wrapper.insertBefore(message_dropdown, wrapper.firstChild) + } + + return message_div +} + +/** + * Build a timestamp for passed message object + * @param message_object to treat + */ +function buildNewTimestamp(message_object) { + const message_type = message_object["type"] + const message_direction = message_object["direction"] + const message_timestamp = message_object["timestamp"] + + const formattedTimestamp = getMessageTimestampText(message_timestamp, true) + const date_elt = document.createElement("div") + + date_elt.innerText = formattedTimestamp + var typeIsCallOrContact = (message_type === "call" || message_type === "contact") + var timestamp_div_classes = ["timestamp", typeIsCallOrContact ? "timestamp_action" : `timestamp_${message_direction}`] + date_elt.setAttribute("class", timestamp_div_classes.join(" ")) + date_elt.setAttribute("message_timestamp", message_timestamp) + + return date_elt +} + +/** + * Add a message to the conversation. + * @param message_object to treat + * @param new_message if it's a new message or if we need to update + * @param insert_after if we want the message at the end or the top of the conversation + * @param messages_div + */ +function addOrUpdateMessage(message_object, new_message, insert_after = true, messages_div) { + const message_id = message_object["id"] + const message_type = message_object["type"] + const message_direction = message_object["direction"] + const delivery_status = message_object["delivery_status"] + + var message_div = messages_div.querySelector("#message_" + message_id) + if (new_message) { + message_div = buildNewMessage(message_object) + + /* Show timestamp if either: + - message has type call or contact + - or most recently added timestamp in this set is different + - or message is the first message in this set */ + + var date_elt = buildNewTimestamp(message_object) + var timestamp = messages_div.querySelector(".timestamp") + + if (message_type === "call" || message_type === "contact") { + message_div.querySelector(".message_wrapper").appendChild(date_elt) + } else if (insert_after || !timestamp || timestamp.className !== date_elt.className + || timestamp.innerHTML !== date_elt.innerHTML) { + message_div.querySelector(".internal_mes_wrapper").appendChild(date_elt) + } + + var isGenerated = message_type === "call" || message_type === "contact" + if (isGenerated) { + message_div.classList.add("generated_message") + } + + if (insert_after) { + var previousMessage = messages_div.lastChild + messages_div.appendChild(message_div) + computeSequencing(previousMessage, message_div, null, insert_after) + if (previousMessage) { + previousMessage.classList.remove("last_message") + } + message_div.classList.add("last_message") + + /* When inserting at the bottom we should also check that the + previously sent message does not have the same timestamp. + If it's the case, remove it.*/ + var previous_timestamp = message_div.previousSibling.querySelector(".timestamp") + if (previous_timestamp && + previous_timestamp.className === date_elt.className && + previous_timestamp.innerHTML === date_elt.innerHTML && + !message_div.previousSibling.classList.contains("last_of_sequence")) { + previous_timestamp.parentNode.removeChild(previous_timestamp) + } + } else { + var nextMessage = messages_div.firstChild + messages_div.prepend(message_div) + computeSequencing(message_div, nextMessage, null, insert_after) + } + } + + if (isErrorStatus(delivery_status) && message_direction === "out") { + const dropdown = messages_div.querySelector(`.dropdown_${message_id}`) + if (!dropdown.querySelector(".retry")) { + const retry = document.createElement("div") + retry.setAttribute("class", "retry") + retry.innerHTML = "Retry" + retry.msg_id = message_id + retry.onclick = function() { + window.jsbridge.retryInteraction(`${this.msg_id}`); + } + dropdown.insertBefore(retry, message_div.querySelector(".delete")) + } + } + + // Update informations if needed + if (message_type === "data_transfer") + updateFileInteraction(message_div, message_object) + if (message_type === "text" && message_direction === "out") + // Modify sent status if necessary + updateTextInteraction(message_div, delivery_status) + if (message_type === "call") + updateCallInteraction(message_div, message_object) + if (message_type === "contact") + updateContactInteraction(message_div, message_object) + + // Clean timestamps + updateTimestamps(messages_div) +} + +function getNextInteraction(interaction, includeLazyLoadedBlock = true) +{ + var nextInteraction = interaction.nextSibling + if (!nextInteraction && includeLazyLoadedBlock) { + var nextBlock = interaction.parentNode.nextElementSibling + if (nextBlock) { + nextInteraction = nextBlock.firstElementChild + } + } + return nextInteraction +} + +function getPreviousInteraction(interaction, includeLazyLoadedBlock = true) +{ + var previousInteraction = interaction.previousSibling + if (!previousInteraction && includeLazyLoadedBlock) { + var previousBlock = interaction.parentNode.previousElementSibling + if (previousBlock) { + previousInteraction = previousBlock.lastElementChild + } + } + return previousInteraction +} + +function isSequenceBreak(firstMessage, secondMessage, insert_after = true) +{ + if (!firstMessage || !secondMessage) { + return false + } + var first_message_direction = firstMessage.classList.contains("message_out") ? "out" : "in" + var second_message_direction = secondMessage.classList.contains("message_out") ? "out" : "in" + if (second_message_direction != first_message_direction) { + return true + } + var firstMessageIsGenerated = firstMessage.classList.contains("generated_message") + var secondMessageIsGenerated = secondMessage.classList.contains("generated_message") + if (firstMessageIsGenerated != secondMessageIsGenerated) { + return true + } + if (insert_after) { + const internal_message_wrapper = firstMessage.querySelector(".internal_mes_wrapper") + if (internal_message_wrapper) { + const firstTimestamp = internal_message_wrapper.querySelector(".timestamp") + return !!(firstTimestamp) && firstTimestamp.innerHTML !== "just now" + } + return false + } else { + const internal_message_wrapper = firstMessage.querySelector(".internal_mes_wrapper") + if (internal_message_wrapper) { + return !!(internal_message_wrapper.querySelector(".timestamp")) + } + return false + } +} + +function updateSequencing(firstMessage, secondMessage) +{ + if (firstMessage) { + if (secondMessage) { + var sequence_break = isSequenceBreak(firstMessage, secondMessage, false) + if (sequence_break) { + if (firstMessage.classList.contains("middle_of_sequence")) { + firstMessage.classList.remove("middle_of_sequence") + firstMessage.classList.add("last_of_sequence") + } else if (firstMessage.classList.contains("first_of_sequence")) { + firstMessage.classList.remove("first_of_sequence") + firstMessage.classList.add("single_message") + } + if (secondMessage.classList.contains("middle_of_sequence")) { + secondMessage.classList.remove("middle_of_sequence") + secondMessage.classList.add("first_of_sequence") + } else if (secondMessage.classList.contains("last_of_sequence")) { + secondMessage.classList.remove("last_of_sequence") + secondMessage.classList.add("single_message") + } + } else { + if (firstMessage.classList.contains("last_of_sequence")) { + firstMessage.classList.remove("last_of_sequence") + firstMessage.classList.add("middle_of_sequence") + } else if (firstMessage.classList.contains("single_message")) { + firstMessage.classList.remove("single_message") + firstMessage.classList.add("first_of_sequence") + } + if (secondMessage.classList.contains("first_of_sequence")) { + secondMessage.classList.remove("first_of_sequence") + secondMessage.classList.add("middle_of_sequence") + } else if (secondMessage.classList.contains("single_message")) { + secondMessage.classList.remove("single_message") + secondMessage.classList.add("last_of_sequence") + } + } + } else { + // this is the last interaction of the conversation + if (firstMessage.classList.contains("first_of_sequence")) { + firstMessage.classList.remove("first_of_sequence") + firstMessage.classList.add("single_message") + } else if (firstMessage.classList.contains("middle_of_sequence")) { + firstMessage.classList.remove("middle_of_sequence") + firstMessage.classList.add("last_of_sequence") + } + } + } else if (secondMessage) { + // this is the first interaction of the conversation + if (secondMessage.classList.contains("middle_of_sequence")) { + secondMessage.classList.remove("middle_of_sequence") + secondMessage.classList.add("first_of_sequence") + } else if (secondMessage.classList.contains("last_of_sequence")) { + secondMessage.classList.remove("last_of_sequence") + secondMessage.classList.add("single_message") + } + } +} + +function computeSequencing(firstMessage, secondMessage, lazyLoadingBlock, insert_after = true) +{ + if (insert_after) { + if (secondMessage) { + var secondMessageIsGenerated = secondMessage.classList.contains("generated_message") + if (firstMessage && !secondMessageIsGenerated) { + var firstMessageIsGenerated = firstMessage.classList.contains("generated_message") + var sequence_break = isSequenceBreak(firstMessage, secondMessage) + if (sequence_break) { + secondMessage.classList.add("single_message") + } else { + if (firstMessage.classList.contains("single_message")) { + firstMessage.classList.remove("single_message") + firstMessage.classList.add("first_of_sequence") + } else if (firstMessage.classList.contains("last_of_sequence")) { + firstMessage.classList.remove("last_of_sequence") + firstMessage.classList.add("middle_of_sequence") + } + if (firstMessageIsGenerated) { + secondMessage.classList.add("single_message") + } else { + secondMessage.classList.add("last_of_sequence") + } + } + } else if (!secondMessageIsGenerated) { + secondMessage.classList.add("single_message") + } + } + } else if (firstMessage) { + var firstMessageIsGenerated = firstMessage.classList.contains("generated_message") + if (secondMessage && !firstMessageIsGenerated) { + var secondMessageIsGenerated = secondMessage.classList.contains("generated_message") + var sequence_break = isSequenceBreak(firstMessage, secondMessage, false) + if (sequence_break) { + firstMessage.classList.add("single_message") + } else { + if (secondMessage.classList.contains("single_message")) { + secondMessage.classList.remove("single_message") + secondMessage.classList.add("last_of_sequence") + } else if (secondMessage.classList.contains("first_of_sequence")) { + secondMessage.classList.remove("first_of_sequence") + secondMessage.classList.add("middle_of_sequence") + } + if (secondMessageIsGenerated) { + firstMessage.classList.add("single_message") + } else { + firstMessage.classList.add("first_of_sequence") + } + } + } else if (!firstMessageIsGenerated) { + firstMessage.classList.add("single_message") + } + } +} + +/** + * Wrapper for addOrUpdateMessage. + * + * Add or update a message and make sure the scrollbar position + * is refreshed correctly + * + * @param message_object message to be added + */ +/* exported addMessage */ +function addMessage(message_object) +{ + if (!messages.lastChild) { + var block_wrapper = document.createElement("div") + messages.append(block_wrapper) + } + + exec_keeping_scroll_position(addOrUpdateMessage, [message_object, true, undefined, messages.lastChild]) +} + +/** + * Update a message that was previously added with addMessage and + * make sure the scrollbar position is refreshed correctly + * + * @param message_object message to be updated + */ +/* exported updateMessage */ +function updateMessage(message_object) +{ + var message_div = messages.querySelector("#message_" + message_object["id"]) + exec_keeping_scroll_position(addOrUpdateMessage, [message_object, false, undefined, message_div.parentNode]) +} + +/** + * Called whenever an image has finished loading. Check lazy loading status + * once all images have finished loading. + */ +function on_image_load_finished() { + imagesLoadingCounter-- + + if (!imagesLoadingCounter) { + /* This code is executed once all images have been loaded. */ + check_lazy_loading() + } +} + +/** + * Make sure at least initialScrollBufferFactor screens of messages are + * available in the DOM. + */ +function check_lazy_loading() { + if (messages.scrollHeight < initialScrollBufferFactor * messages.clientHeight + && historyBufferIndex !== historyBuffer.length) { + /* Not enough messages loaded, print a new batch. Enable isInitialLoading + as reloading a single batch might not be sufficient to fulfill our + criteria (we want to be called back again to check on that) */ + isInitialLoading = true + printHistoryPart(messages, 0) + isInitialLoading = false + } +} + +/** + * Display 'scrollBuffer' messages from history in passed div (reverse order). + * + * @param messages_div that should be modified + * @param setMessages if enabled, #messages will be set to the resulting messages + * div after being modified. If #messages already exists it will + * be removed and replaced by the new div. + * @param fixedAt if setMessages is enabled, maintain scrollbar at the specified + * position (otherwise modifying #messages would result in + * changing the position of the scrollbar) + */ +function printHistoryPart(messages_div, fixedAt) +{ + if (historyBufferIndex === historyBuffer.length) { + return + } + + /* If first element is a spinner, remove it */ + if (messages_div.firstChild && messages_div.firstChild.id === "lazyloading-icon") { + messages_div.removeChild(messages_div.firstChild) + } + + /* Elements are appended to a wrapper div. This div has no style + properties, it allows us to add all messages at once to the main + messages div. */ + var block_wrapper = document.createElement("div") + + messages_div.prepend(block_wrapper) + + for (var i = 0; i < scrollBuffer && historyBufferIndex < historyBuffer.length; ++historyBufferIndex && ++i) { + // TODO on-screen messages should be removed from the buffer + addOrUpdateMessage(historyBuffer[historyBuffer.length - 1 - historyBufferIndex], true, false, block_wrapper) + } + + var lastMessage = block_wrapper.lastChild + + updateSequencing(lastMessage, getNextInteraction(lastMessage)) + + var absoluteLastMessage = messages_div.lastChild.lastChild + if (absoluteLastMessage) { + absoluteLastMessage.classList.add("last_message") + } + + /* Add ellipsis (...) at the top if there are still messages to load */ + if (historyBufferIndex < historyBuffer.length) { + var llicon = document.createElement("span") + llicon.id = "lazyloading-icon" + llicon.innerHTML = "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"#888888\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M0 0h24v24H0z\" fill=\"none\"/><path d=\"M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z\"/></svg>" + messages_div.prepend(llicon) + } + + if (fixedAt !== undefined) { + /* update scrollbar position to take text-message -related + scrollHeight changes in account (not necessary to wait + for DOM redisplay in this case). Changes due to image + messages are handled in their onLoad callbacks. */ + back_to_scroll(fixedAt) + /* schedule a scrollbar position update for changes which + are neither handled by the previous call nor by onLoad + callbacks. This call is necessary but not sufficient, + dropping the previous call would result in visual + glitches during initial load. */ + setTimeout(function() {back_to_scroll(fixedAt)}, 0) + } + + if (!imagesLoadingCounter) { + setTimeout(check_lazy_loading, 0) + } +} + +function hideBody() +{ + if (!document.body.classList.contains('fade')) { + document.body.classList.add('fade'); + } +} + +/** + * Set history buffer, initialize messages div and display a first batch + * of messages. + * + * Make sure that enough messages are displayed to fill initialScrollBufferFactor + * screens of messages (if enough messages are present in the conversation) + * + * @param messages_array should contain history to be printed + */ +/* exported printHistory */ +function printHistory(messages_array) +{ + hideBody() + + historyBuffer = messages_array + historyBufferIndex = 0 + + isInitialLoading = true + printHistoryPart(messages, 0) + isInitialLoading = false + + document.body.classList.remove('fade'); +} + +/** + * Set the image for a given sender + * set_sender_image object should contain the following keys: + * - sender: the name of the sender + * - sender_image: base64 png encoding of the sender image + * + * @param set_sender_image_object sender image object as previously described + */ +/* exported setSenderImage */ +function setSenderImage(set_sender_image_object) +{ + var sender_contact_method = set_sender_image_object["sender_contact_method"], + sender_image = set_sender_image_object["sender_image"], + sender_image_id = "sender_image_" + sender_contact_method, + currentSenderImage = document.getElementById(sender_image_id), // Remove the currently set sender image + style + + if (currentSenderImage) { + currentSenderImage.parentNode.removeChild(currentSenderImage) + } + + // Create a new style element + style = document.createElement("style") + + style.type = "text/css" + style.id = sender_image_id + style.innerHTML = "." + sender_image_id + " {content: url(data:image/png;base64," + sender_image + ");height: 32px;width: 32px;}" + document.head.appendChild(style) +} diff --git a/web/linkify-html.js b/web/linkify-html.js new file mode 100644 index 0000000000000000000000000000000000000000..1e5090c9e6856b3b4a2688345e33b244aee63e25 --- /dev/null +++ b/web/linkify-html.js @@ -0,0 +1,827 @@ +/* + * Copyright (c) 2016 SoapBox Innovations Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. +*/ + +'use strict'; + +;(function (window, linkify) { + var linkifyHtml = function (linkify) { + 'use strict'; + + var HTML5NamedCharRefs = {}; + + function EntityParser(named) { + this.named = named; + } + + var HEXCHARCODE = /^#[xX]([A-Fa-f0-9]+)$/; + var CHARCODE = /^#([0-9]+)$/; + var NAMED = /^([A-Za-z0-9]+)$/; + + EntityParser.prototype.parse = function (entity) { + if (!entity) { + return; + } + var matches = entity.match(HEXCHARCODE); + if (matches) { + return '&#x' + matches[1] + ';'; + } + matches = entity.match(CHARCODE); + if (matches) { + return '&#' + matches[1] + ';'; + } + matches = entity.match(NAMED); + if (matches) { + return '&' + matches[1] + ';'; + } + }; + + var WSP = /[\t\n\f ]/; + var ALPHA = /[A-Za-z]/; + var CRLF = /\r\n?/g; + + function isSpace(char) { + return WSP.test(char); + } + + function isAlpha(char) { + return ALPHA.test(char); + } + + function preprocessInput(input) { + return input.replace(CRLF, "\n"); + } + + function EventedTokenizer(delegate, entityParser) { + this.delegate = delegate; + this.entityParser = entityParser; + + this.state = null; + this.input = null; + + this.index = -1; + this.line = -1; + this.column = -1; + this.tagLine = -1; + this.tagColumn = -1; + + this.reset(); + } + + EventedTokenizer.prototype = { + reset: function reset() { + this.state = 'beforeData'; + this.input = ''; + + this.index = 0; + this.line = 1; + this.column = 0; + + this.tagLine = -1; + this.tagColumn = -1; + + this.delegate.reset(); + }, + + tokenize: function tokenize(input) { + this.reset(); + this.tokenizePart(input); + this.tokenizeEOF(); + }, + + tokenizePart: function tokenizePart(input) { + this.input += preprocessInput(input); + + while (this.index < this.input.length) { + this.states[this.state].call(this); + } + }, + + tokenizeEOF: function tokenizeEOF() { + this.flushData(); + }, + + flushData: function flushData() { + if (this.state === 'data') { + this.delegate.finishData(); + this.state = 'beforeData'; + } + }, + + peek: function peek() { + return this.input.charAt(this.index); + }, + + consume: function consume() { + var char = this.peek(); + + this.index++; + + if (char === "\n") { + this.line++; + this.column = 0; + } else { + this.column++; + } + + return char; + }, + + consumeCharRef: function consumeCharRef() { + var endIndex = this.input.indexOf(';', this.index); + if (endIndex === -1) { + return; + } + var entity = this.input.slice(this.index, endIndex); + var chars = this.entityParser.parse(entity); + if (chars) { + this.index = endIndex + 1; + return chars; + } + }, + + markTagStart: function markTagStart() { + this.tagLine = this.line; + this.tagColumn = this.column; + }, + + states: { + beforeData: function beforeData() { + var char = this.peek(); + + if (char === "<") { + this.state = 'tagOpen'; + this.markTagStart(); + this.consume(); + } else { + this.state = 'data'; + this.delegate.beginData(); + } + }, + + data: function data() { + var char = this.peek(); + + if (char === "<") { + this.delegate.finishData(); + this.state = 'tagOpen'; + this.markTagStart(); + this.consume(); + } else if (char === "&") { + this.consume(); + this.delegate.appendToData(this.consumeCharRef() || "&"); + } else { + this.consume(); + this.delegate.appendToData(char); + } + }, + + tagOpen: function tagOpen() { + var char = this.consume(); + + if (char === "!") { + this.state = 'markupDeclaration'; + } else if (char === "/") { + this.state = 'endTagOpen'; + } else if (isAlpha(char)) { + this.state = 'tagName'; + this.delegate.beginStartTag(); + this.delegate.appendToTagName(char.toLowerCase()); + } + }, + + markupDeclaration: function markupDeclaration() { + var char = this.consume(); + + if (char === "-" && this.input.charAt(this.index) === "-") { + this.index++; + this.state = 'commentStart'; + this.delegate.beginComment(); + } + }, + + commentStart: function commentStart() { + var char = this.consume(); + + if (char === "-") { + this.state = 'commentStartDash'; + } else if (char === ">") { + this.delegate.finishComment(); + this.state = 'beforeData'; + } else { + this.delegate.appendToCommentData(char); + this.state = 'comment'; + } + }, + + commentStartDash: function commentStartDash() { + var char = this.consume(); + + if (char === "-") { + this.state = 'commentEnd'; + } else if (char === ">") { + this.delegate.finishComment(); + this.state = 'beforeData'; + } else { + this.delegate.appendToCommentData("-"); + this.state = 'comment'; + } + }, + + comment: function comment() { + var char = this.consume(); + + if (char === "-") { + this.state = 'commentEndDash'; + } else { + this.delegate.appendToCommentData(char); + } + }, + + commentEndDash: function commentEndDash() { + var char = this.consume(); + + if (char === "-") { + this.state = 'commentEnd'; + } else { + this.delegate.appendToCommentData("-" + char); + this.state = 'comment'; + } + }, + + commentEnd: function commentEnd() { + var char = this.consume(); + + if (char === ">") { + this.delegate.finishComment(); + this.state = 'beforeData'; + } else { + this.delegate.appendToCommentData("--" + char); + this.state = 'comment'; + } + }, + + tagName: function tagName() { + var char = this.consume(); + + if (isSpace(char)) { + this.state = 'beforeAttributeName'; + } else if (char === "/") { + this.state = 'selfClosingStartTag'; + } else if (char === ">") { + this.delegate.finishTag(); + this.state = 'beforeData'; + } else { + this.delegate.appendToTagName(char); + } + }, + + beforeAttributeName: function beforeAttributeName() { + var char = this.consume(); + + if (isSpace(char)) { + return; + } else if (char === "/") { + this.state = 'selfClosingStartTag'; + } else if (char === ">") { + this.delegate.finishTag(); + this.state = 'beforeData'; + } else { + this.state = 'attributeName'; + this.delegate.beginAttribute(); + this.delegate.appendToAttributeName(char); + } + }, + + attributeName: function attributeName() { + var char = this.consume(); + + if (isSpace(char)) { + this.state = 'afterAttributeName'; + } else if (char === "/") { + this.delegate.beginAttributeValue(false); + this.delegate.finishAttributeValue(); + this.state = 'selfClosingStartTag'; + } else if (char === "=") { + this.state = 'beforeAttributeValue'; + } else if (char === ">") { + this.delegate.beginAttributeValue(false); + this.delegate.finishAttributeValue(); + this.delegate.finishTag(); + this.state = 'beforeData'; + } else { + this.delegate.appendToAttributeName(char); + } + }, + + afterAttributeName: function afterAttributeName() { + var char = this.consume(); + + if (isSpace(char)) { + return; + } else if (char === "/") { + this.delegate.beginAttributeValue(false); + this.delegate.finishAttributeValue(); + this.state = 'selfClosingStartTag'; + } else if (char === "=") { + this.state = 'beforeAttributeValue'; + } else if (char === ">") { + this.delegate.beginAttributeValue(false); + this.delegate.finishAttributeValue(); + this.delegate.finishTag(); + this.state = 'beforeData'; + } else { + this.delegate.beginAttributeValue(false); + this.delegate.finishAttributeValue(); + this.state = 'attributeName'; + this.delegate.beginAttribute(); + this.delegate.appendToAttributeName(char); + } + }, + + beforeAttributeValue: function beforeAttributeValue() { + var char = this.consume(); + + if (isSpace(char)) {} else if (char === '"') { + this.state = 'attributeValueDoubleQuoted'; + this.delegate.beginAttributeValue(true); + } else if (char === "'") { + this.state = 'attributeValueSingleQuoted'; + this.delegate.beginAttributeValue(true); + } else if (char === ">") { + this.delegate.beginAttributeValue(false); + this.delegate.finishAttributeValue(); + this.delegate.finishTag(); + this.state = 'beforeData'; + } else { + this.state = 'attributeValueUnquoted'; + this.delegate.beginAttributeValue(false); + this.delegate.appendToAttributeValue(char); + } + }, + + attributeValueDoubleQuoted: function attributeValueDoubleQuoted() { + var char = this.consume(); + + if (char === '"') { + this.delegate.finishAttributeValue(); + this.state = 'afterAttributeValueQuoted'; + } else if (char === "&") { + this.delegate.appendToAttributeValue(this.consumeCharRef('"') || "&"); + } else { + this.delegate.appendToAttributeValue(char); + } + }, + + attributeValueSingleQuoted: function attributeValueSingleQuoted() { + var char = this.consume(); + + if (char === "'") { + this.delegate.finishAttributeValue(); + this.state = 'afterAttributeValueQuoted'; + } else if (char === "&") { + this.delegate.appendToAttributeValue(this.consumeCharRef("'") || "&"); + } else { + this.delegate.appendToAttributeValue(char); + } + }, + + attributeValueUnquoted: function attributeValueUnquoted() { + var char = this.consume(); + + if (isSpace(char)) { + this.delegate.finishAttributeValue(); + this.state = 'beforeAttributeName'; + } else if (char === "&") { + this.delegate.appendToAttributeValue(this.consumeCharRef(">") || "&"); + } else if (char === ">") { + this.delegate.finishAttributeValue(); + this.delegate.finishTag(); + this.state = 'beforeData'; + } else { + this.delegate.appendToAttributeValue(char); + } + }, + + afterAttributeValueQuoted: function afterAttributeValueQuoted() { + var char = this.peek(); + + if (isSpace(char)) { + this.consume(); + this.state = 'beforeAttributeName'; + } else if (char === "/") { + this.consume(); + this.state = 'selfClosingStartTag'; + } else if (char === ">") { + this.consume(); + this.delegate.finishTag(); + this.state = 'beforeData'; + } else { + this.state = 'beforeAttributeName'; + } + }, + + selfClosingStartTag: function selfClosingStartTag() { + var char = this.peek(); + + if (char === ">") { + this.consume(); + this.delegate.markTagAsSelfClosing(); + this.delegate.finishTag(); + this.state = 'beforeData'; + } else { + this.state = 'beforeAttributeName'; + } + }, + + endTagOpen: function endTagOpen() { + var char = this.consume(); + + if (isAlpha(char)) { + this.state = 'tagName'; + this.delegate.beginEndTag(); + this.delegate.appendToTagName(char.toLowerCase()); + } + } + } + }; + + function Tokenizer(entityParser, options) { + this.token = null; + this.startLine = 1; + this.startColumn = 0; + this.options = options || {}; + this.tokenizer = new EventedTokenizer(this, entityParser); + } + + Tokenizer.prototype = { + tokenize: function tokenize(input) { + this.tokens = []; + this.tokenizer.tokenize(input); + return this.tokens; + }, + + tokenizePart: function tokenizePart(input) { + this.tokens = []; + this.tokenizer.tokenizePart(input); + return this.tokens; + }, + + tokenizeEOF: function tokenizeEOF() { + this.tokens = []; + this.tokenizer.tokenizeEOF(); + return this.tokens[0]; + }, + + reset: function reset() { + this.token = null; + this.startLine = 1; + this.startColumn = 0; + }, + + addLocInfo: function addLocInfo() { + if (this.options.loc) { + this.token.loc = { + start: { + line: this.startLine, + column: this.startColumn + }, + end: { + line: this.tokenizer.line, + column: this.tokenizer.column + } + }; + } + this.startLine = this.tokenizer.line; + this.startColumn = this.tokenizer.column; + }, + + // Data + + beginData: function beginData() { + this.token = { + type: 'Chars', + chars: '' + }; + this.tokens.push(this.token); + }, + + appendToData: function appendToData(char) { + this.token.chars += char; + }, + + finishData: function finishData() { + this.addLocInfo(); + }, + + // Comment + + beginComment: function beginComment() { + this.token = { + type: 'Comment', + chars: '' + }; + this.tokens.push(this.token); + }, + + appendToCommentData: function appendToCommentData(char) { + this.token.chars += char; + }, + + finishComment: function finishComment() { + this.addLocInfo(); + }, + + // Tags - basic + + beginStartTag: function beginStartTag() { + this.token = { + type: 'StartTag', + tagName: '', + attributes: [], + selfClosing: false + }; + this.tokens.push(this.token); + }, + + beginEndTag: function beginEndTag() { + this.token = { + type: 'EndTag', + tagName: '' + }; + this.tokens.push(this.token); + }, + + finishTag: function finishTag() { + this.addLocInfo(); + }, + + markTagAsSelfClosing: function markTagAsSelfClosing() { + this.token.selfClosing = true; + }, + + // Tags - name + + appendToTagName: function appendToTagName(char) { + this.token.tagName += char; + }, + + // Tags - attributes + + beginAttribute: function beginAttribute() { + this._currentAttribute = ["", "", null]; + this.token.attributes.push(this._currentAttribute); + }, + + appendToAttributeName: function appendToAttributeName(char) { + this._currentAttribute[0] += char; + }, + + beginAttributeValue: function beginAttributeValue(isQuoted) { + this._currentAttribute[2] = isQuoted; + }, + + appendToAttributeValue: function appendToAttributeValue(char) { + this._currentAttribute[1] = this._currentAttribute[1] || ""; + this._currentAttribute[1] += char; + }, + + finishAttributeValue: function finishAttributeValue() {} + }; + + function tokenize(input, options) { + var tokenizer = new Tokenizer(new EntityParser(HTML5NamedCharRefs), options); + return tokenizer.tokenize(input); + } + + var HTML5Tokenizer = { + HTML5NamedCharRefs: HTML5NamedCharRefs, + EntityParser: EntityParser, + EventedTokenizer: EventedTokenizer, + Tokenizer: Tokenizer, + tokenize: tokenize + }; + + var options = linkify.options; + var Options = options.Options; + + + var StartTag = 'StartTag'; + var EndTag = 'EndTag'; + var Chars = 'Chars'; + var Comment = 'Comment'; + + /** + `tokens` and `token` in this section refer to tokens generated by the HTML + parser. + */ + function linkifyHtml(str) { + var opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + var tokens = HTML5Tokenizer.tokenize(str); + var linkifiedTokens = []; + var linkified = []; + var i; + + opts = new Options(opts); + + // Linkify the tokens given by the parser + for (i = 0; i < tokens.length; i++) { + var token = tokens[i]; + + if (token.type === StartTag) { + linkifiedTokens.push(token); + + // Ignore all the contents of ignored tags + var tagName = token.tagName.toUpperCase(); + var isIgnored = tagName === 'A' || options.contains(opts.ignoreTags, tagName); + if (!isIgnored) { + continue; + } + + var preskipLen = linkifiedTokens.length; + skipTagTokens(tagName, tokens, ++i, linkifiedTokens); + i += linkifiedTokens.length - preskipLen - 1; + continue; + } else if (token.type !== Chars) { + // Skip this token, it's not important + linkifiedTokens.push(token); + continue; + } + + // Valid text token, linkify it! + var linkifedChars = linkifyChars(token.chars, opts); + linkifiedTokens.push.apply(linkifiedTokens, linkifedChars); + } + + // Convert the tokens back into a string + for (i = 0; i < linkifiedTokens.length; i++) { + var _token = linkifiedTokens[i]; + switch (_token.type) { + case StartTag: + var link = '<' + _token.tagName; + if (_token.attributes.length > 0) { + var attrs = attrsToStrings(_token.attributes); + link += ' ' + attrs.join(' '); + } + link += '>'; + linkified.push(link); + break; + case EndTag: + linkified.push('</' + _token.tagName + '>'); + break; + case Chars: + linkified.push(escapeText(_token.chars)); + break; + case Comment: + linkified.push('<!--' + escapeText(_token.chars) + '-->'); + break; + } + } + + return linkified.join(''); + } + + /** + `tokens` and `token` in this section referes to tokens returned by + `linkify.tokenize`. `linkified` will contain HTML Parser-style tokens + */ + function linkifyChars(str, opts) { + var tokens = linkify.tokenize(str); + var result = []; + + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + + if (token.type === 'nl' && opts.nl2br) { + result.push({ + type: StartTag, + tagName: 'br', + attributes: [], + selfClosing: true + }); + continue; + } else if (!token.isLink || !opts.check(token)) { + result.push({ type: Chars, chars: token.toString() }); + continue; + } + + var _opts$resolve = opts.resolve(token); + + var href = _opts$resolve.href; + var formatted = _opts$resolve.formatted; + var formattedHref = _opts$resolve.formattedHref; + var tagName = _opts$resolve.tagName; + var className = _opts$resolve.className; + var target = _opts$resolve.target; + var attributes = _opts$resolve.attributes; + + // Build up attributes + + var attributeArray = [['href', formattedHref]]; + + if (className) { + attributeArray.push(['class', className]); + } + + if (target) { + attributeArray.push(['target', target]); + } + + for (var attr in attributes) { + attributeArray.push([attr, attributes[attr]]); + } + + // Add the required tokens + result.push({ + type: StartTag, + tagName: tagName, + attributes: attributeArray, + selfClosing: false + }); + result.push({ type: Chars, chars: formatted }); + result.push({ type: EndTag, tagName: tagName }); + } + + return result; + } + + /** + Returns a list of tokens skipped until the closing tag of tagName. + + * `tagName` is the closing tag which will prompt us to stop skipping + * `tokens` is the array of tokens generated by HTML5Tokenizer which + * `i` is the index immediately after the opening tag to skip + * `skippedTokens` is an array which skipped tokens are being pushed into + + Caveats + + * Assumes that i is the first token after the given opening tagName + * The closing tag will be skipped, but nothing after it + * Will track whether there is a nested tag of the same type + */ + function skipTagTokens(tagName, tokens, i, skippedTokens) { + + // number of tokens of this type on the [fictional] stack + var stackCount = 1; + + while (i < tokens.length && stackCount > 0) { + var token = tokens[i]; + if (token.type === StartTag && token.tagName.toUpperCase() === tagName) { + // Nested tag of the same type, "add to stack" + stackCount++; + } else if (token.type === EndTag && token.tagName.toUpperCase() === tagName) { + // Closing tag + stackCount--; + } + skippedTokens.push(token); + i++; + } + + // Note that if stackCount > 0 here, the HTML is probably invalid + return skippedTokens; + } + + function escapeText(text) { + // Not required, HTML tokenizer ensures this occurs properly + return text; + } + + function escapeAttr(attr) { + return attr.replace(/"/g, '"'); + } + + function attrsToStrings(attrs) { + var attrStrs = []; + for (var i = 0; i < attrs.length; i++) { + var _attrs$i = attrs[i]; + var name = _attrs$i[0]; + var value = _attrs$i[1]; + + attrStrs.push(name + '="' + escapeAttr(value) + '"'); + } + return attrStrs; + } + + return linkifyHtml; + }(linkify); + window.linkifyHtml = linkifyHtml; +})(window, linkify); diff --git a/web/linkify-string.js b/web/linkify-string.js new file mode 100644 index 0000000000000000000000000000000000000000..699a94d1f47ac7fbc6d1c3dfbd51cc553d12feed --- /dev/null +++ b/web/linkify-string.js @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2016 SoapBox Innovations Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. +*/ + +'use strict'; + +;(function (window, linkify) { + var linkifyString = function (linkify) { + 'use strict'; + + /** + Convert strings of text into linkable HTML text + */ + + var tokenize = linkify.tokenize; + var options = linkify.options; + var Options = options.Options; + + + function escapeText(text) { + return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); + } + + function escapeAttr(href) { + return href.replace(/"/g, '"'); + } + + function attributesToString(attributes) { + if (!attributes) { + return ''; + } + var result = []; + + for (var attr in attributes) { + var val = attributes[attr] + ''; + result.push(attr + '="' + escapeAttr(val) + '"'); + } + return result.join(' '); + } + + function linkifyStr(str) { + var opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + opts = new Options(opts); + + var tokens = tokenize(str); + var result = []; + + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + + if (token.type === 'nl' && opts.nl2br) { + result.push('<br>\n'); + continue; + } else if (!token.isLink || !opts.check(token)) { + result.push(escapeText(token.toString())); + continue; + } + + var _opts$resolve = opts.resolve(token); + + var formatted = _opts$resolve.formatted; + var formattedHref = _opts$resolve.formattedHref; + var tagName = _opts$resolve.tagName; + var className = _opts$resolve.className; + var target = _opts$resolve.target; + var attributes = _opts$resolve.attributes; + + + var link = '<' + tagName + ' href="' + escapeAttr(formattedHref) + '"'; + + if (className) { + link += ' class="' + escapeAttr(className) + '"'; + } + + if (target) { + link += ' target="' + escapeAttr(target) + '"'; + } + + if (attributes) { + link += ' ' + attributesToString(attributes); + } + + link += '>' + escapeText(formatted) + '</' + tagName + '>'; + result.push(link); + } + + return result.join(''); + } + + if (!String.prototype.linkify) { + String.prototype.linkify = function (opts) { + return linkifyStr(this, opts); + }; + } + + return linkifyStr; + }(linkify); + window.linkifyStr = linkifyString; +})(window, linkify); diff --git a/web/linkify.js b/web/linkify.js new file mode 100644 index 0000000000000000000000000000000000000000..15dc03b1a2e936cfcbafa02788722dc382106da0 --- /dev/null +++ b/web/linkify.js @@ -0,0 +1,1271 @@ +/* + * Copyright (c) 2016 SoapBox Innovations Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. +*/ + +;(function () { +'use strict'; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; + +(function (exports) { + 'use strict'; + + function inherits(parent, child) { + var props = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; + + var extended = Object.create(parent.prototype); + for (var p in props) { + extended[p] = props[p]; + } + extended.constructor = child; + child.prototype = extended; + return child; + } + + var defaults = { + defaultProtocol: 'http', + events: null, + format: noop, + formatHref: noop, + nl2br: false, + tagName: 'a', + target: typeToTarget, + validate: true, + ignoreTags: [], + attributes: null, + className: 'linkified' }; + + function Options(opts) { + opts = opts || {}; + + this.defaultProtocol = opts.defaultProtocol || defaults.defaultProtocol; + this.events = opts.events || defaults.events; + this.format = opts.format || defaults.format; + this.formatHref = opts.formatHref || defaults.formatHref; + this.nl2br = opts.nl2br || defaults.nl2br; + this.tagName = opts.tagName || defaults.tagName; + this.target = opts.target || defaults.target; + this.validate = opts.validate || defaults.validate; + this.ignoreTags = []; + + // linkAttributes and linkClass is deprecated + this.attributes = opts.attributes || opts.linkAttributes || defaults.attributes; + this.className = opts.className || opts.linkClass || defaults.className; + + // Make all tags names upper case + + var ignoredTags = opts.ignoreTags || defaults.ignoreTags; + for (var i = 0; i < ignoredTags.length; i++) { + this.ignoreTags.push(ignoredTags[i].toUpperCase()); + } + } + + Options.prototype = { + /** + * Given the token, return all options for how it should be displayed + */ + resolve: function resolve(token) { + var href = token.toHref(this.defaultProtocol); + return { + formatted: this.get('format', token.toString(), token), + formattedHref: this.get('formatHref', href, token), + tagName: this.get('tagName', href, token), + className: this.get('className', href, token), + target: this.get('target', href, token), + events: this.getObject('events', href, token), + attributes: this.getObject('attributes', href, token) + }; + }, + + + /** + * Returns true or false based on whether a token should be displayed as a + * link based on the user options. By default, + */ + check: function check(token) { + return this.get('validate', token.toString(), token); + }, + + + // Private methods + + /** + * Resolve an option's value based on the value of the option and the given + * params. + * @param [String] key Name of option to use + * @param operator will be passed to the target option if it's method + * @param [MultiToken] token The token from linkify.tokenize + */ + get: function get(key, operator, token) { + var option = this[key]; + + if (!option) { + return option; + } + + switch (typeof option === 'undefined' ? 'undefined' : _typeof(option)) { + case 'function': + return option(operator, token.type); + case 'object': + var optionValue = option[token.type] || defaults[key]; + return typeof optionValue === 'function' ? optionValue(operator, token.type) : optionValue; + } + + return option; + }, + getObject: function getObject(key, operator, token) { + var option = this[key]; + return typeof option === 'function' ? option(operator, token.type) : option; + } + }; + + /** + * Quick indexOf replacement for checking the ignoreTags option + */ + function contains(arr, value) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] === value) { + return true; + } + } + return false; + } + + function noop(val) { + return val; + } + + function typeToTarget(href, type) { + return type === 'url' ? '_blank' : null; + } + + var options = Object.freeze({ + defaults: defaults, + Options: Options, + contains: contains + }); + + function createStateClass() { + return function (tClass) { + this.j = []; + this.T = tClass || null; + }; + } + + /** + A simple state machine that can emit token classes + + The `j` property in this class refers to state jumps. It's a + multidimensional array where for each element: + + * index [0] is a symbol or class of symbols to transition to. + * index [1] is a State instance which matches + + The type of symbol will depend on the target implementation for this class. + In Linkify, we have a two-stage scanner. Each stage uses this state machine + but with a slighly different (polymorphic) implementation. + + The `T` property refers to the token class. + + TODO: Can the `on` and `next` methods be combined? + + @class BaseState + */ + var BaseState = createStateClass(); + BaseState.prototype = { + defaultTransition: false, + + /** + @method constructor + @param {Class} tClass Pass in the kind of token to emit if there are + no jumps after this state and the state is accepting. + */ + + /** + On the given symbol(s), this machine should go to the given state + @method on + @param {Array|Mixed} symbol + @param {BaseState} state Note that the type of this state should be the + same as the current instance (i.e., don't pass in a different + subclass) + */ + on: function on(symbol, state) { + if (symbol instanceof Array) { + for (var i = 0; i < symbol.length; i++) { + this.j.push([symbol[i], state]); + } + return this; + } + this.j.push([symbol, state]); + return this; + }, + + + /** + Given the next item, returns next state for that item + @method next + @param {Mixed} item Should be an instance of the symbols handled by + this particular machine. + @return {State} state Returns false if no jumps are available + */ + next: function next(item) { + for (var i = 0; i < this.j.length; i++) { + var jump = this.j[i]; + var symbol = jump[0]; // Next item to check for + var state = jump[1]; // State to jump to if items match + + // compare item with symbol + if (this.test(item, symbol)) { + return state; + } + } + + // Nowhere left to jump! + return this.defaultTransition; + }, + + + /** + Does this state accept? + `true` only of `this.T` exists + @method accepts + @return {Boolean} + */ + accepts: function accepts() { + return !!this.T; + }, + + + /** + Determine whether a given item "symbolizes" the symbol, where symbol is + a class of items handled by this state machine. + This method should be overriden in extended classes. + @method test + @param {Mixed} item Does this item match the given symbol? + @param {Mixed} symbol + @return {Boolean} + */ + test: function test(item, symbol) { + return item === symbol; + }, + + + /** + Emit the token for this State (just return it in this case) + If this emits a token, this instance is an accepting state + @method emit + @return {Class} T + */ + emit: function emit() { + return this.T; + } + }; + + /** + State machine for string-based input + + @class CharacterState + @extends BaseState + */ + var CharacterState = inherits(BaseState, createStateClass(), { + /** + Does the given character match the given character or regular + expression? + @method test + @param {String} char + @param {String|RegExp} charOrRegExp + @return {Boolean} + */ + test: function test(character, charOrRegExp) { + return character === charOrRegExp || charOrRegExp instanceof RegExp && charOrRegExp.test(character); + } + }); + + /** + State machine for input in the form of TextTokens + + @class TokenState + @extends BaseState + */ + var State = inherits(BaseState, createStateClass(), { + + /** + * Similar to `on`, but returns the state the results in the transition from + * the given item + * @method jump + * @param {Mixed} item + * @param {Token} [token] + * @return state + */ + jump: function jump(token) { + var tClass = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1]; + + var state = this.next(new token('')); // dummy temp token + if (state === this.defaultTransition) { + // Make a new state! + state = new this.constructor(tClass); + this.on(token, state); + } else if (tClass) { + state.T = tClass; + } + return state; + }, + + + /** + Is the given token an instance of the given token class? + @method test + @param {TextToken} token + @param {Class} tokenClass + @return {Boolean} + */ + test: function test(token, tokenClass) { + return token instanceof tokenClass; + } + }); + + /** + Given a non-empty target string, generates states (if required) for each + consecutive substring of characters in str starting from the beginning of + the string. The final state will have a special value, as specified in + options. All other "in between" substrings will have a default end state. + + This turns the state machine into a Trie-like data structure (rather than a + intelligently-designed DFA). + + Note that I haven't really tried these with any strings other than + DOMAIN. + + @param {String} str + @param {CharacterState} start State to jump from the first character + @param {Class} endToken Token class to emit when the given string has been + matched and no more jumps exist. + @param {Class} defaultToken "Filler token", or which token type to emit when + we don't have a full match + @return {Array} list of newly-created states + */ + function stateify(str, start, endToken, defaultToken) { + var i = 0, + len = str.length, + state = start, + newStates = [], + nextState = void 0; + + // Find the next state without a jump to the next character + while (i < len && (nextState = state.next(str[i]))) { + state = nextState; + i++; + } + + if (i >= len) { + return []; + } // no new tokens were added + + while (i < len - 1) { + nextState = new CharacterState(defaultToken); + newStates.push(nextState); + state.on(str[i], nextState); + state = nextState; + i++; + } + + nextState = new CharacterState(endToken); + newStates.push(nextState); + state.on(str[len - 1], nextState); + + return newStates; + } + + function createTokenClass() { + return function (value) { + if (value) { + this.v = value; + } + }; + } + + /****************************************************************************** + Text Tokens + Tokens composed of strings + ******************************************************************************/ + + /** + Abstract class used for manufacturing text tokens. + Pass in the value this token represents + + @class TextToken + @abstract + */ + var TextToken = createTokenClass(); + TextToken.prototype = { + toString: function toString() { + return this.v + ''; + } + }; + + function inheritsToken(value) { + var props = value ? { v: value } : {}; + return inherits(TextToken, createTokenClass(), props); + } + + /** + A valid domain token + @class DOMAIN + @extends TextToken + */ + var DOMAIN = inheritsToken(); + + /** + @class AT + @extends TextToken + */ + var AT = inheritsToken('@'); + + /** + Represents a single colon `:` character + + @class COLON + @extends TextToken + */ + var COLON = inheritsToken(':'); + + /** + @class DOT + @extends TextToken + */ + var DOT = inheritsToken('.'); + + /** + A character class that can surround the URL, but which the URL cannot begin + or end with. Does not include certain English punctuation like parentheses. + + @class PUNCTUATION + @extends TextToken + */ + var PUNCTUATION = inheritsToken(); + + /** + The word localhost (by itself) + @class LOCALHOST + @extends TextToken + */ + var LOCALHOST = inheritsToken(); + + /** + Newline token + @class NL + @extends TextToken + */ + var TNL = inheritsToken('\n'); + + /** + @class NUM + @extends TextToken + */ + var NUM = inheritsToken(); + + /** + @class PLUS + @extends TextToken + */ + var PLUS = inheritsToken('+'); + + /** + @class POUND + @extends TextToken + */ + var POUND = inheritsToken('#'); + + /** + Represents a web URL protocol. Supported types include + + * `http:` + * `https:` + * `ftp:` + * `ftps:` + * There's Another super weird one + + @class PROTOCOL + @extends TextToken + */ + var PROTOCOL = inheritsToken(); + + /** + @class QUERY + @extends TextToken + */ + var QUERY = inheritsToken('?'); + + /** + @class SLASH + @extends TextToken + */ + var SLASH = inheritsToken('/'); + + /** + @class UNDERSCORE + @extends TextToken + */ + var UNDERSCORE = inheritsToken('_'); + + /** + One ore more non-whitespace symbol. + @class SYM + @extends TextToken + */ + var SYM = inheritsToken(); + + /** + @class TLD + @extends TextToken + */ + var TLD = inheritsToken(); + + /** + Represents a string of consecutive whitespace characters + + @class WS + @extends TextToken + */ + var WS = inheritsToken(); + + /** + Opening/closing bracket classes + */ + + var OPENBRACE = inheritsToken('{'); + var OPENBRACKET = inheritsToken('['); + var OPENANGLEBRACKET = inheritsToken('<'); + var OPENPAREN = inheritsToken('('); + var CLOSEBRACE = inheritsToken('}'); + var CLOSEBRACKET = inheritsToken(']'); + var CLOSEANGLEBRACKET = inheritsToken('>'); + var CLOSEPAREN = inheritsToken(')'); + + var TOKENS = Object.freeze({ + Base: TextToken, + DOMAIN: DOMAIN, + AT: AT, + COLON: COLON, + DOT: DOT, + PUNCTUATION: PUNCTUATION, + LOCALHOST: LOCALHOST, + NL: TNL, + NUM: NUM, + PLUS: PLUS, + POUND: POUND, + QUERY: QUERY, + PROTOCOL: PROTOCOL, + SLASH: SLASH, + UNDERSCORE: UNDERSCORE, + SYM: SYM, + TLD: TLD, + WS: WS, + OPENBRACE: OPENBRACE, + OPENBRACKET: OPENBRACKET, + OPENANGLEBRACKET: OPENANGLEBRACKET, + OPENPAREN: OPENPAREN, + CLOSEBRACE: CLOSEBRACE, + CLOSEBRACKET: CLOSEBRACKET, + CLOSEANGLEBRACKET: CLOSEANGLEBRACKET, + CLOSEPAREN: CLOSEPAREN + }); + + /** + The scanner provides an interface that takes a string of text as input, and + outputs an array of tokens instances that can be used for easy URL parsing. + + @module linkify + @submodule scanner + @main scanner + */ + + var tlds = 'aaa|aarp|abb|abbott|abogado|ac|academy|accenture|accountant|accountants|aco|active|actor|ad|adac|ads|adult|ae|aeg|aero|af|afl|ag|agency|ai|aig|airforce|airtel|al|alibaba|alipay|allfinanz|alsace|am|amica|amsterdam|an|analytics|android|ao|apartments|app|apple|aq|aquarelle|ar|aramco|archi|army|arpa|arte|as|asia|associates|at|attorney|au|auction|audi|audio|author|auto|autos|avianca|aw|ax|axa|az|azure|ba|baidu|band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bb|bbc|bbva|bcg|bcn|bd|be|beats|beer|bentley|berlin|best|bet|bf|bg|bh|bharti|bi|bible|bid|bike|bing|bingo|bio|biz|bj|black|blackfriday|bloomberg|blue|bm|bms|bmw|bn|bnl|bnpparibas|bo|boats|boehringer|bom|bond|boo|book|boots|bosch|bostik|bot|boutique|br|bradesco|bridgestone|broadway|broker|brother|brussels|bs|bt|budapest|bugatti|build|builders|business|buy|buzz|bv|bw|by|bz|bzh|ca|cab|cafe|cal|call|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|cc|cd|ceb|center|ceo|cern|cf|cfa|cfd|cg|ch|chanel|channel|chase|chat|cheap|chloe|christmas|chrome|church|ci|cipriani|circle|cisco|citic|city|cityeats|ck|cl|claims|cleaning|click|clinic|clinique|clothing|cloud|club|clubmed|cm|cn|co|coach|codes|coffee|college|cologne|com|commbank|community|company|compare|computer|comsec|condos|construction|consulting|contact|contractors|cooking|cool|coop|corsica|country|coupon|coupons|courses|cr|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cu|cuisinella|cv|cw|cx|cy|cymru|cyou|cz|dabur|dad|dance|date|dating|datsun|day|dclk|de|dealer|deals|degree|delivery|dell|deloitte|delta|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount|dj|dk|dm|dnp|do|docs|dog|doha|domains|download|drive|dubai|durban|dvag|dz|earth|eat|ec|edeka|edu|education|ee|eg|email|emerck|energy|engineer|engineering|enterprises|epson|equipment|er|erni|es|esq|estate|et|eu|eurovision|eus|events|everbank|exchange|expert|exposed|express|fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|fast|feedback|ferrero|fi|film|final|finance|financial|firestone|firmdale|fish|fishing|fit|fitness|fj|fk|flickr|flights|florist|flowers|flsmidth|fly|fm|fo|foo|football|ford|forex|forsale|forum|foundation|fox|fr|fresenius|frl|frogans|frontier|fund|furniture|futbol|fyi|ga|gal|gallery|gallup|game|garden|gb|gbiz|gd|gdn|ge|gea|gent|genting|gf|gg|ggee|gh|gi|gift|gifts|gives|giving|gl|glass|gle|global|globo|gm|gmail|gmbh|gmo|gmx|gn|gold|goldpoint|golf|goo|goog|google|gop|got|gov|gp|gq|gr|grainger|graphics|gratis|green|gripe|group|gs|gt|gu|gucci|guge|guide|guitars|guru|gw|gy|hamburg|hangout|haus|hdfcbank|health|healthcare|help|helsinki|here|hermes|hiphop|hitachi|hiv|hk|hm|hn|hockey|holdings|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hr|hsbc|ht|hu|hyundai|ibm|icbc|ice|icu|id|ie|ifm|iinet|il|im|immo|immobilien|in|industries|infiniti|info|ing|ink|institute|insurance|insure|int|international|investments|io|ipiranga|iq|ir|irish|is|iselect|ist|istanbul|it|itau|iwc|jaguar|java|jcb|je|jetzt|jewelry|jlc|jll|jm|jmp|jo|jobs|joburg|jot|joy|jp|jpmorgan|jprs|juegos|kaufen|kddi|ke|kerryhotels|kerrylogistics|kerryproperties|kfh|kg|kh|ki|kia|kim|kinder|kitchen|kiwi|km|kn|koeln|komatsu|kp|kpn|kr|krd|kred|kuokgroup|kw|ky|kyoto|kz|la|lacaixa|lamborghini|lamer|lancaster|land|landrover|lanxess|lasalle|lat|latrobe|law|lawyer|lb|lc|lds|lease|leclerc|legal|lexus|lgbt|li|liaison|lidl|life|lifeinsurance|lifestyle|lighting|like|limited|limo|lincoln|linde|link|live|living|lixil|lk|loan|loans|local|locus|lol|london|lotte|lotto|love|lr|ls|lt|ltd|ltda|lu|lupin|luxe|luxury|lv|ly|ma|madrid|maif|maison|makeup|man|management|mango|market|marketing|markets|marriott|mba|mc|md|me|med|media|meet|melbourne|meme|memorial|men|menu|meo|mg|mh|miami|microsoft|mil|mini|mk|ml|mm|mma|mn|mo|mobi|mobily|moda|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar|mp|mq|mr|ms|mt|mtn|mtpc|mtr|mu|museum|mutuelle|mv|mw|mx|my|mz|na|nadex|nagoya|name|natura|navy|nc|ne|nec|net|netbank|network|neustar|new|news|nexus|nf|ng|ngo|nhk|ni|nico|nikon|ninja|nissan|nl|no|nokia|norton|nowruz|np|nr|nra|nrw|ntt|nu|nyc|nz|obi|office|okinawa|om|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|origins|osaka|otsuka|ovh|pa|page|pamperedchef|panerai|paris|pars|partners|parts|party|passagens|pe|pet|pf|pg|ph|pharmacy|philips|photo|photography|photos|physio|piaget|pics|pictet|pictures|pid|pin|ping|pink|pizza|pk|pl|place|play|playstation|plumbing|plus|pm|pn|pohl|poker|porn|post|pr|praxi|press|pro|prod|productions|prof|promo|properties|property|protection|ps|pt|pub|pw|pwc|py|qa|qpon|quebec|quest|racing|re|read|realtor|realty|recipes|red|redstone|redumbrella|rehab|reise|reisen|reit|ren|rent|rentals|repair|report|republican|rest|restaurant|review|reviews|rexroth|rich|ricoh|rio|rip|ro|rocher|rocks|rodeo|room|rs|rsvp|ru|ruhr|run|rw|rwe|ryukyu|sa|saarland|safe|safety|sakura|sale|salon|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|sas|saxo|sb|sbs|sc|sca|scb|schaeffler|schmidt|scholarships|school|schule|schwarz|science|scor|scot|sd|se|seat|security|seek|select|sener|services|seven|sew|sex|sexy|sfr|sg|sh|sharp|shell|shia|shiksha|shoes|show|shriram|si|singles|site|sj|sk|ski|skin|sky|skype|sl|sm|smile|sn|sncf|so|soccer|social|softbank|software|sohu|solar|solutions|song|sony|soy|space|spiegel|spot|spreadbetting|sr|srl|st|stada|star|starhub|statefarm|statoil|stc|stcgroup|stockholm|storage|store|studio|study|style|su|sucks|supplies|supply|support|surf|surgery|suzuki|sv|swatch|swiss|sx|sy|sydney|symantec|systems|sz|tab|taipei|taobao|tatamotors|tatar|tattoo|tax|taxi|tc|tci|td|team|tech|technology|tel|telecity|telefonica|temasek|tennis|tf|tg|th|thd|theater|theatre|tickets|tienda|tiffany|tips|tires|tirol|tj|tk|tl|tm|tmall|tn|to|today|tokyo|tools|top|toray|toshiba|total|tours|town|toyota|toys|tp|tr|trade|trading|training|travel|travelers|travelersinsurance|trust|trv|tt|tube|tui|tunes|tushu|tv|tvs|tw|tz|ua|ubs|ug|uk|unicom|university|uno|uol|us|uy|uz|va|vacations|vana|vc|ve|vegas|ventures|verisign|versicherung|vet|vg|vi|viajes|video|viking|villas|vin|vip|virgin|vision|vista|vistaprint|viva|vlaanderen|vn|vodka|volkswagen|vote|voting|voto|voyage|vu|vuelos|wales|walter|wang|wanggou|watch|watches|weather|weatherchannel|webcam|weber|website|wed|wedding|weir|wf|whoswho|wien|wiki|williamhill|win|windows|wine|wme|wolterskluwer|work|works|world|ws|wtc|wtf|xbox|xerox|xin|xperia|xxx|xyz|yachts|yahoo|yamaxun|yandex|ye|yodobashi|yoga|yokohama|youtube|yt|za|zara|zero|zip|zm|zone|zuerich|zw'.split('|'); // macro, see gulpfile.js + + var NUMBERS = '0123456789'.split(''); + var ALPHANUM = '0123456789abcdefghijklmnopqrstuvwxyz'.split(''); + var WHITESPACE = [' ', '\f', '\r', '\t', '\v', ' ', ' ', '']; // excluding line breaks + + var domainStates = []; // states that jump to DOMAIN on /[a-z0-9]/ + var makeState = function makeState(tokenClass) { + return new CharacterState(tokenClass); + }; + + // Frequently used states + var S_START = makeState(); + var S_NUM = makeState(NUM); + var S_DOMAIN = makeState(DOMAIN); + var S_DOMAIN_HYPHEN = makeState(); // domain followed by 1 or more hyphen characters + var S_WS = makeState(WS); + + // States for special URL symbols + S_START.on('@', makeState(AT)).on('.', makeState(DOT)).on('+', makeState(PLUS)).on('#', makeState(POUND)).on('?', makeState(QUERY)).on('/', makeState(SLASH)).on('_', makeState(UNDERSCORE)).on(':', makeState(COLON)).on('{', makeState(OPENBRACE)).on('[', makeState(OPENBRACKET)).on('<', makeState(OPENANGLEBRACKET)).on('(', makeState(OPENPAREN)).on('}', makeState(CLOSEBRACE)).on(']', makeState(CLOSEBRACKET)).on('>', makeState(CLOSEANGLEBRACKET)).on(')', makeState(CLOSEPAREN)).on([',', ';', '!', '"', '\''], makeState(PUNCTUATION)); + + // Whitespace jumps + // Tokens of only non-newline whitespace are arbitrarily long + S_START.on('\n', makeState(TNL)).on(WHITESPACE, S_WS); + + // If any whitespace except newline, more whitespace! + S_WS.on(WHITESPACE, S_WS); + + // Generates states for top-level domains + // Note that this is most accurate when tlds are in alphabetical order + for (var i = 0; i < tlds.length; i++) { + var newStates = stateify(tlds[i], S_START, TLD, DOMAIN); + domainStates.push.apply(domainStates, newStates); + } + + // Collect the states generated by different protocls + var partialProtocolFileStates = stateify('file', S_START, DOMAIN, DOMAIN); + var partialProtocolFtpStates = stateify('ftp', S_START, DOMAIN, DOMAIN); + var partialProtocolHttpStates = stateify('http', S_START, DOMAIN, DOMAIN); + + // Add the states to the array of DOMAINeric states + domainStates.push.apply(domainStates, partialProtocolFileStates); + domainStates.push.apply(domainStates, partialProtocolFtpStates); + domainStates.push.apply(domainStates, partialProtocolHttpStates); + + // Protocol states + var S_PROTOCOL_FILE = partialProtocolFileStates.pop(); + var S_PROTOCOL_FTP = partialProtocolFtpStates.pop(); + var S_PROTOCOL_HTTP = partialProtocolHttpStates.pop(); + var S_PROTOCOL_SECURE = makeState(DOMAIN); + var S_FULL_PROTOCOL = makeState(PROTOCOL); // Full protocol ends with COLON + + // Secure protocols (end with 's') + S_PROTOCOL_FTP.on('s', S_PROTOCOL_SECURE).on(':', S_FULL_PROTOCOL); + + S_PROTOCOL_HTTP.on('s', S_PROTOCOL_SECURE).on(':', S_FULL_PROTOCOL); + + domainStates.push(S_PROTOCOL_SECURE); + + // Become protocol tokens after a COLON + S_PROTOCOL_FILE.on(':', S_FULL_PROTOCOL); + S_PROTOCOL_SECURE.on(':', S_FULL_PROTOCOL); + + // Localhost + var partialLocalhostStates = stateify('localhost', S_START, LOCALHOST, DOMAIN); + domainStates.push.apply(domainStates, partialLocalhostStates); + + // Everything else + // DOMAINs make more DOMAINs + // Number and character transitions + S_START.on(NUMBERS, S_NUM); + S_NUM.on('-', S_DOMAIN_HYPHEN).on(NUMBERS, S_NUM).on(ALPHANUM, S_DOMAIN); // number becomes DOMAIN + + S_DOMAIN.on('-', S_DOMAIN_HYPHEN).on(ALPHANUM, S_DOMAIN); + + // All the generated states should have a jump to DOMAIN + for (var _i = 0; _i < domainStates.length; _i++) { + domainStates[_i].on('-', S_DOMAIN_HYPHEN).on(ALPHANUM, S_DOMAIN); + } + + S_DOMAIN_HYPHEN.on('-', S_DOMAIN_HYPHEN).on(NUMBERS, S_DOMAIN).on(ALPHANUM, S_DOMAIN); + + // Set default transition + S_START.defaultTransition = makeState(SYM); + + /** + Given a string, returns an array of TOKEN instances representing the + composition of that string. + + @method run + @param {String} str Input string to scan + @return {Array} Array of TOKEN instances + */ + var run = function run(str) { + + // The state machine only looks at lowercase strings. + // This selective `toLowerCase` is used because lowercasing the entire + // string causes the length and character position to vary in some in some + // non-English strings. This happens only on V8-based runtimes. + var lowerStr = str.replace(/[A-Z]/g, function (c) { + return c.toLowerCase(); + }); + var len = str.length; + var tokens = []; // return value + + var cursor = 0; + + // Tokenize the string + while (cursor < len) { + var state = S_START; + var secondState = null; + var nextState = null; + var tokenLength = 0; + var latestAccepting = null; + var sinceAccepts = -1; + + while (cursor < len && (nextState = state.next(lowerStr[cursor]))) { + secondState = null; + state = nextState; + + // Keep track of the latest accepting state + if (state.accepts()) { + sinceAccepts = 0; + latestAccepting = state; + } else if (sinceAccepts >= 0) { + sinceAccepts++; + } + + tokenLength++; + cursor++; + } + + if (sinceAccepts < 0) { + continue; + } // Should never happen + + // Roll back to the latest accepting state + cursor -= sinceAccepts; + tokenLength -= sinceAccepts; + + // Get the class for the new token + var TOKEN = latestAccepting.emit(); // Current token class + + // No more jumps, just make a new token + tokens.push(new TOKEN(str.substr(cursor - tokenLength, tokenLength))); + } + + return tokens; + }; + + var start = S_START; + + var scanner = Object.freeze({ + State: CharacterState, + TOKENS: TOKENS, + run: run, + start: start + }); + + /****************************************************************************** + Multi-Tokens + Tokens composed of arrays of TextTokens + ******************************************************************************/ + + // Is the given token a valid domain token? + // Should nums be included here? + function isDomainToken(token) { + return token instanceof DOMAIN || token instanceof TLD; + } + + /** + Abstract class used for manufacturing tokens of text tokens. That is rather + than the value for a token being a small string of text, it's value an array + of text tokens. + + Used for grouping together URLs, emails, hashtags, and other potential + creations. + + @class MultiToken + @abstract + */ + var MultiToken = createTokenClass(); + + MultiToken.prototype = { + /** + String representing the type for this token + @property type + @default 'TOKEN' + */ + type: 'token', + + /** + Is this multitoken a link? + @property isLink + @default false + */ + isLink: false, + + /** + Return the string this token represents. + @method toString + @return {String} + */ + toString: function toString() { + var result = []; + for (var _i2 = 0; _i2 < this.v.length; _i2++) { + result.push(this.v[_i2].toString()); + } + return result.join(''); + }, + + + /** + What should the value for this token be in the `href` HTML attribute? + Returns the `.toString` value by default. + @method toHref + @return {String} + */ + toHref: function toHref() { + return this.toString(); + }, + + + /** + Returns a hash of relevant values for this token, which includes keys + * type - Kind of token ('url', 'email', etc.) + * value - Original text + * href - The value that should be added to the anchor tag's href + attribute + @method toObject + @param {String} [protocol] `'http'` by default + @return {Object} + */ + toObject: function toObject() { + var protocol = arguments.length <= 0 || arguments[0] === undefined ? 'http' : arguments[0]; + + return { + type: this.type, + value: this.toString(), + href: this.toHref(protocol) + }; + } + }; + + /** + Represents a list of tokens making up a valid email address + @class EMAIL + @extends MultiToken + */ + var EMAIL = inherits(MultiToken, createTokenClass(), { + type: 'email', + isLink: true, + toHref: function toHref() { + return 'mailto:' + this.toString(); + } + }); + + /** + Represents some plain text + @class TEXT + @extends MultiToken + */ + var TEXT = inherits(MultiToken, createTokenClass(), { type: 'text' }); + + /** + Multi-linebreak token - represents a line break + @class NL + @extends MultiToken + */ + var NL = inherits(MultiToken, createTokenClass(), { type: 'nl' }); + + /** + Represents a list of tokens making up a valid URL + @class URL + @extends MultiToken + */ + var URL = inherits(MultiToken, createTokenClass(), { + type: 'url', + isLink: true, + + /** + Lowercases relevant parts of the domain and adds the protocol if + required. Note that this will not escape unsafe HTML characters in the + URL. + @method href + @param {String} protocol + @return {String} + */ + toHref: function toHref() { + var protocol = arguments.length <= 0 || arguments[0] === undefined ? 'http' : arguments[0]; + + var hasProtocol = false; + var hasSlashSlash = false; + var tokens = this.v; + var result = []; + var i = 0; + + // Make the first part of the domain lowercase + // Lowercase protocol + while (tokens[i] instanceof PROTOCOL) { + hasProtocol = true; + result.push(tokens[i].toString().toLowerCase()); + i++; + } + + // Skip slash-slash + while (tokens[i] instanceof SLASH) { + hasSlashSlash = true; + result.push(tokens[i].toString()); + i++; + } + + // Lowercase all other characters in the domain + while (isDomainToken(tokens[i])) { + result.push(tokens[i].toString().toLowerCase()); + i++; + } + + // Leave all other characters as they were written + for (; i < tokens.length; i++) { + result.push(tokens[i].toString()); + } + + result = result.join(''); + + if (!(hasProtocol || hasSlashSlash)) { + result = protocol + '://' + result; + } + + return result; + }, + hasProtocol: function hasProtocol() { + return this.v[0] instanceof PROTOCOL; + } + }); + + var TOKENS$1 = Object.freeze({ + Base: MultiToken, + EMAIL: EMAIL, + NL: NL, + TEXT: TEXT, + URL: URL + }); + + /** + Not exactly parser, more like the second-stage scanner (although we can + theoretically hotswap the code here with a real parser in the future... but + for a little URL-finding utility abstract syntax trees may be a little + overkill). + + URL format: http://en.wikipedia.org/wiki/URI_scheme + Email format: http://en.wikipedia.org/wiki/Email_address (links to RFC in + reference) + + @module linkify + @submodule parser + @main parser + */ + + var makeState$1 = function makeState$1(tokenClass) { + return new State(tokenClass); + }; + + // The universal starting state. + var S_START$1 = makeState$1(); + + // Intermediate states for URLs. Note that domains that begin with a protocol + // are treated slighly differently from those that don't. + var S_PROTOCOL = makeState$1(); // e.g., 'http:' + var S_PROTOCOL_SLASH = makeState$1(); // e.g., '/', 'http:/'' + var S_PROTOCOL_SLASH_SLASH = makeState$1(); // e.g., '//', 'http://' + var S_DOMAIN$1 = makeState$1(); // parsed string ends with a potential domain name (A) + var S_DOMAIN_DOT = makeState$1(); // (A) domain followed by DOT + var S_TLD = makeState$1(URL); // (A) Simplest possible URL with no query string + var S_TLD_COLON = makeState$1(); // (A) URL followed by colon (potential port number here) + var S_TLD_PORT = makeState$1(URL); // TLD followed by a port number + var S_URL = makeState$1(URL); // Long URL with optional port and maybe query string + var S_URL_NON_ACCEPTING = makeState$1(); // URL followed by some symbols (will not be part of the final URL) + var S_URL_OPENBRACE = makeState$1(); // URL followed by { + var S_URL_OPENBRACKET = makeState$1(); // URL followed by [ + var S_URL_OPENANGLEBRACKET = makeState$1(); // URL followed by < + var S_URL_OPENPAREN = makeState$1(); // URL followed by ( + var S_URL_OPENBRACE_Q = makeState$1(URL); // URL followed by { and some symbols that the URL can end it + var S_URL_OPENBRACKET_Q = makeState$1(URL); // URL followed by [ and some symbols that the URL can end it + var S_URL_OPENANGLEBRACKET_Q = makeState$1(URL); // URL followed by < and some symbols that the URL can end it + var S_URL_OPENPAREN_Q = makeState$1(URL); // URL followed by ( and some symbols that the URL can end it + var S_URL_OPENBRACE_SYMS = makeState$1(); // S_URL_OPENBRACE_Q followed by some symbols it cannot end it + var S_URL_OPENBRACKET_SYMS = makeState$1(); // S_URL_OPENBRACKET_Q followed by some symbols it cannot end it + var S_URL_OPENANGLEBRACKET_SYMS = makeState$1(); // S_URL_OPENANGLEBRACKET_Q followed by some symbols it cannot end it + var S_URL_OPENPAREN_SYMS = makeState$1(); // S_URL_OPENPAREN_Q followed by some symbols it cannot end it + var S_EMAIL_DOMAIN = makeState$1(); // parsed string starts with local email info + @ with a potential domain name (C) + var S_EMAIL_DOMAIN_DOT = makeState$1(); // (C) domain followed by DOT + var S_EMAIL = makeState$1(EMAIL); // (C) Possible email address (could have more tlds) + var S_EMAIL_COLON = makeState$1(); // (C) URL followed by colon (potential port number here) + var S_EMAIL_PORT = makeState$1(EMAIL); // (C) Email address with a port + var S_LOCALPART = makeState$1(); // Local part of the email address + var S_LOCALPART_AT = makeState$1(); // Local part of the email address plus @ + var S_LOCALPART_DOT = makeState$1(); // Local part of the email address plus '.' (localpart cannot end in .) + var S_NL = makeState$1(NL); // single new line + + // Make path from start to protocol (with '//') + S_START$1.on(TNL, S_NL).on(PROTOCOL, S_PROTOCOL).on(SLASH, S_PROTOCOL_SLASH); + + S_PROTOCOL.on(SLASH, S_PROTOCOL_SLASH); + S_PROTOCOL_SLASH.on(SLASH, S_PROTOCOL_SLASH_SLASH); + + // The very first potential domain name + S_START$1.on(TLD, S_DOMAIN$1).on(DOMAIN, S_DOMAIN$1).on(LOCALHOST, S_TLD).on(NUM, S_DOMAIN$1); + + // Force URL for anything sane followed by protocol + S_PROTOCOL_SLASH_SLASH.on(TLD, S_URL).on(DOMAIN, S_URL).on(NUM, S_URL).on(LOCALHOST, S_URL); + + // Account for dots and hyphens + // hyphens are usually parts of domain names + S_DOMAIN$1.on(DOT, S_DOMAIN_DOT); + S_EMAIL_DOMAIN.on(DOT, S_EMAIL_DOMAIN_DOT); + + // Hyphen can jump back to a domain name + + // After the first domain and a dot, we can find either a URL or another domain + S_DOMAIN_DOT.on(TLD, S_TLD).on(DOMAIN, S_DOMAIN$1).on(NUM, S_DOMAIN$1).on(LOCALHOST, S_DOMAIN$1); + + S_EMAIL_DOMAIN_DOT.on(TLD, S_EMAIL).on(DOMAIN, S_EMAIL_DOMAIN).on(NUM, S_EMAIL_DOMAIN).on(LOCALHOST, S_EMAIL_DOMAIN); + + // S_TLD accepts! But the URL could be longer, try to find a match greedily + // The `run` function should be able to "rollback" to the accepting state + S_TLD.on(DOT, S_DOMAIN_DOT); + S_EMAIL.on(DOT, S_EMAIL_DOMAIN_DOT); + + // Become real URLs after `SLASH` or `COLON NUM SLASH` + // Here PSS and non-PSS converge + S_TLD.on(COLON, S_TLD_COLON).on(SLASH, S_URL); + S_TLD_COLON.on(NUM, S_TLD_PORT); + S_TLD_PORT.on(SLASH, S_URL); + S_EMAIL.on(COLON, S_EMAIL_COLON); + S_EMAIL_COLON.on(NUM, S_EMAIL_PORT); + + // Types of characters the URL can definitely end in + var qsAccepting = [DOMAIN, AT, LOCALHOST, NUM, PLUS, POUND, PROTOCOL, SLASH, TLD, UNDERSCORE, SYM]; + + // Types of tokens that can follow a URL and be part of the query string + // but cannot be the very last characters + // Characters that cannot appear in the URL at all should be excluded + var qsNonAccepting = [COLON, DOT, QUERY, PUNCTUATION, CLOSEBRACE, CLOSEBRACKET, CLOSEANGLEBRACKET, CLOSEPAREN, OPENBRACE, OPENBRACKET, OPENANGLEBRACKET, OPENPAREN]; + + // These states are responsible primarily for determining whether or not to + // include the final round bracket. + + // URL, followed by an opening bracket + S_URL.on(OPENBRACE, S_URL_OPENBRACE).on(OPENBRACKET, S_URL_OPENBRACKET).on(OPENANGLEBRACKET, S_URL_OPENANGLEBRACKET).on(OPENPAREN, S_URL_OPENPAREN); + + // URL with extra symbols at the end, followed by an opening bracket + S_URL_NON_ACCEPTING.on(OPENBRACE, S_URL_OPENBRACE).on(OPENBRACKET, S_URL_OPENBRACKET).on(OPENANGLEBRACKET, S_URL_OPENANGLEBRACKET).on(OPENPAREN, S_URL_OPENPAREN); + + // Closing bracket component. This character WILL be included in the URL + S_URL_OPENBRACE.on(CLOSEBRACE, S_URL); + S_URL_OPENBRACKET.on(CLOSEBRACKET, S_URL); + S_URL_OPENANGLEBRACKET.on(CLOSEANGLEBRACKET, S_URL); + S_URL_OPENPAREN.on(CLOSEPAREN, S_URL); + S_URL_OPENBRACE_Q.on(CLOSEBRACE, S_URL); + S_URL_OPENBRACKET_Q.on(CLOSEBRACKET, S_URL); + S_URL_OPENANGLEBRACKET_Q.on(CLOSEANGLEBRACKET, S_URL); + S_URL_OPENPAREN_Q.on(CLOSEPAREN, S_URL); + S_URL_OPENBRACE_SYMS.on(CLOSEBRACE, S_URL); + S_URL_OPENBRACKET_SYMS.on(CLOSEBRACKET, S_URL); + S_URL_OPENANGLEBRACKET_SYMS.on(CLOSEANGLEBRACKET, S_URL); + S_URL_OPENPAREN_SYMS.on(CLOSEPAREN, S_URL); + + // URL that beings with an opening bracket, followed by a symbols. + // Note that the final state can still be `S_URL_OPENBRACE_Q` (if the URL only + // has a single opening bracket for some reason). + S_URL_OPENBRACE.on(qsAccepting, S_URL_OPENBRACE_Q); + S_URL_OPENBRACKET.on(qsAccepting, S_URL_OPENBRACKET_Q); + S_URL_OPENANGLEBRACKET.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q); + S_URL_OPENPAREN.on(qsAccepting, S_URL_OPENPAREN_Q); + S_URL_OPENBRACE.on(qsNonAccepting, S_URL_OPENBRACE_SYMS); + S_URL_OPENBRACKET.on(qsNonAccepting, S_URL_OPENBRACKET_SYMS); + S_URL_OPENANGLEBRACKET.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_SYMS); + S_URL_OPENPAREN.on(qsNonAccepting, S_URL_OPENPAREN_SYMS); + + // URL that begins with an opening bracket, followed by some symbols + S_URL_OPENBRACE_Q.on(qsAccepting, S_URL_OPENBRACE_Q); + S_URL_OPENBRACKET_Q.on(qsAccepting, S_URL_OPENBRACKET_Q); + S_URL_OPENANGLEBRACKET_Q.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q); + S_URL_OPENPAREN_Q.on(qsAccepting, S_URL_OPENPAREN_Q); + S_URL_OPENBRACE_Q.on(qsNonAccepting, S_URL_OPENBRACE_Q); + S_URL_OPENBRACKET_Q.on(qsNonAccepting, S_URL_OPENBRACKET_Q); + S_URL_OPENANGLEBRACKET_Q.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_Q); + S_URL_OPENPAREN_Q.on(qsNonAccepting, S_URL_OPENPAREN_Q); + + S_URL_OPENBRACE_SYMS.on(qsAccepting, S_URL_OPENBRACE_Q); + S_URL_OPENBRACKET_SYMS.on(qsAccepting, S_URL_OPENBRACKET_Q); + S_URL_OPENANGLEBRACKET_SYMS.on(qsAccepting, S_URL_OPENANGLEBRACKET_Q); + S_URL_OPENPAREN_SYMS.on(qsAccepting, S_URL_OPENPAREN_Q); + S_URL_OPENBRACE_SYMS.on(qsNonAccepting, S_URL_OPENBRACE_SYMS); + S_URL_OPENBRACKET_SYMS.on(qsNonAccepting, S_URL_OPENBRACKET_SYMS); + S_URL_OPENANGLEBRACKET_SYMS.on(qsNonAccepting, S_URL_OPENANGLEBRACKET_SYMS); + S_URL_OPENPAREN_SYMS.on(qsNonAccepting, S_URL_OPENPAREN_SYMS); + + // Account for the query string + S_URL.on(qsAccepting, S_URL); + S_URL_NON_ACCEPTING.on(qsAccepting, S_URL); + + S_URL.on(qsNonAccepting, S_URL_NON_ACCEPTING); + S_URL_NON_ACCEPTING.on(qsNonAccepting, S_URL_NON_ACCEPTING); + + // Email address-specific state definitions + // Note: We are not allowing '/' in email addresses since this would interfere + // with real URLs + + // Tokens allowed in the localpart of the email + var localpartAccepting = [DOMAIN, NUM, PLUS, POUND, QUERY, UNDERSCORE, SYM, TLD]; + + // Some of the tokens in `localpartAccepting` are already accounted for here and + // will not be overwritten (don't worry) + S_DOMAIN$1.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT); + S_TLD.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT); + S_DOMAIN_DOT.on(localpartAccepting, S_LOCALPART); + + // Okay we're on a localpart. Now what? + // TODO: IP addresses and what if the email starts with numbers? + S_LOCALPART.on(localpartAccepting, S_LOCALPART).on(AT, S_LOCALPART_AT) // close to an email address now + .on(DOT, S_LOCALPART_DOT); + S_LOCALPART_DOT.on(localpartAccepting, S_LOCALPART); + S_LOCALPART_AT.on(TLD, S_EMAIL_DOMAIN).on(DOMAIN, S_EMAIL_DOMAIN).on(LOCALHOST, S_EMAIL); + // States following `@` defined above + + var run$1 = function run$1(tokens) { + var len = tokens.length; + var cursor = 0; + var multis = []; + var textTokens = []; + + while (cursor < len) { + var state = S_START$1; + var secondState = null; + var nextState = null; + var multiLength = 0; + var latestAccepting = null; + var sinceAccepts = -1; + + while (cursor < len && !(secondState = state.next(tokens[cursor]))) { + // Starting tokens with nowhere to jump to. + // Consider these to be just plain text + textTokens.push(tokens[cursor++]); + } + + while (cursor < len && (nextState = secondState || state.next(tokens[cursor]))) { + + // Get the next state + secondState = null; + state = nextState; + + // Keep track of the latest accepting state + if (state.accepts()) { + sinceAccepts = 0; + latestAccepting = state; + } else if (sinceAccepts >= 0) { + sinceAccepts++; + } + + cursor++; + multiLength++; + } + + if (sinceAccepts < 0) { + + // No accepting state was found, part of a regular text token + // Add all the tokens we looked at to the text tokens array + for (var _i3 = cursor - multiLength; _i3 < cursor; _i3++) { + textTokens.push(tokens[_i3]); + } + } else { + + // Accepting state! + + // First close off the textTokens (if available) + if (textTokens.length > 0) { + multis.push(new TEXT(textTokens)); + textTokens = []; + } + + // Roll back to the latest accepting state + cursor -= sinceAccepts; + multiLength -= sinceAccepts; + + // Create a new multitoken + var MULTI = latestAccepting.emit(); + multis.push(new MULTI(tokens.slice(cursor - multiLength, cursor))); + } + } + + // Finally close off the textTokens (if available) + if (textTokens.length > 0) { + multis.push(new TEXT(textTokens)); + } + + return multis; + }; + + var parser = Object.freeze({ + State: State, + TOKENS: TOKENS$1, + run: run$1, + start: S_START$1 + }); + + if (!Array.isArray) { + Array.isArray = function (arg) { + return Object.prototype.toString.call(arg) === '[object Array]'; + }; + } + + /** + Converts a string into tokens that represent linkable and non-linkable bits + @method tokenize + @param {String} str + @return {Array} tokens + */ + var tokenize = function tokenize(str) { + return run$1(run(str)); + }; + + /** + Returns a list of linkable items in the given string. + */ + var find = function find(str) { + var type = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1]; + + var tokens = tokenize(str); + var filtered = []; + + for (var i = 0; i < tokens.length; i++) { + var token = tokens[i]; + if (token.isLink && (!type || token.type === type)) { + filtered.push(token.toObject()); + } + } + + return filtered; + }; + + /** + Is the given string valid linkable text of some sort + Note that this does not trim the text for you. + + Optionally pass in a second `type` param, which is the type of link to test + for. + + For example, + + test(str, 'email'); + + Will return `true` if str is a valid email. + */ + var test = function test(str) { + var type = arguments.length <= 1 || arguments[1] === undefined ? null : arguments[1]; + + var tokens = tokenize(str); + return tokens.length === 1 && tokens[0].isLink && (!type || tokens[0].type === type); + }; + + exports.find = find; + exports.inherits = inherits; + exports.options = options; + exports.parser = parser; + exports.scanner = scanner; + exports.test = test; + exports.tokenize = tokenize; +})(window.linkify = window.linkify || {}); +})(); diff --git a/web/qwebchannel.js b/web/qwebchannel.js new file mode 100644 index 0000000000000000000000000000000000000000..abf8461be4f726e3b6ea4b8a239f507b05f61c31 --- /dev/null +++ b/web/qwebchannel.js @@ -0,0 +1,427 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtWebChannel module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +"use strict"; + +var QWebChannelMessageTypes = { + signal: 1, + propertyUpdate: 2, + init: 3, + idle: 4, + debug: 5, + invokeMethod: 6, + connectToSignal: 7, + disconnectFromSignal: 8, + setProperty: 9, + response: 10, +}; + +var QWebChannel = function(transport, initCallback) +{ + if (typeof transport !== "object" || typeof transport.send !== "function") { + console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + + " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); + return; + } + + var channel = this; + this.transport = transport; + + this.send = function(data) + { + if (typeof(data) !== "string") { + data = JSON.stringify(data); + } + channel.transport.send(data); + } + + this.transport.onmessage = function(message) + { + var data = message.data; + if (typeof data === "string") { + data = JSON.parse(data); + } + switch (data.type) { + case QWebChannelMessageTypes.signal: + channel.handleSignal(data); + break; + case QWebChannelMessageTypes.response: + channel.handleResponse(data); + break; + case QWebChannelMessageTypes.propertyUpdate: + channel.handlePropertyUpdate(data); + break; + default: + console.error("invalid message received:", message.data); + break; + } + } + + this.execCallbacks = {}; + this.execId = 0; + this.exec = function(data, callback) + { + if (!callback) { + // if no callback is given, send directly + channel.send(data); + return; + } + if (channel.execId === Number.MAX_VALUE) { + // wrap + channel.execId = Number.MIN_VALUE; + } + if (data.hasOwnProperty("id")) { + console.error("Cannot exec message with property id: " + JSON.stringify(data)); + return; + } + data.id = channel.execId++; + channel.execCallbacks[data.id] = callback; + channel.send(data); + }; + + this.objects = {}; + + this.handleSignal = function(message) + { + var object = channel.objects[message.object]; + if (object) { + object.signalEmitted(message.signal, message.args); + } else { + console.warn("Unhandled signal: " + message.object + "::" + message.signal); + } + } + + this.handleResponse = function(message) + { + if (!message.hasOwnProperty("id")) { + console.error("Invalid response message received: ", JSON.stringify(message)); + return; + } + channel.execCallbacks[message.id](message.data); + delete channel.execCallbacks[message.id]; + } + + this.handlePropertyUpdate = function(message) + { + for (var i in message.data) { + var data = message.data[i]; + var object = channel.objects[data.object]; + if (object) { + object.propertyUpdate(data.signals, data.properties); + } else { + console.warn("Unhandled property update: " + data.object + "::" + data.signal); + } + } + channel.exec({type: QWebChannelMessageTypes.idle}); + } + + this.debug = function(message) + { + channel.send({type: QWebChannelMessageTypes.debug, data: message}); + }; + + channel.exec({type: QWebChannelMessageTypes.init}, function(data) { + for (var objectName in data) { + var object = new QObject(objectName, data[objectName], channel); + } + // now unwrap properties, which might reference other registered objects + for (var objectName in channel.objects) { + channel.objects[objectName].unwrapProperties(); + } + if (initCallback) { + initCallback(channel); + } + channel.exec({type: QWebChannelMessageTypes.idle}); + }); +}; + +function QObject(name, data, webChannel) +{ + this.__id__ = name; + webChannel.objects[name] = this; + + // List of callbacks that get invoked upon signal emission + this.__objectSignals__ = {}; + + // Cache of all properties, updated when a notify signal is emitted + this.__propertyCache__ = {}; + + var object = this; + + // ---------------------------------------------------------------------- + + this.unwrapQObject = function(response) + { + if (response instanceof Array) { + // support list of objects + var ret = new Array(response.length); + for (var i = 0; i < response.length; ++i) { + ret[i] = object.unwrapQObject(response[i]); + } + return ret; + } + if (!response + || !response["__QObject*__"] + || response.id === undefined) { + return response; + } + + var objectId = response.id; + if (webChannel.objects[objectId]) + return webChannel.objects[objectId]; + + if (!response.data) { + console.error("Cannot unwrap unknown QObject " + objectId + " without data."); + return; + } + + var qObject = new QObject( objectId, response.data, webChannel ); + qObject.destroyed.connect(function() { + if (webChannel.objects[objectId] === qObject) { + delete webChannel.objects[objectId]; + // reset the now deleted QObject to an empty {} object + // just assigning {} though would not have the desired effect, but the + // below also ensures all external references will see the empty map + // NOTE: this detour is necessary to workaround QTBUG-40021 + var propertyNames = []; + for (var propertyName in qObject) { + propertyNames.push(propertyName); + } + for (var idx in propertyNames) { + delete qObject[propertyNames[idx]]; + } + } + }); + // here we are already initialized, and thus must directly unwrap the properties + qObject.unwrapProperties(); + return qObject; + } + + this.unwrapProperties = function() + { + for (var propertyIdx in object.__propertyCache__) { + object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); + } + } + + function addSignal(signalData, isPropertyNotifySignal) + { + var signalName = signalData[0]; + var signalIndex = signalData[1]; + object[signalName] = { + connect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to connect to signal " + signalName); + return; + } + + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + object.__objectSignals__[signalIndex].push(callback); + + if (!isPropertyNotifySignal && signalName !== "destroyed") { + // only required for "pure" signals, handled separately for properties in propertyUpdate + // also note that we always get notified about the destroyed signal + webChannel.exec({ + type: QWebChannelMessageTypes.connectToSignal, + object: object.__id__, + signal: signalIndex + }); + } + }, + disconnect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to disconnect from signal " + signalName); + return; + } + object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; + var idx = object.__objectSignals__[signalIndex].indexOf(callback); + if (idx === -1) { + console.error("Cannot find connection of signal " + signalName + " to " + callback.name); + return; + } + object.__objectSignals__[signalIndex].splice(idx, 1); + if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { + // only required for "pure" signals, handled separately for properties in propertyUpdate + webChannel.exec({ + type: QWebChannelMessageTypes.disconnectFromSignal, + object: object.__id__, + signal: signalIndex + }); + } + } + }; + } + + /** + * Invokes all callbacks for the given signalname. Also works for property notify callbacks. + */ + function invokeSignalCallbacks(signalName, signalArgs) + { + var connections = object.__objectSignals__[signalName]; + if (connections) { + connections.forEach(function(callback) { + callback.apply(callback, signalArgs); + }); + } + } + + this.propertyUpdate = function(signals, propertyMap) + { + // update property cache + for (var propertyIndex in propertyMap) { + var propertyValue = propertyMap[propertyIndex]; + object.__propertyCache__[propertyIndex] = propertyValue; + } + + for (var signalName in signals) { + // Invoke all callbacks, as signalEmitted() does not. This ensures the + // property cache is updated before the callbacks are invoked. + invokeSignalCallbacks(signalName, signals[signalName]); + } + } + + this.signalEmitted = function(signalName, signalArgs) + { + invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs)); + } + + function addMethod(methodData) + { + var methodName = methodData[0]; + var methodIdx = methodData[1]; + object[methodName] = function() { + var args = []; + var callback; + for (var i = 0; i < arguments.length; ++i) { + var argument = arguments[i]; + if (typeof argument === "function") + callback = argument; + else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined) + args.push({ + "id": argument.__id__ + }); + else + args.push(argument); + } + + webChannel.exec({ + "type": QWebChannelMessageTypes.invokeMethod, + "object": object.__id__, + "method": methodIdx, + "args": args + }, function(response) { + if (response !== undefined) { + var result = object.unwrapQObject(response); + if (callback) { + (callback)(result); + } + } + }); + }; + } + + function bindGetterSetter(propertyInfo) + { + var propertyIndex = propertyInfo[0]; + var propertyName = propertyInfo[1]; + var notifySignalData = propertyInfo[2]; + // initialize property cache with current value + // NOTE: if this is an object, it is not directly unwrapped as it might + // reference other QObject that we do not know yet + object.__propertyCache__[propertyIndex] = propertyInfo[3]; + + if (notifySignalData) { + if (notifySignalData[0] === 1) { + // signal name is optimized away, reconstruct the actual name + notifySignalData[0] = propertyName + "Changed"; + } + addSignal(notifySignalData, true); + } + + Object.defineProperty(object, propertyName, { + configurable: true, + get: function () { + var propertyValue = object.__propertyCache__[propertyIndex]; + if (propertyValue === undefined) { + // This shouldn't happen + console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); + } + + return propertyValue; + }, + set: function(value) { + if (value === undefined) { + console.warn("Property setter for " + propertyName + " called with undefined value!"); + return; + } + object.__propertyCache__[propertyIndex] = value; + var valueToSend = value; + if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined) + valueToSend = { "id": valueToSend.__id__ }; + webChannel.exec({ + "type": QWebChannelMessageTypes.setProperty, + "object": object.__id__, + "property": propertyIndex, + "value": valueToSend + }); + } + }); + + } + + // ---------------------------------------------------------------------- + + data.methods.forEach(addMethod); + + data.properties.forEach(bindGetterSetter); + + data.signals.forEach(function(signal) { addSignal(signal, false); }); + + for (var name in data.enums) { + object[name] = data.enums[name]; + } +} + +//required for use with nodejs +if (typeof module === 'object') { + module.exports = { + QWebChannel: QWebChannel + }; +} diff --git a/webchathelpers.cpp b/webchathelpers.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9503d2dbe984f5bb511e01ed88e5c25bfbbaab31 --- /dev/null +++ b/webchathelpers.cpp @@ -0,0 +1,141 @@ +/*************************************************************************** + * Copyright (C) 2017-2018 by Savoir-faire Linux * + * Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com> * + * Author: S�bastien Blin <sebastien.blin@savoirfairelinux.com> * + * Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com> * + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * + + * * + * 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/>. * + **************************************************************************/ + +#include "webchathelpers.h" + +QJsonObject +buildInteractionJson(lrc::api::ConversationModel& conversationModel, + const uint64_t msgId, + const lrc::api::interaction::Info& interaction) +{ + auto sender = QString(interaction.authorUri.c_str()); + auto timestamp = QString::number(interaction.timestamp); + auto direction = lrc::api::interaction::isOutgoing(interaction) ? QString("out") : QString("in"); + + QJsonObject interactionObject = QJsonObject(); + interactionObject.insert("text", QJsonValue(QString(interaction.body.c_str()))); + interactionObject.insert("id", QJsonValue(QString::number(msgId))); + interactionObject.insert("sender", QJsonValue(sender)); + interactionObject.insert("sender_contact_method", QJsonValue(sender)); + interactionObject.insert("timestamp", QJsonValue(timestamp)); + interactionObject.insert("direction", QJsonValue(direction)); + + switch (interaction.type) + { + case lrc::api::interaction::Type::TEXT: + interactionObject.insert("type", QJsonValue("text")); + break; + case lrc::api::interaction::Type::CALL: + interactionObject.insert("type", QJsonValue("call")); + break; + case lrc::api::interaction::Type::CONTACT: + interactionObject.insert("type", QJsonValue("contact")); + break; + case lrc::api::interaction::Type::OUTGOING_DATA_TRANSFER: + case lrc::api::interaction::Type::INCOMING_DATA_TRANSFER: { + interactionObject.insert("type", QJsonValue("data_transfer")); + lrc::api::datatransfer::Info info = {}; + conversationModel.getTransferInfo(msgId, info); + if (info.status != lrc::api::datatransfer::Status::INVALID) { + interactionObject.insert("totalSize", QJsonValue(qint64(info.totalSize))); + interactionObject.insert("progress", QJsonValue(qint64(info.progress))); + } + break; + } + case lrc::api::interaction::Type::INVALID: + default: + interactionObject.insert("type", QJsonValue("")); + break; + } + + switch (interaction.status) + { + case lrc::api::interaction::Status::READ: + interactionObject.insert("delivery_status", QJsonValue("read")); + break; + case lrc::api::interaction::Status::SUCCEED: + interactionObject.insert("delivery_status", QJsonValue("sent")); + break; + case lrc::api::interaction::Status::FAILED: + case lrc::api::interaction::Status::TRANSFER_ERROR: + interactionObject.insert("delivery_status", QJsonValue("failure")); + break; + case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER: + interactionObject.insert("delivery_status", QJsonValue("unjoinable peer")); + break; + case lrc::api::interaction::Status::SENDING: + interactionObject.insert("delivery_status", QJsonValue("sending")); + break; + case lrc::api::interaction::Status::TRANSFER_CREATED: + interactionObject.insert("delivery_status", QJsonValue("connecting")); + break; + case lrc::api::interaction::Status::TRANSFER_ACCEPTED: + interactionObject.insert("delivery_status", QJsonValue("accepted")); + break; + case lrc::api::interaction::Status::TRANSFER_CANCELED: + interactionObject.insert("delivery_status", QJsonValue("canceled")); + break; + case lrc::api::interaction::Status::TRANSFER_ONGOING: + interactionObject.insert("delivery_status", QJsonValue("ongoing")); + break; + case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER: + interactionObject.insert("delivery_status", QJsonValue("awaiting peer")); + break; + case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST: + interactionObject.insert("delivery_status", QJsonValue("awaiting host")); + break; + case lrc::api::interaction::Status::TRANSFER_TIMEOUT_EXPIRED: + interactionObject.insert("delivery_status", QJsonValue("awaiting peer timeout")); + break; + case lrc::api::interaction::Status::TRANSFER_FINISHED: + interactionObject.insert("delivery_status", QJsonValue("finished")); + break; + case lrc::api::interaction::Status::INVALID: + case lrc::api::interaction::Status::UNKNOWN: + case lrc::api::interaction::Status::UNREAD: + default: + interactionObject.insert("delivery_status", QJsonValue("unknown")); + break; + } + return interactionObject; +} + +QString +interactionToJsonInteractionObject(lrc::api::ConversationModel& conversationModel, + const uint64_t msgId, + const lrc::api::interaction::Info& interaction) +{ + auto interactionObject = buildInteractionJson(conversationModel, msgId, interaction); + return QString(QJsonDocument(interactionObject).toJson(QJsonDocument::Compact)); +} + +QString +interactionsToJsonArrayObject(lrc::api::ConversationModel& conversationModel, + const std::map<uint64_t, + lrc::api::interaction::Info> interactions) +{ + QJsonArray array; + for (const auto& interaction : interactions) { + array.append(buildInteractionJson(conversationModel, interaction.first, interaction.second)); + } + return QString(QJsonDocument(array).toJson(QJsonDocument::Compact)); +} \ No newline at end of file diff --git a/webchathelpers.h b/webchathelpers.h new file mode 100644 index 0000000000000000000000000000000000000000..6267a2c8e60e0f6ea218582a471c304a9bd95691 --- /dev/null +++ b/webchathelpers.h @@ -0,0 +1,40 @@ +/*************************************************************************** + * Copyright (C) 2017-2018 by Savoir-faire Linux * + * Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com> * + * Author: S�bastien Blin <sebastien.blin@savoirfairelinux.com> * + * Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com> * + * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> * + + * * + * 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/>. * + **************************************************************************/ + +#pragma once + +#include <QJsonObject> +#include <QJsonDocument> +#include <QJsonArray> +#include <QFile> + +#include "api/conversationmodel.h" + +QJsonObject buildInteractionJson(lrc::api::ConversationModel& conversationModel, + const uint64_t msgId, + const lrc::api::interaction::Info& interaction); +QString interactionToJsonInteractionObject(lrc::api::ConversationModel& conversationModel, + const uint64_t msgId, + const lrc::api::interaction::Info& interaction); +QString interactionsToJsonArrayObject(lrc::api::ConversationModel& conversationModel, + const std::map<uint64_t, + lrc::api::interaction::Info> interactions); \ No newline at end of file