From 97c87cf3e9b5e150152898e7907577c3ca3fdd86 Mon Sep 17 00:00:00 2001
From: Tobias Markmann <tm@ayena.de>
Date: Fri, 11 Nov 2016 12:28:53 +0100
Subject: Add update channel configuration dialog to the about window

If software updates are enabled the about dialog will show
the currently configured update channel and provides a link
to a dialog to change the update channel.

Test-Information:

Builds on macOS 10.12.1, unit tests pass, and dialogs behave
as expected. Did not test Sparkle updating.

Change-Id: I05d5014f0d719ba9b2146c1e599db4f7fde80558

diff --git a/SwifTools/AutoUpdater/AutoUpdater.h b/SwifTools/AutoUpdater/AutoUpdater.h
index ed53e11..274bf50 100644
--- a/SwifTools/AutoUpdater/AutoUpdater.h
+++ b/SwifTools/AutoUpdater/AutoUpdater.h
@@ -6,6 +6,8 @@
 
 #pragma once
 
+#include <string>
+
 #include <boost/signals2.hpp>
 
 namespace Swift {
@@ -13,6 +15,7 @@ namespace Swift {
         public:
             virtual ~AutoUpdater();
 
+            virtual void setAppcastFeed(const std::string& appcastFeed) = 0;
             virtual void checkForUpdates() = 0;
             virtual bool recommendRestartToUpdate() = 0;
 
diff --git a/SwifTools/AutoUpdater/SparkleAutoUpdater.h b/SwifTools/AutoUpdater/SparkleAutoUpdater.h
index 41a25f0..dd22e73 100644
--- a/SwifTools/AutoUpdater/SparkleAutoUpdater.h
+++ b/SwifTools/AutoUpdater/SparkleAutoUpdater.h
@@ -20,9 +20,10 @@ namespace Swift {
      */
     class SparkleAutoUpdater : public AutoUpdater {
         public:
-            SparkleAutoUpdater(const std::string& url);
+            SparkleAutoUpdater(const std::string& appcastFeed);
             ~SparkleAutoUpdater();
 
+            void setAppcastFeed(const std::string& appcastFeed);
             void checkForUpdates();
             bool recommendRestartToUpdate();
 
diff --git a/SwifTools/AutoUpdater/SparkleAutoUpdater.mm b/SwifTools/AutoUpdater/SparkleAutoUpdater.mm
index 6b27ba7..ed5f094 100644
--- a/SwifTools/AutoUpdater/SparkleAutoUpdater.mm
+++ b/SwifTools/AutoUpdater/SparkleAutoUpdater.mm
@@ -21,7 +21,7 @@ class SparkleAutoUpdater::Private {
         bool restartToUpdate = false;
 };
 
-SparkleAutoUpdater::SparkleAutoUpdater(const std::string& url) : d(new Private()) {
+SparkleAutoUpdater::SparkleAutoUpdater(const std::string& appcastFeed) : d(new Private()) {
     d->updater = [SUUpdater sharedUpdater];
     [d->updater retain];
 
@@ -37,7 +37,7 @@ SparkleAutoUpdater::SparkleAutoUpdater(const std::string& url) : d(new Private()
     [d->updater setUpdateCheckInterval: 86400];
     [d->updater setAutomaticallyDownloadsUpdates: true];
 
-    NSURL* nsurl = [NSURL URLWithString: std2NSString(url)];
+    NSURL* nsurl = [NSURL URLWithString: std2NSString(appcastFeed)];
     [d->updater setFeedURL: nsurl];
 }
 
@@ -45,6 +45,11 @@ SparkleAutoUpdater::~SparkleAutoUpdater() {
     [d->updater release];
 }
 
+void SparkleAutoUpdater::setAppcastFeed(const std::string& appcastFeed) {
+    NSURL* nsurl = [NSURL URLWithString: std2NSString(appcastFeed)];
+    [d->updater setFeedURL: nsurl];
+}
+
 void SparkleAutoUpdater::checkForUpdates() {
     //[d->updater resetUpdateCycle]; // This is useful for testing to force a check ot start.
     [d->updater checkForUpdatesInBackground];
diff --git a/Swift/QtUI/QtAboutWidget.cpp b/Swift/QtUI/QtAboutWidget.cpp
index d90e35a..9047525 100644
--- a/Swift/QtUI/QtAboutWidget.cpp
+++ b/Swift/QtUI/QtAboutWidget.cpp
@@ -20,10 +20,13 @@
 #include <Swiften/Base/Platform.h>
 
 #include <Swift/QtUI/QtSwiftUtil.h>
+#include <Swift/QtUI/QtUISettingConstants.h>
+#include <Swift/QtUI/QtUpdateFeedSelectionDialog.h>
+#include <Swift/QtUI/SwiftUpdateFeeds.h>
 
 namespace Swift {
 
-QtAboutWidget::QtAboutWidget() : QDialog() {
+QtAboutWidget::QtAboutWidget(SettingsProvider* settingsProvider) : QDialog(), settingsProvider_(settingsProvider) {
 #ifndef Q_OS_MAC
     setWindowTitle(QString(tr("About %1")).arg("Swift"));
 #endif
@@ -46,6 +49,19 @@ QtAboutWidget::QtAboutWidget() : QDialog() {
     versionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
     mainLayout->addWidget(versionLabel);
 
+    settingsChangedConnection_ = settingsProvider_->onSettingChanged.connect([&](const std::string& path) {
+        if (path == QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL.getKey() || path == QtUISettingConstants::ENABLE_SOFTWARE_UPDATES.getKey()) {
+            updateUpdateInfoLabel();
+        }
+    });
+
+    updateInfoLabel_ = new QLabel("", this);
+    updateInfoLabel_->setTextInteractionFlags(Qt::LinksAccessibleByKeyboard | Qt::LinksAccessibleByMouse);
+    connect(updateInfoLabel_, SIGNAL(linkActivated(const QString &)), this, SLOT(handleChangeUpdateChannelClicked()));
+    mainLayout->addWidget(updateInfoLabel_);
+
+    updateUpdateInfoLabel();
+
     if (QCoreApplication::translate("TRANSLATION_INFO", "TRANSLATION_AUTHOR") != "TRANSLATION_AUTHOR") {
         mainLayout->addWidget(new QLabel(QString("<center><font size='-1'>") + QString(tr("Using the English translation by\n%1")).arg(QCoreApplication::translate("TRANSLATION_INFO", "TRANSLATION_AUTHOR")).replace("\n", "<br/>") + "</font></center>", this));
     }
@@ -89,6 +105,11 @@ void QtAboutWidget::handleChangelogClicked() {
     openPlainTextWindow(":/ChangeLog.md");
 }
 
+void QtAboutWidget::handleChangeUpdateChannelClicked() {
+    auto feedSelectionDialog = new QtUpdateFeedSelectionDialog(settingsProvider_);
+    feedSelectionDialog->show();
+}
+
 void QtAboutWidget::openPlainTextWindow(const QString& path) {
     QTextEdit* text = new QTextEdit();
     text->setAttribute(Qt::WA_DeleteOnClose);
@@ -108,4 +129,35 @@ void QtAboutWidget::openPlainTextWindow(const QString& path) {
     }
 }
 
+void QtAboutWidget::updateUpdateInfoLabel() {
+    if (settingsProvider_->getSetting(QtUISettingConstants::ENABLE_SOFTWARE_UPDATES)) {
+        if (!settingsProvider_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL).empty()) {
+            QString updateFeedDescription;
+            auto addUpdateFeedDialogLink = false;
+            if (settingsProvider_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL) == UpdateFeeds::StableChannel) {
+                updateFeedDescription = tr("You are receiving updates from the Stable update channel.");
+                addUpdateFeedDialogLink = true;
+            }
+            else if (settingsProvider_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL) == UpdateFeeds::DevelopmentChannel) {
+                updateFeedDescription = tr("You are receiving updates from the Development update channel.");
+                addUpdateFeedDialogLink = true;
+            }
+            else if (settingsProvider_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL) == UpdateFeeds::TestingChannel) {
+                updateFeedDescription = tr("You are receiving updates from the Testing update channel.");
+                addUpdateFeedDialogLink = true;
+            }
+            auto updateFeedDialogLink = QString( addUpdateFeedDialogLink ? "<a href=\"#\">%1</a>" : "" ).arg(tr("Change the update channel."));
+            updateInfoLabel_->setText(QString("<center><font size='-1'>%1<br/>%2</font></center>").arg(updateFeedDescription, updateFeedDialogLink));
+            updateInfoLabel_->show();
+        }
+        else {
+            updateInfoLabel_->hide();
+        }
+    }
+    else {
+        updateInfoLabel_->hide();
+    }
+
+}
+
 }
diff --git a/Swift/QtUI/QtAboutWidget.h b/Swift/QtUI/QtAboutWidget.h
index c8b0356..fb54c6e 100644
--- a/Swift/QtUI/QtAboutWidget.h
+++ b/Swift/QtUI/QtAboutWidget.h
@@ -6,20 +6,33 @@
 
 #pragma once
 
+#include <boost/signals2/connection.hpp>
+
 #include <QDialog>
 
+class QLabel;
+
 namespace Swift {
+    class SettingsProvider;
+
     class QtAboutWidget : public QDialog {
             Q_OBJECT
 
         public:
-            QtAboutWidget();
+            QtAboutWidget(SettingsProvider* settings);
 
         private slots:
             void handleLicenseClicked();
             void handleChangelogClicked();
+            void handleChangeUpdateChannelClicked();
 
         private:
             void openPlainTextWindow(const QString& path);
+            void updateUpdateInfoLabel();
+
+        private:
+            SettingsProvider* settingsProvider_;
+            QLabel* updateInfoLabel_ = nullptr;
+            boost::signals2::scoped_connection settingsChangedConnection_;
     };
 }
diff --git a/Swift/QtUI/QtLoginWindow.cpp b/Swift/QtUI/QtLoginWindow.cpp
index 6e9510d..d91f694 100644
--- a/Swift/QtUI/QtLoginWindow.cpp
+++ b/Swift/QtUI/QtLoginWindow.cpp
@@ -442,7 +442,7 @@ void QtLoginWindow::handleCertficateChecked(bool checked) {
 
 void QtLoginWindow::handleAbout() {
     if (!aboutDialog_) {
-        aboutDialog_ = new QtAboutWidget();
+        aboutDialog_ = new QtAboutWidget(settings_);
         aboutDialog_->show();
     }
     else {
diff --git a/Swift/QtUI/QtSwift.cpp b/Swift/QtUI/QtSwift.cpp
index 3aff999..3f6ce8e 100644
--- a/Swift/QtUI/QtSwift.cpp
+++ b/Swift/QtUI/QtSwift.cpp
@@ -51,6 +51,7 @@
 #include <Swift/QtUI/QtSystemTray.h>
 #include <Swift/QtUI/QtUIFactory.h>
 #include <Swift/QtUI/QtUISettingConstants.h>
+#include <Swift/QtUI/SwiftUpdateFeeds.h>
 
 #if defined(SWIFTEN_PLATFORM_WINDOWS)
 #include <Swift/QtUI/WindowsNotifier.h>
@@ -84,12 +85,6 @@
 
 namespace Swift{
 
-#if defined(SWIFTEN_PLATFORM_MACOSX)
-#define SWIFT_APPCAST_URL "https://swift.im/appcast/swift-mac-dev.xml"
-#else
-#define SWIFT_APPCAST_URL ""
-#endif
-
 po::options_description QtSwift::getOptionsDescription() {
     po::options_description result("Options");
     result.add_options()
@@ -141,6 +136,22 @@ void QtSwift::loadEmoticonsFile(const QString& fileName, std::map<std::string, s
     }
 }
 
+const std::string& QtSwift::updateChannelToFeed(const std::string& channel) {
+    static const std::string invalidChannel;
+    if (channel == UpdateFeeds::StableChannel) {
+        return UpdateFeeds::StableAppcastFeed;
+    }
+    else if (channel == UpdateFeeds::TestingChannel) {
+        return UpdateFeeds::TestingAppcastFeed;
+    }
+    else if (channel == UpdateFeeds::DevelopmentChannel) {
+        return UpdateFeeds::DevelopmentAppcastFeed;
+    }
+    else {
+        return invalidChannel;
+    }
+}
+
 QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMainThreadCaller_), autoUpdater_(nullptr), idleDetector_(&idleQuerier_, networkFactories_.getTimerFactory(), 1000) {
     QCoreApplication::setApplicationName(SWIFT_APPLICATION_NAME);
     QCoreApplication::setOrganizationName(SWIFT_ORGANIZATION_NAME);
@@ -285,10 +296,18 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa
     connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit()));
 
     PlatformAutoUpdaterFactory autoUpdaterFactory;
-    if (autoUpdaterFactory.isSupported() && settingsHierachy_->getSetting(QtUISettingConstants::ENABLE_SOFTWARE_UPDATES)) {
-        autoUpdater_ = autoUpdaterFactory.createAutoUpdater(SWIFT_APPCAST_URL);
+    if (autoUpdaterFactory.isSupported() && settingsHierachy_->getSetting(QtUISettingConstants::ENABLE_SOFTWARE_UPDATES)
+        && !settingsHierachy_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL).empty()) {
+        autoUpdater_ = autoUpdaterFactory.createAutoUpdater(updateChannelToFeed(settingsHierachy_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL)));
         autoUpdater_->checkForUpdates();
         autoUpdater_->onSuggestRestartToUserToUpdate.connect(boost::bind(&QtSwift::handleRecommendRestartToInstallUpdate, this));
+
+        settingsHierachy_->onSettingChanged.connect([&](const std::string& path) {
+            if (path == QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL.getKey()) {
+                autoUpdater_->setAppcastFeed(updateChannelToFeed(settingsHierachy_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL)));
+                autoUpdater_->checkForUpdates();
+            }
+        });
     }
 }
 
@@ -328,7 +347,7 @@ void QtSwift::handleAboutToQuit() {
 }
 
 void QtSwift::handleRecommendRestartToInstallUpdate() {
-    notifier_->showMessage(Notifier::SystemMessage, Q2PSTRING(tr("Swift Update Available")), Q2PSTRING(tr("Restart Swift now or later to update to the new Swift version")), "", [](){});
+    notifier_->showMessage(Notifier::SystemMessage, Q2PSTRING(tr("Swift Update Available")), Q2PSTRING(tr("Restart Swift to update to the new Swift version.")), "", [](){});
 }
 
 }
diff --git a/Swift/QtUI/QtSwift.h b/Swift/QtUI/QtSwift.h
index 64b79b8..3ad5714 100644
--- a/Swift/QtUI/QtSwift.h
+++ b/Swift/QtUI/QtSwift.h
@@ -67,6 +67,7 @@ namespace Swift {
         private:
             XMLSettingsProvider* loadSettingsFile(const QString& fileName);
             void loadEmoticonsFile(const QString& fileName, std::map<std::string, std::string>& emoticons);
+            static const std::string& updateChannelToFeed(const std::string& channel);
 
         private:
             QtEventLoop clientMainThreadCaller_;
diff --git a/Swift/QtUI/QtUISettingConstants.cpp b/Swift/QtUI/QtUISettingConstants.cpp
index c81a234..24cc6dc 100644
--- a/Swift/QtUI/QtUISettingConstants.cpp
+++ b/Swift/QtUI/QtUISettingConstants.cpp
@@ -6,6 +6,8 @@
 
 #include <Swift/QtUI/QtUISettingConstants.h>
 
+#include <Swift/QtUI/SwiftUpdateFeeds.h>
+
 namespace Swift {
 
 const SettingsProvider::Setting<bool> QtUISettingConstants::COMPACT_ROSTER("compactRoster", false);
@@ -22,4 +24,6 @@ const SettingsProvider::Setting<std::string> QtUISettingConstants::SPELL_CHECKER
 const SettingsProvider::Setting<std::string> QtUISettingConstants::TRELLIS_GRID_SIZE("trellisGridSize", "");
 const SettingsProvider::Setting<std::string> QtUISettingConstants::TRELLIS_GRID_POSITIONS("trellisGridPositions", "");
 const SettingsProvider::Setting<bool> QtUISettingConstants::ENABLE_SOFTWARE_UPDATES("enableSoftwareUpdates", true);
+const SettingsProvider::Setting<std::string> QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL("softwareUpdateChannel", UpdateFeeds::StableChannel);
+
 }
diff --git a/Swift/QtUI/QtUISettingConstants.h b/Swift/QtUI/QtUISettingConstants.h
index 4616656..a569587 100644
--- a/Swift/QtUI/QtUISettingConstants.h
+++ b/Swift/QtUI/QtUISettingConstants.h
@@ -6,6 +6,8 @@
 
 #pragma once
 
+#include <string>
+
 #include <Swift/Controllers/Settings/SettingsProvider.h>
 
 namespace Swift {
@@ -37,10 +39,15 @@ namespace Swift {
              */
             static const SettingsProvider::Setting<std::string> TRELLIS_GRID_POSITIONS;
             /**
-             * The #ENABLE_SOFTWARE_UPDATES settings specifies, whether Swift
+             * The #ENABLE_SOFTWARE_UPDATES setting specifies, whether Swift
              * should automatically check for software updates in regular
              * intervals and install them automatically.
              */
             static const SettingsProvider::Setting<bool> ENABLE_SOFTWARE_UPDATES;
+            /**
+             * The #SOFTWARE_UPDATE_CHANNEL setting defines what update channel
+             * Swift uses to check for, and receive, updates.
+             */
+            static const SettingsProvider::Setting<std::string> SOFTWARE_UPDATE_CHANNEL;
     };
 }
diff --git a/Swift/QtUI/QtUpdateFeedSelectionDialog.cpp b/Swift/QtUI/QtUpdateFeedSelectionDialog.cpp
new file mode 100644
index 0000000..1f4058a
--- /dev/null
+++ b/Swift/QtUI/QtUpdateFeedSelectionDialog.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2016 Isode Limited.
+ * All rights reserved.
+ * See the COPYING file for more information.
+ */
+
+#include <Swift/QtUI/QtUpdateFeedSelectionDialog.h>
+
+#include <QComboBox>
+
+#include <Swift/Controllers/Settings/SettingsProvider.h>
+
+#include <Swift/QtUI/QtUISettingConstants.h>
+#include <Swift/QtUI/SwiftUpdateFeeds.h>
+
+namespace Swift {
+
+QtUpdateFeedSelectionDialog::QtUpdateFeedSelectionDialog(SettingsProvider* settingsProvider) : QDialog(), settings_(settingsProvider) {
+    ui.setupUi(this);
+
+    connect(ui.currentChannelComboBox, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [&] (int newIndex) {
+        setDescriptionForIndex(newIndex);
+    });
+
+    auto updateChannel = settings_->getSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL);
+    if (updateChannel == UpdateFeeds::StableChannel) {
+        ui.currentChannelComboBox->setCurrentIndex(0);
+    }
+    else if (updateChannel == UpdateFeeds::TestingChannel) {
+        ui.currentChannelComboBox->setCurrentIndex(1);
+    }
+    else if (updateChannel == UpdateFeeds::DevelopmentChannel) {
+        ui.currentChannelComboBox->setCurrentIndex(2);
+    }
+
+    connect(this, &QDialog::accepted, [&]() {
+        auto newUpdateChannel = std::string("");
+        switch (ui.currentChannelComboBox->currentIndex()) {
+            case 0:
+                newUpdateChannel = UpdateFeeds::StableChannel;
+                break;
+            case 1:
+                newUpdateChannel = UpdateFeeds::TestingChannel;
+                break;
+            case 2:
+                newUpdateChannel = UpdateFeeds::DevelopmentChannel;
+            break;
+        }
+        settings_->storeSetting(QtUISettingConstants::SOFTWARE_UPDATE_CHANNEL, newUpdateChannel);
+    });
+
+    setAttribute(Qt::WA_DeleteOnClose);
+}
+
+void QtUpdateFeedSelectionDialog::setDescriptionForIndex(int index) {
+    switch (index) {
+        case 0:
+            ui.stableDescriptionLabel->show();
+            ui.testingDescriptionLabel->hide();
+            ui.developmentDescriptionLabel->hide();
+            break;
+        case 1:
+            ui.stableDescriptionLabel->hide();
+            ui.testingDescriptionLabel->show();
+            ui.developmentDescriptionLabel->hide();
+            break;
+        case 2:
+            ui.stableDescriptionLabel->hide();
+            ui.testingDescriptionLabel->hide();
+            ui.developmentDescriptionLabel->show();
+            break;
+        default:
+            ui.stableDescriptionLabel->hide();
+            ui.testingDescriptionLabel->hide();
+            ui.developmentDescriptionLabel->hide();
+            break;
+    }
+    setFixedSize(sizeHint());
+}
+
+
+
+}
diff --git a/Swift/QtUI/QtUpdateFeedSelectionDialog.h b/Swift/QtUI/QtUpdateFeedSelectionDialog.h
new file mode 100644
index 0000000..80b986f
--- /dev/null
+++ b/Swift/QtUI/QtUpdateFeedSelectionDialog.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2016 Isode Limited.
+ * All rights reserved.
+ * See the COPYING file for more information.
+ */
+
+#pragma once
+
+#include <QDialog>
+
+#include <Swift/QtUI/ui_QtUpdateFeedSelectionDialog.h>
+
+namespace Swift {
+
+class SettingsProvider;
+
+class QtUpdateFeedSelectionDialog : public QDialog {
+        Q_OBJECT
+    public:
+        QtUpdateFeedSelectionDialog(SettingsProvider* settingsProvider);
+
+    private:
+        void setDescriptionForIndex(int index);
+
+    private:
+        Ui::QtUpdateFeedSelectionDialog ui;
+        SettingsProvider* settings_ = nullptr;
+};
+
+}
diff --git a/Swift/QtUI/QtUpdateFeedSelectionDialog.ui b/Swift/QtUI/QtUpdateFeedSelectionDialog.ui
new file mode 100644
index 0000000..4107f3a
--- /dev/null
+++ b/Swift/QtUI/QtUpdateFeedSelectionDialog.ui
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QtUpdateFeedSelectionDialog</class>
+ <widget class="QDialog" name="QtUpdateFeedSelectionDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>335</width>
+    <height>158</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Select Update Channel</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <widget class="QComboBox" name="currentChannelComboBox">
+     <property name="currentText">
+      <string/>
+     </property>
+     <property name="currentIndex">
+      <number>-1</number>
+     </property>
+     <item>
+      <property name="text">
+       <string>Stable Channel</string>
+      </property>
+      <property name="icon">
+       <iconset theme=":/icons/delivery-success.svg"/>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>Testing Channel</string>
+      </property>
+      <property name="icon">
+       <iconset theme=":/icons/delivery-warning.svg"/>
+      </property>
+     </item>
+     <item>
+      <property name="text">
+       <string>Development Channel</string>
+      </property>
+      <property name="icon">
+       <iconset theme=":/icons/delivery-warning.svg"/>
+      </property>
+     </item>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="stableDescriptionLabel">
+     <property name="text">
+      <string>This release channel includes our stable releases. They went throught internal QA testing and had previous RC releases to find critical bugs.</string>
+     </property>
+     <property name="textFormat">
+      <enum>Qt::PlainText</enum>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignJustify|Qt::AlignTop</set>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+     <property name="textInteractionFlags">
+      <set>Qt::NoTextInteraction</set>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="testingDescriptionLabel">
+     <property name="enabled">
+      <bool>true</bool>
+     </property>
+     <property name="text">
+      <string>This release channel includes our stable releases, beta releases and release candidates. They should be free from obvious bugs and are released for wider testing to find more obscure bugs.</string>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignJustify|Qt::AlignTop</set>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QLabel" name="developmentDescriptionLabel">
+     <property name="text">
+      <string>This release channel includes our stable releases, beta releases, release candidates and development releases. The development releases are not thoroughly tested and might contained bugs.</string>
+     </property>
+     <property name="alignment">
+      <set>Qt::AlignJustify|Qt::AlignTop</set>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>QtUpdateFeedSelectionDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>QtUpdateFeedSelectionDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript
index fd47dd4..acfe7d0 100644
--- a/Swift/QtUI/SConscript
+++ b/Swift/QtUI/SConscript
@@ -199,6 +199,7 @@ sources = [
     "QtURLValidator.cpp",
     "QtResourceHelper.cpp",
     "QtSpellCheckHighlighter.cpp",
+    "QtUpdateFeedSelectionDialog.cpp",
     "Trellis/QtDynamicGridLayout.cpp",
     "Trellis/QtDNDTabBar.cpp",
     "Trellis/QtGridSelectionDialog.cpp"
@@ -299,6 +300,7 @@ myenv.Uic4("QtConnectionSettings.ui")
 myenv.Uic4("QtHighlightEditor.ui")
 myenv.Uic4("QtBlockListEditorWindow.ui")
 myenv.Uic4("QtSpellCheckerWindow.ui")
+myenv.Uic4("QtUpdateFeedSelectionDialog.ui")
 myenv.Qrc("DefaultTheme.qrc")
 myenv.Qrc("Swift.qrc")
 
diff --git a/Swift/QtUI/SwiftUpdateFeeds.h b/Swift/QtUI/SwiftUpdateFeeds.h
new file mode 100644
index 0000000..a6476f5
--- /dev/null
+++ b/Swift/QtUI/SwiftUpdateFeeds.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2010-2016 Isode Limited.
+ * All rights reserved.
+ * See the COPYING file for more information.
+ */
+
+#pragma once
+
+#include <Swiften/Base/Platform.h>
+
+namespace Swift {
+
+namespace UpdateFeeds {
+    const std::string StableChannel = "stable";
+    const std::string TestingChannel = "testing";
+    const std::string DevelopmentChannel = "development";
+
+#if defined(SWIFTEN_PLATFORM_MACOSX)
+    const std::string StableAppcastFeed = "https://swift.im/downloads/swift-stable-appcast-mac.xml";
+    const std::string TestingAppcastFeed = "https://swift.im/downloads/swift-testing-appcast-mac.xml";
+    const std::string DevelopmentAppcastFeed = "https://swift.im/downloads/swift-development-appcast-mac.xml";
+#else
+    const std::string StableAppcastFeed = "";
+    const std::string TestingAppcastFeed = "";
+    const std::string DevelopmentAppcastFeed = "";
+#endif
+}
+
+}
-- 
cgit v0.10.2-6-g49f6