Commit c1b38dc6 authored by Ronan Abhamon's avatar Ronan Abhamon

feat(app): build linphone core in a specific thread

parent 5944653b
...@@ -62,7 +62,7 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}") ...@@ -62,7 +62,7 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}")
# Define packages, libs, sources, headers, resources and languages. # Define packages, libs, sources, headers, resources and languages.
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
set(QT5_PACKAGES Core Gui Quick Widgets QuickControls2 Svg LinguistTools Network) set(QT5_PACKAGES Core Gui Quick Widgets QuickControls2 Svg LinguistTools Network Concurrent)
find_package(BcToolbox REQUIRED) find_package(BcToolbox REQUIRED)
find_package(Belcard REQUIRED) find_package(Belcard REQUIRED)
......
...@@ -95,6 +95,10 @@ App::~App () { ...@@ -95,6 +95,10 @@ App::~App () {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void App::initContentApp () { void App::initContentApp () {
// Init core.
CoreManager::init(this, m_parser.value("config"));
qInfo() << "Activated selectors:" << QQmlFileSelector::get(&m_engine)->selector()->allSelectors();
// Avoid double free. // Avoid double free.
m_engine.setObjectOwnership(this, QQmlEngine::CppOwnership); m_engine.setObjectOwnership(this, QQmlEngine::CppOwnership);
...@@ -117,66 +121,44 @@ void App::initContentApp () { ...@@ -117,66 +121,44 @@ void App::initContentApp () {
// Don't quit if last window is closed!!! // Don't quit if last window is closed!!!
setQuitOnLastWindowClosed(false); setQuitOnLastWindowClosed(false);
// Init core.
CoreManager::init(nullptr, m_parser.value("config"));
qInfo() << "Core manager initialized.";
qInfo() << "Activated selectors:" << QQmlFileSelector::get(&m_engine)->selector()->allSelectors();
// Try to use preferred locale.
{
QString locale = getConfigLocale();
if (!locale.isEmpty()) {
DefaultTranslator *translator = new DefaultTranslator(this);
if (installLocale(*this, *translator, QLocale(locale))) {
// Use config.
m_translator->deleteLater();
m_translator = translator;
m_locale = locale;
qInfo() << QStringLiteral("Use preferred locale: %1").arg(locale);
} else {
// Reset config.
setConfigLocale("");
translator->deleteLater();
}
}
}
// Register types. // Register types.
registerTypes(); registerTypes();
// Enable notifications. // Enable notifications.
m_notifier = new Notifier(this); m_notifier = new Notifier(this);
{
CoreManager *core = CoreManager::getInstance();
core->enableHandlers();
core->setParent(this);
}
// Load main view. // Load main view.
qInfo() << "Loading main view..."; qInfo() << "Loading main view...";
m_engine.load(QUrl(QML_VIEW_MAIN_WINDOW)); m_engine.load(QUrl(QML_VIEW_MAIN_WINDOW));
if (m_engine.rootObjects().isEmpty()) if (m_engine.rootObjects().isEmpty())
qFatal("Unable to open main window."); qFatal("Unable to open main window.");
#ifndef __APPLE__ CoreManager *core = CoreManager::getInstance();
// Enable TrayIconSystem.
if (!QSystemTrayIcon::isSystemTrayAvailable())
qWarning("System tray not found on this system.");
else
setTrayIcon();
if (!m_parser.isSet("iconified"))
getMainWindow()->showNormal();
#else
getMainWindow()->showNormal();
#endif // ifndef __APPLE__
if (m_parser.isSet("selftest")) if (m_parser.isSet("selftest"))
QTimer::singleShot(300, this, &App::quit); QObject::connect(core, &CoreManager::linphoneCoreCreated, this, &App::quit);
else
QObject::connect(
core, &CoreManager::linphoneCoreCreated, this, [core, this]() {
tryToUsePreferredLocale();
qInfo() << QStringLiteral("Linphone core created.");
core->enableHandlers();
#ifndef __APPLE__
// Enable TrayIconSystem.
if (!QSystemTrayIcon::isSystemTrayAvailable())
qWarning("System tray not found on this system.");
else
setTrayIcon();
if (!m_parser.isSet("iconified"))
getMainWindow()->showNormal();
#else
getMainWindow()->showNormal();
#endif // ifndef __APPLE__
}
);
QObject::connect( QObject::connect(
this, &App::receivedMessage, this, [this](int, QByteArray message) { this, &App::receivedMessage, this, [this](int, QByteArray message) {
...@@ -213,6 +195,29 @@ void App::parseArgs () { ...@@ -213,6 +195,29 @@ void App::parseArgs () {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void App::tryToUsePreferredLocale () {
QString locale = getConfigLocale();
if (!locale.isEmpty()) {
DefaultTranslator *translator = new DefaultTranslator(this);
if (installLocale(*this, *translator, QLocale(locale))) {
// Use config.
m_translator->deleteLater();
m_translator = translator;
m_locale = locale;
qInfo() << QStringLiteral("Use preferred locale: %1").arg(locale);
} else {
// Reset config.
setConfigLocale("");
translator->deleteLater();
}
}
}
// -----------------------------------------------------------------------------
inline QQuickWindow *createSubWindow (App *app, const char *path) { inline QQuickWindow *createSubWindow (App *app, const char *path) {
QQmlEngine *engine = app->getEngine(); QQmlEngine *engine = app->getEngine();
......
...@@ -24,13 +24,12 @@ ...@@ -24,13 +24,12 @@
#define APP_H_ #define APP_H_
#include "../components/notifier/Notifier.hpp" #include "../components/notifier/Notifier.hpp"
#include "../externals/single-application/SingleApplication.hpp"
#include <QCommandLineParser> #include <QCommandLineParser>
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQuickWindow> #include <QQuickWindow>
#include "../externals/single-application/SingleApplication.hpp"
// ============================================================================= // =============================================================================
class DefaultTranslator; class DefaultTranslator;
...@@ -49,6 +48,8 @@ public: ...@@ -49,6 +48,8 @@ public:
void initContentApp (); void initContentApp ();
void parseArgs (); void parseArgs ();
void tryToUsePreferredLocale ();
QQmlEngine *getEngine () { QQmlEngine *getEngine () {
return &m_engine; return &m_engine;
} }
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QtConcurrent>
#include <QTimer> #include <QTimer>
using namespace std; using namespace std;
...@@ -37,35 +38,34 @@ using namespace std; ...@@ -37,35 +38,34 @@ using namespace std;
CoreManager *CoreManager::m_instance = nullptr; CoreManager *CoreManager::m_instance = nullptr;
CoreManager::CoreManager (QObject *parent, const QString &config_path) : QObject(parent), m_handlers(make_shared<CoreHandlers>()) { CoreManager::CoreManager (QObject *parent, const QString &config_path) : QObject(parent), m_handlers(make_shared<CoreHandlers>()) {
// TODO: activate migration when ready to switch to this new version m_promise_build = QtConcurrent::run(this, &CoreManager::createLinphoneCore, config_path);
// Paths::migrate();
setResourcesPaths();
m_core = linphone::Factory::get()->createCore(m_handlers, Paths::getConfigFilepath(config_path), ""); QObject::connect(
&m_promise_watcher, &QFutureWatcher<void>::finished, this, []() {
m_instance->m_calls_list_model = new CallsListModel(m_instance);
m_instance->m_contacts_list_model = new ContactsListModel(m_instance);
m_instance->m_sip_addresses_model = new SipAddressesModel(m_instance);
m_instance->m_settings_model = new SettingsModel(m_instance);
m_core->setVideoDisplayFilter("MSOGL"); emit m_instance->linphoneCoreCreated();
m_core->usePreviewWindow(true); }
);
setDatabasesPaths(); m_promise_watcher.setFuture(m_promise_build);
setOtherPaths();
} }
void CoreManager::enableHandlers () { void CoreManager::enableHandlers () {
m_cbs_timer->start(); m_cbs_timer->start();
} }
// -----------------------------------------------------------------------------
void CoreManager::init (QObject *parent, const QString &config_path) { void CoreManager::init (QObject *parent, const QString &config_path) {
if (m_instance) if (m_instance)
return; return;
m_instance = new CoreManager(parent, config_path); m_instance = new CoreManager(parent, config_path);
m_instance->m_calls_list_model = new CallsListModel(m_instance);
m_instance->m_contacts_list_model = new ContactsListModel(m_instance);
m_instance->m_sip_addresses_model = new SipAddressesModel(m_instance);
m_instance->m_settings_model = new SettingsModel(m_instance);
QTimer *timer = m_instance->m_cbs_timer = new QTimer(m_instance); QTimer *timer = m_instance->m_cbs_timer = new QTimer(m_instance);
timer->setInterval(20); timer->setInterval(20);
...@@ -117,3 +117,22 @@ void CoreManager::setResourcesPaths () { ...@@ -117,3 +117,22 @@ void CoreManager::setResourcesPaths () {
factory->setTopResourcesDir(::Utils::qStringToLinphoneString(datadir.absolutePath())); factory->setTopResourcesDir(::Utils::qStringToLinphoneString(datadir.absolutePath()));
} }
} }
// -----------------------------------------------------------------------------
void CoreManager::createLinphoneCore (const QString &config_path) {
qInfo() << QStringLiteral("Launch async linphone core creation.");
// TODO: activate migration when ready to switch to this new version
// Paths::migrate();
setResourcesPaths();
m_core = linphone::Factory::get()->createCore(m_handlers, Paths::getConfigFilepath(config_path), "");
m_core->setVideoDisplayFilter("MSOGL");
m_core->usePreviewWindow(true);
setDatabasesPaths();
setOtherPaths();
}
...@@ -30,6 +30,8 @@ ...@@ -30,6 +30,8 @@
#include "CoreHandlers.hpp" #include "CoreHandlers.hpp"
#include <QFuture>
#include <QFutureWatcher>
#include <QMutex> #include <QMutex>
// ============================================================================= // =============================================================================
...@@ -39,6 +41,8 @@ class QTimer; ...@@ -39,6 +41,8 @@ class QTimer;
class CoreManager : public QObject { class CoreManager : public QObject {
Q_OBJECT; Q_OBJECT;
Q_PROPERTY(bool linphoneCoreCreated READ getLinphoneCoreCreated NOTIFY linphoneCoreCreated);
public: public:
~CoreManager () = default; ~CoreManager () = default;
...@@ -102,6 +106,9 @@ public: ...@@ -102,6 +106,9 @@ public:
Q_INVOKABLE void forceRefreshRegisters (); Q_INVOKABLE void forceRefreshRegisters ();
signals:
void linphoneCoreCreated ();
private: private:
CoreManager (QObject *parent, const QString &config_path); CoreManager (QObject *parent, const QString &config_path);
...@@ -109,6 +116,12 @@ private: ...@@ -109,6 +116,12 @@ private:
void setOtherPaths (); void setOtherPaths ();
void setResourcesPaths (); void setResourcesPaths ();
void createLinphoneCore (const QString &config_path);
bool getLinphoneCoreCreated () {
return m_promise_build.isFinished();
}
std::shared_ptr<linphone::Core> m_core; std::shared_ptr<linphone::Core> m_core;
std::shared_ptr<CoreHandlers> m_handlers; std::shared_ptr<CoreHandlers> m_handlers;
...@@ -119,6 +132,9 @@ private: ...@@ -119,6 +132,9 @@ private:
QTimer *m_cbs_timer; QTimer *m_cbs_timer;
QFuture<void> m_promise_build;
QFutureWatcher<void> m_promise_watcher;
QMutex m_mutex_video_render; QMutex m_mutex_video_render;
static CoreManager *m_instance; static CoreManager *m_instance;
......
...@@ -20,10 +20,7 @@ ...@@ -20,10 +20,7 @@
* Author: Ronan Abhamon * Author: Ronan Abhamon
*/ */
#include <iostream>
#include "app/App.hpp" #include "app/App.hpp"
#include "app/Logger.hpp"
using namespace std; using namespace std;
...@@ -33,6 +30,10 @@ int main (int argc, char *argv[]) { ...@@ -33,6 +30,10 @@ int main (int argc, char *argv[]) {
// Disable QML cache. Avoid malformed cache. // Disable QML cache. Avoid malformed cache.
qputenv("QML_DISABLE_DISK_CACHE", "true"); qputenv("QML_DISABLE_DISK_CACHE", "true");
// ---------------------------------------------------------------------------
// OpenGL properties.
// ---------------------------------------------------------------------------
// Options to get a nice video render. // Options to get a nice video render.
#ifdef _WIN32 #ifdef _WIN32
QCoreApplication::setAttribute(Qt::AA_UseOpenGLES, true); QCoreApplication::setAttribute(Qt::AA_UseOpenGLES, true);
...@@ -58,6 +59,10 @@ int main (int argc, char *argv[]) { ...@@ -58,6 +59,10 @@ int main (int argc, char *argv[]) {
QSurfaceFormat::setDefaultFormat(format); QSurfaceFormat::setDefaultFormat(format);
} }
// ---------------------------------------------------------------------------
// App creation.
// ---------------------------------------------------------------------------
App app(argc, argv); App app(argc, argv);
app.parseArgs(); app.parseArgs();
...@@ -69,5 +74,6 @@ int main (int argc, char *argv[]) { ...@@ -69,5 +74,6 @@ int main (int argc, char *argv[]) {
app.initContentApp(); app.initContentApp();
// Run! // Run!
qInfo() << "Running app...";
return app.exec(); return app.exec();
} }
...@@ -8,6 +8,16 @@ ...@@ -8,6 +8,16 @@
// ============================================================================= // =============================================================================
function handleActiveFocusItemChanged (activeFocusItem) {
var smartSearchBar = window._smartSearchBar
if (activeFocusItem == null && smartSearchBar) {
smartSearchBar.hideMenu()
}
}
// -----------------------------------------------------------------------------
function lockView (info) { function lockView (info) {
window._lockedInfo = info window._lockedInfo = info
} }
...@@ -24,10 +34,12 @@ function setView (view, props) { ...@@ -24,10 +34,12 @@ function setView (view, props) {
window.setVisible(true) window.setVisible(true)
} }
collapse.setCollapsed(true) var item = mainLoader.item
item.collapse.setCollapsed(true)
updateSelectedEntry(view, props) updateSelectedEntry(view, props)
window._currentView = view window._currentView = view
contentLoader.setSource(view + '.qml', props || {}) item.contentLoader.setSource(view + '.qml', props || {})
} }
var lockedInfo = window._lockedInfo var lockedInfo = window._lockedInfo
...@@ -57,6 +69,11 @@ function manageAccounts () { ...@@ -57,6 +69,11 @@ function manageAccounts () {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
function updateSelectedEntry (view, props) { function updateSelectedEntry (view, props) {
var item = mainLoader.item
var menu = item.menu
var timeline = item.timeline
if (view === 'Home' || view === 'Contacts') { if (view === 'Home' || view === 'Contacts') {
menu.setSelectedEntry(view === 'Home' ? 0 : 1) menu.setSelectedEntry(view === 'Home' ? 0 : 1)
timeline.resetSelectedEntry() timeline.resetSelectedEntry()
......
...@@ -39,7 +39,6 @@ ApplicationWindow { ...@@ -39,7 +39,6 @@ ApplicationWindow {
maximumHeight: MainWindowStyle.toolBar.height maximumHeight: MainWindowStyle.toolBar.height
minimumHeight: MainWindowStyle.toolBar.height minimumHeight: MainWindowStyle.toolBar.height
minimumWidth: MainWindowStyle.minimumWidth minimumWidth: MainWindowStyle.minimumWidth
width: MainWindowStyle.width width: MainWindowStyle.width
title: MainWindowStyle.title title: MainWindowStyle.title
...@@ -49,12 +48,12 @@ ApplicationWindow { ...@@ -49,12 +48,12 @@ ApplicationWindow {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
menuBar: MainWindowMenuBar { menuBar: MainWindowMenuBar {
hide: !collapse.isCollapsed hide: mainLoader.item ? !mainLoader.item.collapse.isCollapsed : true
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
onActiveFocusItemChanged: activeFocusItem == null && smartSearchBar.hideMenu() onActiveFocusItemChanged: Logic.handleActiveFocusItemChanged(activeFocusItem)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
...@@ -65,160 +64,171 @@ ApplicationWindow { ...@@ -65,160 +64,171 @@ ApplicationWindow {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
ColumnLayout { Loader {
id: container id: mainLoader
active: CoreManager.linphoneCoreCreated
anchors.fill: parent anchors.fill: parent
spacing: 0
// ------------------------------------------------------------------------- sourceComponent: ColumnLayout {
// Toolbar properties. // Workaround to get these properties in `MainWindow.js`.
// ------------------------------------------------------------------------- // TODO: Find better Workaround.
readonly property alias collapse: collapse
readonly property alias contentLoader: contentLoader
readonly property alias menu: menu
readonly property alias timeline: timeline
ToolBar { spacing: 0
Layout.fillWidth: true
Layout.preferredHeight: MainWindowStyle.toolBar.height
background: MainWindowStyle.toolBar.background // -----------------------------------------------------------------------
// Toolbar properties.
// -----------------------------------------------------------------------
RowLayout { ToolBar {
anchors { Layout.fillWidth: true
fill: parent Layout.preferredHeight: MainWindowStyle.toolBar.height
leftMargin: MainWindowStyle.toolBar.leftMargin
rightMargin: MainWindowStyle.toolBar.rightMargin
}
spacing: MainWindowStyle.toolBar.spacing
Collapse { background: MainWindowStyle.toolBar.background
id: collapse
Layout.fillHeight: parent.height RowLayout {
target: window anchors {
targetHeight: MainWindowStyle.minimumHeight fill: parent
visible: Qt.platform.os !== 'linux' leftMargin: MainWindowStyle.toolBar.leftMargin
rightMargin: MainWindowStyle.toolBar.rightMargin
}
spacing: MainWindowStyle.toolBar.spacing
Component.onCompleted: setCollapsed(true) Collapse {
} id: collapse
AccountStatus { Layout.fillHeight: parent.height
id: accountStatus target: window
targetHeight: MainWindowStyle.minimumHeight
visible: Qt.platform.os !== 'linux'
Layout.fillHeight: parent.height Component.onCompleted: setCollapsed(true)
Layout.preferredWidth: MainWindowStyle.accountStatus.width }
account: AccountSettingsModel AccountStatus {
presence: PresenceStatusModel id: accountStatus
TooltipArea { Layout.fillHeight: parent.height
text: AccountSettingsModel.sipAddress Layout.preferredWidth: MainWindowStyle.accountStatus.width
}
onClicked: Logic.manageAccounts() account: AccountSettingsModel
} presence: PresenceStatusModel
Column { TooltipArea {
width: MainWindowStyle.autoAnswerStatus.width text: AccountSettingsModel.sipAddress
}
Icon { onClicked: Logic.manageAccounts()
icon: SettingsModel.autoAnswerStatus
? 'auto_answer'
: ''
iconSize: MainWindowStyle.autoAnswerStatus.iconSize
} }
Text { Column {
clip: true width: MainWindowStyle.autoAnswerStatus.width
color: MainWindowStyle.autoAnswerStatus.text.color
font.pointSize: MainWindowStyle.autoAnswerStatus.text.fontSize Icon {
text: qsTr('autoAnswerStatus') icon: SettingsModel.autoAnswerStatus
visible: SettingsModel.autoAnswerStatus ? 'auto_answer'
width: parent.width : ''
iconSize: MainWindowStyle.autoAnswerStatus.iconSize
}
Text {
clip: true
color: MainWindowStyle.autoAnswerStatus.text.color
font.pointSize: MainWindowStyle.autoAnswerStatus.text.fontSize
text: qsTr('autoAnswerStatus')
visible: SettingsModel.autoAnswerStatus
width: parent.width
}
} }
}
SmartSearchBar { SmartSearchBar {
id: smartSearchBar id: smartSearchBar
Layout.fillWidth: true Layout.fillWidth: true
entryHeight: MainWindowStyle.searchBox.entryHeight entryHeight: MainWindowStyle.searchBox.entryHeight
maxMenuHeight: MainWindowStyle.searchBox.maxHeight maxMenuHeight: MainWindowStyle.searchBox.maxHeight
placeholderText: qsTr('mainSearchBarPlaceholder') placeholderText: qsTr('mainSearchBarPlaceholder')
model: SmartSearchBarModel {} model: SmartSearchBarModel {}
onAddContact: window.setView('ContactEdit', { onAddContact: window.setView('ContactEdit', {
sipAddress: sipAddress sipAddress: sipAddress
}) })
onEntryClicked: window.setView(entry.contact ? 'ContactEdit' : 'Conversation', { onEntryClicked: window.setView(entry.contact ? 'ContactEdit' : 'Conversation', {
sipAddress: entry.sipAddress sipAddress: entry.sipAddress
}) })
onLaunchCall: CallsListModel.launchAudioCall(sipAddress) onLaunchCall: CallsListModel.launchAudioCall(sipAddress)
onLaunchChat: window.setView('Conversation', { onLaunchChat: window.setView('Conversation', {
sipAddress: sipAddress sipAddress: sipAddress
}) })
onLaunchVideoCall: CallsListModel.launchVideoCall(sipAddress) onLaunchVideoCall: CallsListModel.launchVideoCall(sipAddress)
}
} }
} }
}
// ------------------------------------------------------------------------- // -----------------------------------------------------------------------
// Content. // Content.
// ------------------------------------------------------------------------- // -----------------------------------------------------------------------
RowLayout { RowLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
spacing: 0 spacing: 0
// Main menu. // Main menu.
ColumnLayout { ColumnLayout {
Layout.maximumWidth: MainWindowStyle.menu.width Layout.maximumWidth: MainWindowStyle.menu.width
Layout.preferredWidth: MainWindowStyle.menu.width Layout.preferredWidth: MainWindowStyle.menu.width
spacing: 0 spacing: 0
Menu { Menu {
id: menu id: menu
entryHeight: MainWindowStyle.menu.entryHeight entryHeight: MainWindowStyle.menu.entryHeight
entryWidth: MainWindowStyle.menu.width entryWidth: MainWindowStyle.menu.width
entries: [{ entries: [{
entryName: qsTr('homeEntry'), entryName: qsTr('homeEntry'),
icon: 'home' icon: 'home'
}, { }, {
entryName: qsTr('contactsEntry'), entryName: qsTr('contactsEntry'),
icon: 'contact' icon: 'contact'
}] }]
onEntrySelected: !entry ? setView('Home') : setView('Contacts') onEntrySelected: !entry ? setView('Home') : setView('Contacts')
} }
// History. // History.
Timeline { Timeline {
id: timeline id: timeline
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
model: TimelineModel model: TimelineModel
onEntrySelected: setView('Conversation', { sipAddress: entry }) onEntrySelected: setView('Conversation', { sipAddress: entry })
}
} }
}
// Main content. // Main content.
Loader { Loader {
id: contentLoader id: contentLoader
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
source: 'Home.qml' source: 'Home.qml'
}
} }
} }
} }
...@@ -239,9 +249,9 @@ ApplicationWindow { ...@@ -239,9 +249,9 @@ ApplicationWindow {
flat: true flat: true
height: accountStatus.height height: MainWindowStyle.toolBar.height
width: MainWindowStyle.toolBar.leftMargin width: MainWindowStyle.toolBar.leftMargin
onClicked: CoreManager.forceRefreshRegisters() onClicked: CoreManager.linphoneCoreCreated && CoreManager.forceRefreshRegisters()
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment