diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index 5c42f9145d7..dff725c9e69 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -108,6 +108,7 @@ set(viewer_SOURCE_FILES
alstreaminfo.cpp
altoolalign.cpp
alunzip.cpp
+ alupdatemanager.cpp
alviewermenu.cpp
bdanimator.cpp
bdfloaterposer.cpp
@@ -854,6 +855,7 @@ set(viewer_HEADER_FILES
alstreaminfo.h
altoolalign.h
alunzip.h
+ alupdatemanager.h
alviewermenu.h
bdanimator.h
bdfloaterposer.h
diff --git a/indra/newview/alupdatemanager.cpp b/indra/newview/alupdatemanager.cpp
new file mode 100644
index 00000000000..7b8b27e491e
--- /dev/null
+++ b/indra/newview/alupdatemanager.cpp
@@ -0,0 +1,256 @@
+/**
+ * @file alupdatemanager.h
+ * @brief Manager for updating!
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Alchemy Viewer Source Code
+ * Copyright (C) 2024, Kyler "Felix" Eastridge.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * $/LicenseInfo$
+ */
+
+#include "alupdatemanager.h"
+#include "llversioninfo.h"
+#include "llviewercontrol.h"
+#include "llcorehttputil.h"
+#include "llsdserialize.h"
+#include "llsdutil.h"
+#include "llnotificationmanager.h"
+#include "llnotifications.h"
+#include "llnotificationsutil.h"
+#include "llappviewer.h" // For gDisconnected
+#include "llweb.h"
+#include "llstartup.h"
+
+/*
+Current LLSD data template:
+
+
+
+
+*/
+
+ALUpdateManager::ALUpdateManager() :
+ mChecking(false),
+ mSupressAuto(false),
+ mFirstCheck(true),
+ mUpdateAvailable(false),
+ mUpdateVersion(""),
+ mUpdateURL(""),
+ mAddMessage("")
+{
+}
+
+ALUpdateManager::~ALUpdateManager()
+{
+}
+
+void ALUpdateManager::showNotification()
+{
+ // If we already have one, don't grief the user.
+ if (mNotification && mNotification->isActive())
+ return;
+
+ LLSD args;
+ args["CHANNEL"] = LLVersionInfo::instance().getChannel();
+ args["VERSION"] = mUpdateVersion;
+ args["MYVERSION"] = LLVersionInfo::instance().getVersion();
+ args["URL"] = mUpdateURL;
+ args["MESSAGE"] = mAddMessage;
+ if (LLStartUp::getStartupState() < STATE_LOGIN_CLEANUP)
+ mNotification = LLNotificationsUtil::add("AlchemyUpdateAlert", args);
+ else
+ mNotification = LLNotificationsUtil::add("AlchemyUpdateToast", args);
+}
+
+void ALUpdateManager::handleUpdateData(const LLSD& content)
+{
+ // Check if we have a channel in the list
+ std::string channel = LLVersionInfo::instance().getChannel();
+ if (!content.has(channel))
+ return;
+
+ // Make sure we have a build number
+ if (!content[channel].has("build"))
+ return;
+
+ // Make sure the build number is newer than the one we have
+ int build = LLVersionInfo::instance().getBuild();
+ if (content[channel]["build"].asInteger() <= build)
+ return;
+
+ //Make sure we have all the remaining fields
+ if (!content[channel].has("version"))
+ return;
+
+ if (!content[channel].has("download"))
+ return;
+
+ LL_INFOS("ALUpdateManager") << "Update detected! 🥳" << LL_ENDL;
+
+ // And now only, and only if we got this far, update the variables
+ mUpdateVersion = content[channel]["version"].asString();
+ mUpdateURL = content[channel]["download"].asString();
+
+ if (content[channel].has("message"))
+ mAddMessage = "\n\n" + content[channel]["message"].asString();
+ else
+ mAddMessage = "";
+
+ // And mark that we have a update ready!
+ mUpdateAvailable = true;
+
+ showNotification();
+}
+
+void ALUpdateManager::launchRequest()
+{
+ LLCore::HttpRequest::policy_t httpPolicy(LLCore::HttpRequest::DEFAULT_POLICY_ID);
+ LLCoreHttpUtil::HttpCoroutineAdapter::ptr_t
+ httpAdapter(new LLCoreHttpUtil::HttpCoroutineAdapter("ALUpdateManager", httpPolicy));
+ LLCore::HttpRequest::ptr_t httpRequest(new LLCore::HttpRequest);
+ LLCore::HttpOptions::ptr_t httpOpts(new LLCore::HttpOptions);
+
+ // If we change the URL
+ httpOpts->setFollowRedirects(true);
+
+ // Also try again if we fail the first time, just in case
+ httpOpts->setRetries(1);
+
+ static LLCachedControl AlchemyUpdateURL(gSavedSettings, "AlchemyUpdateURL");
+ LL_INFOS("ALUpdateManager") << "Launching update check to " << AlchemyUpdateURL() << LL_ENDL;
+ LLSD result = httpAdapter->getRawAndSuspend(httpRequest, AlchemyUpdateURL);
+
+
+ LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS];
+ LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults);
+
+ // Mark we aren't checking anymore first and reset the timer, just in case
+ // we end up bailing due to HTTP failure
+ ALUpdateManager::getInstance()->mChecking = false;
+ ALUpdateManager::getInstance()->mLastChecked.reset();
+
+ if (!status)
+ {
+ LL_WARNS("ALUpdateManager") << "HTTP status, " << status.toTerseString() <<
+ ". Update check failed." << LL_ENDL;
+ return;
+ }
+
+ const LLSD::Binary &rawBody = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary();
+ std::string body(rawBody.begin(), rawBody.end());
+ std::istringstream is(body);
+
+ LLPointer parser = new LLSDXMLParser();
+ LLSD data;
+ if(parser->parse(is, data, body.size()) == LLSDParser::PARSE_FAILURE)
+ {
+ LL_WARNS("ALUpdateManager") << "Failed to parse update info: " << ll_stream_notation_sd(result) << LL_ENDL;
+ return;
+ }
+
+ LL_INFOS("ALUpdateManager") << "Got response: " << ll_stream_notation_sd(data) << LL_ENDL;
+
+ if (data.has("channels"))
+ ALUpdateManager::getInstance()->handleUpdateData(data["channels"]);
+ //Handle anything else here
+}
+
+void ALUpdateManager::checkNow()
+{
+ // No need to check when we are disconnected
+ if(gDisconnected)
+ return;
+
+ // Bail out if we are already checking
+ if (mChecking)
+ return;
+
+ mChecking = true;
+
+ LL_INFOS("ALUpdateManager") << "Checking for update..." << LL_ENDL;
+ LLCoros::instance().launch("ALUpdateManager::LaunchRequest",
+ boost::bind(ALUpdateManager::launchRequest));
+}
+
+void ALUpdateManager::tryAutoCheck()
+{
+ if (mSupressAuto)
+ return;
+
+ // Don't check if the user has requested to disable it
+ static LLCachedControl AlchemyUpdateEnableAutoCheck(gSavedSettings, "AlchemyUpdateEnableAutoCheck");
+ if (!AlchemyUpdateEnableAutoCheck)
+ return;
+
+ static LLCachedControl AlchemyUpdateCheckFrequency(gSavedSettings, "AlchemyUpdateCheckFrequency");
+ if ((mLastChecked.getElapsedTimeF32() < AlchemyUpdateCheckFrequency || AlchemyUpdateCheckFrequency == .0f)
+ && !mFirstCheck)
+ return;
+
+ mFirstCheck = false;
+
+ LL_INFOS("ALUpdateManager") << "Attempting automatic update check..." << LL_ENDL;
+ checkNow();
+}
+
+static bool AlchemyUpdateToastlert(const LLSD& notification, const LLSD& response)
+{
+ std::string option = LLNotification::getSelectedOptionName(response);
+
+ if (option == "DOWNLOAD")
+ {
+ LLWeb::loadURLExternal(ALUpdateManager::getInstance()->mUpdateURL);
+ }
+ else if (option == "CLOSE")
+ {
+ // This is the remind me later option
+ // We don't really do anything here, instead we just wait for the next
+ // tryAutoCheck.
+ }
+ else if (option == "SUPPRESS")
+ {
+ ALUpdateManager::getInstance()->mSupressAuto = true;
+ }
+ return false;
+}
+static LLNotificationFunctorRegistration AlchemyUpdateAlert_reg("AlchemyUpdateAlert", AlchemyUpdateToastlert);
+static LLNotificationFunctorRegistration AlchemyUpdateToast_reg("AlchemyUpdateToast", AlchemyUpdateToastlert);
diff --git a/indra/newview/alupdatemanager.h b/indra/newview/alupdatemanager.h
new file mode 100644
index 00000000000..e33670f875b
--- /dev/null
+++ b/indra/newview/alupdatemanager.h
@@ -0,0 +1,58 @@
+/**
+ * @file alupdatemanager.h
+ * @brief Manager for updating!
+ *
+ * $LicenseInfo:firstyear=2024&license=viewerlgpl$
+ * Alchemy Viewer Source Code
+ * Copyright (C) 2024, Kyler "Felix" Eastridge.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * $/LicenseInfo$
+ */
+
+#ifndef LL_ALUPDATEMANAGER_H
+#define LL_ALUPDATEMANAGER_H
+
+#include "llsingleton.h"
+#include "llnotificationptr.h"
+
+class ALUpdateManager final : public LLSingleton
+{
+ LLSINGLETON(ALUpdateManager);
+public:
+ ~ALUpdateManager();
+ void showNotification();
+ void handleUpdateData(const LLSD& content);
+ static void launchRequest();
+ void checkNow();
+ void tryAutoCheck();
+
+// Portion used for checking updates
+ bool mFirstCheck;
+ bool mSupressAuto;
+ bool mChecking;
+ LLTimer mLastChecked;
+
+// Portion used if we have a update
+ bool mUpdateAvailable;
+ bool getUpdateAvailable(){ return mUpdateAvailable; }
+ std::string mUpdateVersion;
+ std::string mUpdateURL;
+ std::string mAddMessage; // Optional message regarding the update
+ LLNotificationPtr mNotification;
+};
+
+#endif // LL_ALUPDATEMANAGER_H
diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml
index acf2676b966..4d6abbda4d6 100644
--- a/indra/newview/app_settings/settings_alchemy.xml
+++ b/indra/newview/app_settings/settings_alchemy.xml
@@ -1445,6 +1445,39 @@
Value
8.0
+ AlchemyUpdateCheckFrequency
+
+ Comment
+ How often to check for updates
+ Persist
+ 1
+ Type
+ F32
+ Value
+ 3600.0
+
+ AlchemyUpdateEnableAutoCheck
+
+ Comment
+ Enable update notifications
+ Persist
+ 1
+ Type
+ Boolean
+ Value
+ 1
+
+ AlchemyUpdateURL
+
+ Comment
+ URL to pull update checks from
+ Persist
+ 1
+ Type
+ String
+ Value
+ https://crocuta.softhyena.com/alupdate.xml
+
ResetUserColorsOnLogout
Comment
diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp
index e8138b6b44b..9449efd3fff 100644
--- a/indra/newview/llappviewer.cpp
+++ b/indra/newview/llappviewer.cpp
@@ -270,6 +270,8 @@ using namespace LL;
#include "alstreaminfo.h"
+#include "alupdatemanager.h"
+
// *FIX: These extern globals should be cleaned up.
// The globals either represent state/config/resource-storage of either
// this app, or another 'component' of the viewer. App globals should be
@@ -4645,6 +4647,9 @@ void LLAppViewer::idle()
// here.
LLIMProcessing::requestOfflineMessages();
+ // Check if it is time to do a update check
+ ALUpdateManager::getInstance()->tryAutoCheck();
+
///////////////////////////////////
//
// Special case idle if still starting up
diff --git a/indra/newview/llfloaterpreference.cpp b/indra/newview/llfloaterpreference.cpp
index 8915a159f50..4548dd91a32 100644
--- a/indra/newview/llfloaterpreference.cpp
+++ b/indra/newview/llfloaterpreference.cpp
@@ -90,6 +90,7 @@
#include "llrect.h"
#include "llstring.h"
#include "alunzip.h"
+#include "alupdatemanager.h"
// project includes
@@ -346,6 +347,7 @@ LLFloaterPreference::LLFloaterPreference(const LLSD& key)
mCommitCallbackRegistrar.add("Pref.getUIColor", boost::bind(&LLFloaterPreference::getUIColor, this ,_1, _2));
mCommitCallbackRegistrar.add("Pref.MaturitySettings", boost::bind(&LLFloaterPreference::onChangeMaturity, this));
mCommitCallbackRegistrar.add("Pref.BlockList", boost::bind(&LLFloaterPreference::onClickBlockList, this));
+ mCommitCallbackRegistrar.add("Pref.UpdateCheckNow", boost::bind(&LLFloaterPreference::onClickUpdateCheckNow, this));
mCommitCallbackRegistrar.add("Pref.Proxy", boost::bind(&LLFloaterPreference::onClickProxySettings, this));
mCommitCallbackRegistrar.add("Pref.TranslationSettings", boost::bind(&LLFloaterPreference::onClickTranslationSettings, this));
mCommitCallbackRegistrar.add("Pref.AutoReplace", boost::bind(&LLFloaterPreference::onClickAutoReplace, this));
@@ -961,6 +963,16 @@ void LLFloaterPreference::draw()
has_first_selected = (getChildRef("enabled_popups").getFirstSelected()!=NULL);
gSavedSettings.setBOOL("FirstSelectedEnabledPopups", has_first_selected);
+ getChild("AlchemyUpdateCheckNow")->setEnabled(!ALUpdateManager::getInstance()->mChecking);
+ static LLCachedControl AlchemyUpdateEnableAutoCheck(gSavedSettings, "AlchemyUpdateEnableAutoCheck");
+ getChild("AlchemyUpdateLastChecked")->setVisible(AlchemyUpdateEnableAutoCheck);
+ if (AlchemyUpdateEnableAutoCheck)
+ {
+ std::string lastCheck = LLTrans::getString("LastCheckedNSecondsAgo");
+ LLStringUtil::format(lastCheck, LLSD().with("SECONDS", static_cast(ALUpdateManager::getInstance()->mLastChecked.getElapsedTimeF32())));
+ getChild("AlchemyUpdateLastChecked")->setText(lastCheck);
+ }
+
LLFloater::draw();
}
@@ -2119,6 +2131,11 @@ void LLFloaterPreference::onClickBlockList()
// LLSD().with("people_panel_tab_name", "blocked_panel"));
}
+void LLFloaterPreference::onClickUpdateCheckNow()
+{
+ ALUpdateManager::getInstance()->checkNow();
+}
+
void LLFloaterPreference::onClickProxySettings()
{
LLFloaterReg::showInstance("prefs_proxy");
diff --git a/indra/newview/llfloaterpreference.h b/indra/newview/llfloaterpreference.h
index 248bc5002cf..7f4e98a3594 100644
--- a/indra/newview/llfloaterpreference.h
+++ b/indra/newview/llfloaterpreference.h
@@ -182,6 +182,7 @@ class LLFloaterPreference final : public LLFloater, public LLAvatarPropertiesObs
void onChangeSoundFolder();
void onChangeAnimationFolder();
void onClickBlockList();
+ void onClickUpdateCheckNow();
void onClickProxySettings();
void onClickTranslationSettings();
void onClickPermsDefault();
diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml
index eb112756f5d..3df169ee3d3 100644
--- a/indra/newview/skins/default/xui/en/notifications.xml
+++ b/indra/newview/skins/default/xui/en/notifications.xml
@@ -13207,6 +13207,51 @@ Always Run disabled.
type="notifytip">
I’m sorry Dave, I’m afraid I can’t do that
+
+
+A new version of [APP_NAME] is available for download!
+
+Latest: [CHANNEL] [VERSION]
+You have: [CHANNEL] [MYVERSION][MESSAGE]
+
+
+
+A new version of [APP_NAME] is available for download!
+
+Latest: [CHANNEL] [VERSION]
+You have: [CHANNEL] [MYVERSION][MESSAGE]
+
+
Web:
@@ -113,7 +113,7 @@
layout="topleft"
left_delta="50"
name="preferred_browser_behavior"
- top_pad="0"
+ top_pad="5"
width="480">
-
Toggling the preprocessor will not take full effect until you close and reopen this editor.