From d296d8da0df601f55ab1aff58c6f28a7daaf978c Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 23 Oct 2026 09:48:38 -0400 Subject: Show the LindeX USD value beside the L$ balance (#42) The rate comes from the LindeX homepage feed on the main thread. The USD amount uses the same thousands separators as L$. --- indra/newview/llstatusbar.cpp | 191 ++++++++++++++++++++- indra/newview/llstatusbar.h | 16 ++ .../skins/default/xui/en/panel_status_bar.xml | 4 + 3 files changed, 209 insertions(+), 2 deletions(-) diff --git a/indra/newview/llstatusbar.cpp b/indra/newview/llstatusbar.cpp index a6c2a8dc63..dfa852bf79 100644 --- a/indra/newview/llstatusbar.cpp +++ b/indra/newview/llstatusbar.cpp @@ -85,9 +85,15 @@ #include "message.h" #include "llsearchableui.h" #include "llsearcheditor.h" +#include "bufferarray.h" +#include "httphandler.h" +#include "httpoptions.h" +#include "httprequest.h" +#include "httpresponse.h" // system includes #include +#include // @@ -116,6 +122,9 @@ LLStatusBar::LLStatusBar(const LLRect& rect) mBtnVolume(NULL), mBoxBalance(NULL), mBalance(0), + mExchangeRate(0.0), + mExchangeRateFetchInFlight(false), + mExchangeRateRequest(nullptr), mBalanceClicked(false), mObscureBalance(false), mHealth(100), @@ -136,6 +145,8 @@ LLStatusBar::LLStatusBar(const LLRect& rect) LLStatusBar::~LLStatusBar() { + delete mExchangeRateRequest; + mExchangeRateRequest = nullptr; // LLView destructor cleans up children } @@ -147,6 +158,11 @@ LLStatusBar::~LLStatusBar() void LLStatusBar::draw() { LL_PROFILE_ZONE_SCOPED_CATEGORY_UI; + // Deliver the LindeX reply on this thread. update() only invokes the handler. + if (mExchangeRateRequest && mExchangeRateFetchInFlight) + { + mExchangeRateRequest->update(0); + } refresh(); LLPanel::draw(); } @@ -202,6 +218,7 @@ bool LLStatusBar::postBuild() mBtnVolume->setImageUnselected(LLUI::getUIImage("VoiceMute_Off")); } mObscureBalance = gSavedSettings.getBOOL("ObscureBalanceInStatusBar"); + requestExchangeRate(); // Adding Net Stat Graph S32 x = getRect().getWidth() - 2; @@ -348,6 +365,15 @@ void LLStatusBar::refresh() mTextTime->setToolTip (dtStr); } + // The LindeX feed updates about every few minutes. Refresh the rate + // a few times an hour so the USD figure stays current. Retry sooner + // until the first rate arrives. + const F32 rate_refresh_delay = (mExchangeRate > 0.0) ? (15.f * 60.f) : 30.f; + if (!mExchangeRateFetchInFlight && mExchangeRateTimer.getElapsedTimeF32() > rate_refresh_delay) + { + requestExchangeRate(); + } + if (mBalanceClicked && mBalanceClickTimer.getElapsedTimeF32() > 1.f) { mBalanceClicked = false; @@ -409,6 +435,30 @@ void LLStatusBar::creditBalance(S32 credit) setBalance(getBalance() + credit); } +// Same thousands separators as L$. Cents stay two digits after the locale decimal point. +static std::string format_usd_amount(F64 usd) +{ + const bool negative = usd < 0.0; + const F64 magnitude = negative ? -usd : usd; + const S64 cents_total = (S64)ll_round(magnitude * 100.0); + const S32 dollars = (S32)(cents_total / 100); + const S32 cents = (S32)(cents_total % 100); + + LLResMgr* res = LLResMgr::getInstance(); + std::string whole = res->getMonetaryString(dollars); + char decimal = res->getMonetaryDecimalPoint(); + if (decimal == '\0') + { + decimal = '.'; + } + std::string amount = llformat("%s%c%02d", whole.c_str(), decimal, cents); + if (negative) + { + amount.insert(0, "-"); + } + return amount; +} + void LLStatusBar::setBalance(S32 balance) { if (balance > getBalance() && getBalance() != 0) @@ -417,19 +467,37 @@ void LLStatusBar::setBalance(S32 balance) } std::string money_str = LLResMgr::getInstance()->getMonetaryString( balance ); + std::string usd_str; + if (!mObscureBalance && mExchangeRate > 0.0) + { + usd_str = format_usd_amount((F64)balance / mExchangeRate); + } LLStringUtil::format_map_t string_args; if (mObscureBalance) { string_args["[AMT]"] = "****"; + string_args["[USD]"] = "****"; } else { - string_args["[AMT]"] = llformat("%s", money_str.c_str()); + string_args["[AMT]"] = money_str; + if (!usd_str.empty()) + { + string_args["[USD]"] = usd_str; + } } - std::string label_str = getString("buycurrencylabel", string_args); + const char* label_name = (!mObscureBalance && mExchangeRate > 0.0) + ? "buycurrencylabel_usd" : "buycurrencylabel"; + std::string label_str = getString(label_name, string_args); mBoxBalance->setValue(label_str); mBoxBalance->setToolTipArg(LLStringExplicit("[AMT]"), llformat("%s", money_str.c_str())); + if (!usd_str.empty()) + { + mBoxBalance->setToolTip(llformat( + "L$ %s / $USD %s\nLindeX rate: L$ %.2f per USD\nClick to refresh your L$ balance.\nDouble-click to display or hide your L$ balance.", + money_str.c_str(), usd_str.c_str(), mExchangeRate)); + } updateBalancePanelPosition(); @@ -510,6 +578,125 @@ S32 LLStatusBar::getBalance() const return mBalance; } +void LLStatusBar::setExchangeRate(F64 lindens_per_usd) +{ + if (lindens_per_usd <= 0.0) + { + return; + } + mExchangeRate = lindens_per_usd; + setBalance(mBalance); +} + +namespace +{ + +const char* const LINDEX_HOMEPAGE_URL = "http://secondlife.com/httprequest/homepage.php"; + +class ExchangeRateHandler : public LLCore::HttpHandler +{ +public: + void onCompleted(LLCore::HttpHandle handle, LLCore::HttpResponse* response) override + { + if (!gStatusBar) + { + return; + } + if (!response) + { + gStatusBar->finishExchangeRateRequest(0.0, "empty LindeX response"); + return; + } + + const LLCore::HttpStatus status = response->getStatus(); + if (!status) + { + gStatusBar->finishExchangeRateRequest(0.0, status.toString()); + return; + } + + std::string body; + LLCore::BufferArray* raw = response->getBody(); + if (raw && raw->size() > 0) + { + body.resize(raw->size()); + raw->read(0, body.data(), body.size()); + } + + std::istringstream lines(body); + std::string line; + std::string previous; + F64 rate = 0.0; + while (std::getline(lines, line)) + { + if (!line.empty() && line.back() == '\r') + { + line.pop_back(); + } + if (previous == "exchange_rate") + { + rate = std::strtod(line.c_str(), nullptr); + break; + } + previous = line; + } + if (rate > 0.0) + { + gStatusBar->finishExchangeRateRequest(rate, std::string()); + } + else + { + gStatusBar->finishExchangeRateRequest(0.0, "LindeX feed did not contain an exchange rate"); + } + } +}; + +} // namespace + +void LLStatusBar::finishExchangeRateRequest(F64 lindens_per_usd, const std::string& error) +{ + mExchangeRateFetchInFlight = false; + if (!error.empty()) + { + LL_WARNS("StatusBar") << "LindeX rate request failed: " << error << LL_ENDL; + return; + } + setExchangeRate(lindens_per_usd); +} + +void LLStatusBar::requestExchangeRate() +{ + if (mExchangeRateFetchInFlight) + { + return; + } + if (!mExchangeRateRequest) + { + mExchangeRateRequest = new LLCore::HttpRequest(); + } + + LLCore::HttpOptions::ptr_t httpOpts = std::make_shared(); + httpOpts->setFollowRedirects(true); + httpOpts->setTimeout(30); + + const LLCore::HttpHandle handle = mExchangeRateRequest->requestGet( + LLCore::HttpRequest::DEFAULT_POLICY_ID, + LINDEX_HOMEPAGE_URL, + httpOpts, + LLCore::HttpHeaders::ptr_t(), + std::make_shared()); + if (handle == LLCORE_HTTP_HANDLE_INVALID) + { + LL_WARNS("StatusBar") << "LindeX rate request was not queued: " + << mExchangeRateRequest->getStatus().toString() << LL_ENDL; + mExchangeRateTimer.reset(); + return; + } + + mExchangeRateFetchInFlight = true; + mExchangeRateTimer.reset(); +} + S32 LLStatusBar::getHealth() const { diff --git a/indra/newview/llstatusbar.h b/indra/newview/llstatusbar.h index e54ccfe736..510ad97c85 100644 --- a/indra/newview/llstatusbar.h +++ b/indra/newview/llstatusbar.h @@ -29,6 +29,8 @@ #include "llpanel.h" +#include + // "Constants" loaded from settings.xml at start time extern S32 STATUS_BAR_HEIGHT; @@ -48,6 +50,11 @@ class LLPanelNearByMedia; class LLIconCtrl; class LLSearchEditor; +namespace LLCore +{ + class HttpRequest; +} + namespace ll { namespace statusbar @@ -95,6 +102,10 @@ public: S32 getSquareMetersLeft() const; void setBalanceVisible(bool visible); + void setExchangeRate(F64 lindens_per_usd); + void requestExchangeRate(); + // Called on the main thread when the LindeX GET finishes. rate is L$ per USD. + void finishExchangeRateRequest(F64 lindens_per_usd, const std::string& error); LLPanelNearByMedia* getNearbyMediaPanel() { return mPanelNearByMedia; } @@ -143,6 +154,11 @@ private: LLFrameTimer *mFpsUpdateTimer; S32 mBalance; + F64 mExchangeRate; // L$ per 1 USD from the LindeX feed. 0 until known. + bool mExchangeRateFetchInFlight; + LLTimer mExchangeRateTimer; + // Pumped from draw(). Not a coroutine: a fiber here aborted the process. + LLCore::HttpRequest* mExchangeRateRequest; bool mBalanceClicked; bool mObscureBalance; LLTimer mBalanceClickTimer; diff --git a/indra/newview/skins/default/xui/en/panel_status_bar.xml b/indra/newview/skins/default/xui/en/panel_status_bar.xml index 6045c58e21..3e6b179d6f 100644 --- a/indra/newview/skins/default/xui/en/panel_status_bar.xml +++ b/indra/newview/skins/default/xui/en/panel_status_bar.xml @@ -35,6 +35,10 @@ L$ [AMT] + + + L$ [AMT] / $USD [USD]