/** * @file llpanelprofileclassifieds.cpp * @brief LLPanelProfileClassifieds and related class implementations * * $LicenseInfo:firstyear=2022&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2022, Linden Research, Inc. * * 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 * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "llviewerprecompiledheaders.h" #include "llpanelprofileclassifieds.h" #include "llagent.h" #include "llavataractions.h" #include "llavatarpropertiesprocessor.h" #include "llclassifiedflags.h" #include "llcombobox.h" #include "llcommandhandler.h" // for classified HTML detail page click tracking #include "llcorehttputil.h" #include "lldispatcher.h" #include "llfloaterclassified.h" #include "llfloaterreg.h" #include "llfloatersidepanelcontainer.h" #include "llfloaterworldmap.h" #include "lliconctrl.h" #include "lllineeditor.h" #include "llnotifications.h" #include "llnotificationsutil.h" #include "llpanelavatar.h" #include "llparcel.h" #include "llregistry.h" #include "llscrollcontainer.h" #include "llstartup.h" #include "llstatusbar.h" #include "lltabcontainer.h" #include "lltexteditor.h" #include "lltexturectrl.h" #include "lltrans.h" #include "llviewergenericmessage.h" // send_generic_message #include "llviewerparcelmgr.h" #include "llviewerregion.h" #include "llviewertexture.h" #include "llviewertexture.h" //*TODO: verify this limit const S32 MAX_AVATAR_CLASSIFIEDS = 100; const S32 MINIMUM_PRICE_FOR_LISTING = 50; // L$ const S32 DEFAULT_EDIT_CLASSIFIED_SCROLL_HEIGHT = 530; //static LLPanelProfileClassified::panel_list_t LLPanelProfileClassified::sAllPanels; static LLPanelInjector t_panel_profile_classifieds("panel_profile_classifieds"); static LLPanelInjector t_panel_profile_classified("panel_profile_classified"); class LLClassifiedHandler : public LLCommandHandler, public LLAvatarPropertiesObserver { public: // throttle calls from untrusted browsers LLClassifiedHandler() : LLCommandHandler("classified", UNTRUSTED_THROTTLE) {} std::set mClassifiedIds; std::string mRequestVerb; virtual bool canHandleUntrusted( const LLSD& params, const LLSD& query_map, LLMediaCtrl* web, const std::string& nav_type) { if (params.size() < 1) { return true; // don't block, will fail later } if (nav_type == NAV_TYPE_CLICKED || nav_type == NAV_TYPE_EXTERNAL) { return true; } const std::string verb = params[0].asString(); if (verb == "create") { return false; } return true; } bool handle(const LLSD& params, const LLSD& query_map, const std::string& grid, LLMediaCtrl* web) { if (LLStartUp::getStartupState() < STATE_STARTED) { return true; } if (!LLUI::getInstance()->mSettingGroups["config"]->getBOOL("EnableClassifieds")) { LLNotificationsUtil::add("NoClassifieds", LLSD(), LLSD(), std::string("SwitchToStandardSkinAndQuit")); return true; } // handle app/classified/create urls first if (params.size() == 1 && params[0].asString() == "create") { LLAvatarActions::createClassified(); return true; } // then handle the general app/classified/{UUID}/{CMD} urls if (params.size() < 2) { return false; } // get the ID for the classified LLUUID classified_id; if (!classified_id.set(params[0], false)) { return false; } // show the classified in the side tray. // need to ask the server for more info first though... const std::string verb = params[1].asString(); if (verb == "about") { mRequestVerb = verb; mClassifiedIds.insert(classified_id); LLAvatarPropertiesProcessor::getInstance()->addObserver(LLUUID(), this); LLAvatarPropertiesProcessor::getInstance()->sendClassifiedInfoRequest(classified_id); return true; } else if (verb == "edit") { LLAvatarActions::showClassified(gAgent.getID(), classified_id, true); return true; } return false; } void openClassified(LLAvatarClassifiedInfo* c_info) { if (mRequestVerb == "about") { if (c_info->creator_id == gAgent.getID()) { LLAvatarActions::showClassified(gAgent.getID(), c_info->classified_id, false); } else { LLSD params; params["id"] = c_info->creator_id; params["classified_id"] = c_info->classified_id; params["classified_creator_id"] = c_info->creator_id; params["classified_snapshot_id"] = c_info->snapshot_id; params["classified_name"] = c_info->name; params["classified_desc"] = c_info->description; params["from_search"] = true; LLFloaterClassified* floaterp = LLFloaterReg::getTypedInstance("classified", params); if (floaterp) { floaterp->openFloater(params); floaterp->setVisibleAndFrontmost(); } } } } void processProperties(void* data, EAvatarProcessorType type) { if (APT_CLASSIFIED_INFO != type) { return; } // is this the classified that we asked for? LLAvatarClassifiedInfo* c_info = static_cast(data); if (!c_info || mClassifiedIds.find(c_info->classified_id) == mClassifiedIds.end()) { return; } // open the detail side tray for this classified openClassified(c_info); // remove our observer now that we're done mClassifiedIds.erase(c_info->classified_id); LLAvatarPropertiesProcessor::getInstance()->removeObserver(LLUUID(), this); } }; LLClassifiedHandler gClassifiedHandler; ////////////////////////////////////////////////////////////////////////// //----------------------------------------------------------------------------- // LLPanelProfileClassifieds //----------------------------------------------------------------------------- LLPanelProfileClassifieds::LLPanelProfileClassifieds() : LLPanelProfilePropertiesProcessorTab() , mClassifiedToSelectOnLoad(LLUUID::null) , mClassifiedEditOnLoad(false) , mSheduledClassifiedCreation(false) { } LLPanelProfileClassifieds::~LLPanelProfileClassifieds() { } void LLPanelProfileClassifieds::onOpen(const LLSD& key) { LLPanelProfilePropertiesProcessorTab::onOpen(key); resetData(); bool own_profile = getSelfProfile(); if (own_profile) { mNewButton->setVisible(true); mNewButton->setEnabled(false); mDeleteButton->setVisible(true); mDeleteButton->setEnabled(false); } childSetVisible("buttons_header", own_profile); } void LLPanelProfileClassifieds::selectClassified(const LLUUID& classified_id, bool edit) { if (getIsLoaded()) { for (S32 tab_idx = 0; tab_idx < mTabContainer->getTabCount(); ++tab_idx) { LLPanelProfileClassified* classified_panel = dynamic_cast(mTabContainer->getPanelByIndex(tab_idx)); if (classified_panel) { if (classified_panel->getClassifiedId() == classified_id) { mTabContainer->selectTabPanel(classified_panel); if (edit) { classified_panel->setEditMode(true); } break; } } } } else { mClassifiedToSelectOnLoad = classified_id; mClassifiedEditOnLoad = edit; } } void LLPanelProfileClassifieds::createClassified() { if (getIsLoaded()) { mNoItemsLabel->setVisible(false); LLPanelProfileClassified* classified_panel = LLPanelProfileClassified::create(); classified_panel->onOpen(LLSD()); mTabContainer->addTabPanel( LLTabContainer::TabPanelParams(). panel(classified_panel). select_tab(true). label(classified_panel->getClassifiedName())); updateButtons(); } else { mSheduledClassifiedCreation = true; } } bool LLPanelProfileClassifieds::postBuild() { mTabContainer = getChild("tab_classifieds"); mNoItemsLabel = getChild("classifieds_panel_text"); mNewButton = getChild("new_btn"); mDeleteButton = getChild("delete_btn"); mNewButton->setCommitCallback(boost::bind(&LLPanelProfileClassifieds::onClickNewBtn, this)); mDeleteButton->setCommitCallback(boost::bind(&LLPanelProfileClassifieds::onClickDelete, this)); return true; } void LLPanelProfileClassifieds::onClickNewBtn() { mNoItemsLabel->setVisible(false); LLPanelProfileClassified* classified_panel = LLPanelProfileClassified::create(); classified_panel->onOpen(LLSD()); mTabContainer->addTabPanel( LLTabContainer::TabPanelParams(). panel(classified_panel). select_tab(true). label(classified_panel->getClassifiedName())); updateButtons(); } void LLPanelProfileClassifieds::onClickDelete() { LLPanelProfileClassified* classified_panel = dynamic_cast(mTabContainer->getCurrentPanel()); if (classified_panel) { LLUUID classified_id = classified_panel->getClassifiedId(); LLSD args; args["CLASSIFIED"] = classified_panel->getClassifiedName(); LLSD payload; payload["classified_id"] = classified_id; payload["tab_idx"] = mTabContainer->getCurrentPanelIndex(); LLNotificationsUtil::add("ProfileDeleteClassified", args, payload, boost::bind(&LLPanelProfileClassifieds::callbackDeleteClassified, this, _1, _2)); } } void LLPanelProfileClassifieds::callbackDeleteClassified(const LLSD& notification, const LLSD& response) { S32 option = LLNotificationsUtil::getSelectedOption(notification, response); if (0 == option) { LLUUID classified_id = notification["payload"]["classified_id"].asUUID(); S32 tab_idx = notification["payload"]["tab_idx"].asInteger(); LLPanelProfileClassified* classified_panel = dynamic_cast(mTabContainer->getPanelByIndex(tab_idx)); if (classified_panel && classified_panel->getClassifiedId() == classified_id) { mTabContainer->removeTabPanel(classified_panel); } if (classified_id.notNull()) { LLAvatarPropertiesProcessor::getInstance()->sendClassifiedDelete(classified_id); } updateButtons(); bool no_data = !mTabContainer->getTabCount(); mNoItemsLabel->setVisible(no_data); } } void LLPanelProfileClassifieds::processProperties(void* data, EAvatarProcessorType type) { if ((APT_CLASSIFIEDS == type) || (APT_CLASSIFIED_INFO == type)) { LLUUID avatar_id = getAvatarId(); LLAvatarClassifieds* c_info = static_cast(data); if (c_info && getAvatarId() == c_info->target_id) { // do not clear classified list in case we will receive two or more data packets. // list has been cleared in updateData(). (fix for EXT-6436) LLUUID selected_id = mClassifiedToSelectOnLoad; bool has_selection = false; LLAvatarClassifieds::classifieds_list_t::const_iterator it = c_info->classifieds_list.begin(); for (; c_info->classifieds_list.end() != it; ++it) { LLAvatarClassifieds::classified_data c_data = *it; LLPanelProfileClassified* classified_panel = LLPanelProfileClassified::create(); LLSD params; params["classified_creator_id"] = avatar_id; params["classified_id"] = c_data.classified_id; params["classified_name"] = c_data.name; params["from_search"] = (selected_id == c_data.classified_id); //SLURL handling and stats tracking params["edit"] = (selected_id == c_data.classified_id) && mClassifiedEditOnLoad; classified_panel->onOpen(params); mTabContainer->addTabPanel( LLTabContainer::TabPanelParams(). panel(classified_panel). select_tab(selected_id == c_data.classified_id). label(c_data.name)); if (selected_id == c_data.classified_id) { has_selection = true; } } if (mSheduledClassifiedCreation) { LLPanelProfileClassified* classified_panel = LLPanelProfileClassified::create(); classified_panel->onOpen(LLSD()); mTabContainer->addTabPanel( LLTabContainer::TabPanelParams(). panel(classified_panel). select_tab(!has_selection). label(classified_panel->getClassifiedName())); has_selection = true; } // reset 'do on load' values mClassifiedToSelectOnLoad = LLUUID::null; mClassifiedEditOnLoad = false; mSheduledClassifiedCreation = false; // set even if not visible, user might delete own // calassified and this string will need to be shown if (getSelfProfile()) { mNoItemsLabel->setValue(LLTrans::getString("NoClassifiedsText")); } else { mNoItemsLabel->setValue(LLTrans::getString("NoAvatarClassifiedsText")); } bool has_data = mTabContainer->getTabCount() > 0; mNoItemsLabel->setVisible(!has_data); if (has_data && !has_selection) { mTabContainer->selectFirstTab(); } setLoaded(); updateButtons(); } } } void LLPanelProfileClassifieds::resetData() { resetLoading(); mTabContainer->deleteAllTabs(); } void LLPanelProfileClassifieds::updateButtons() { if (getSelfProfile()) { mNewButton->setEnabled(canAddNewClassified()); mDeleteButton->setEnabled(canDeleteClassified()); } } void LLPanelProfileClassifieds::updateData() { // Send picks request only once LLUUID avatar_id = getAvatarId(); if (!getStarted() && avatar_id.notNull()) { setIsLoading(); mNoItemsLabel->setValue(LLTrans::getString("PicksClassifiedsLoadingText")); mNoItemsLabel->setVisible(true); LLAvatarPropertiesProcessor::getInstance()->sendAvatarClassifiedsRequest(avatar_id); } } bool LLPanelProfileClassifieds::hasNewClassifieds() { for (S32 tab_idx = 0; tab_idx < mTabContainer->getTabCount(); ++tab_idx) { LLPanelProfileClassified* classified_panel = dynamic_cast(mTabContainer->getPanelByIndex(tab_idx)); if (classified_panel && classified_panel->isNew()) { return true; } } return false; } bool LLPanelProfileClassifieds::hasUnsavedChanges() { for (S32 tab_idx = 0; tab_idx < mTabContainer->getTabCount(); ++tab_idx) { LLPanelProfileClassified* classified_panel = dynamic_cast(mTabContainer->getPanelByIndex(tab_idx)); if (classified_panel && classified_panel->isDirty()) // includes 'new' { return true; } } return false; } bool LLPanelProfileClassifieds::canAddNewClassified() { return (mTabContainer->getTabCount() < MAX_AVATAR_CLASSIFIEDS); } bool LLPanelProfileClassifieds::canDeleteClassified() { return (mTabContainer->getTabCount() > 0); } void LLPanelProfileClassifieds::commitUnsavedChanges() { if (getIsLoaded()) { for (S32 tab_idx = 0; tab_idx < mTabContainer->getTabCount(); ++tab_idx) { LLPanelProfileClassified* classified_panel = dynamic_cast(mTabContainer->getPanelByIndex(tab_idx)); if (classified_panel && classified_panel->isDirty() && !classified_panel->isNew()) { classified_panel->doSave(); } } } } //----------------------------------------------------------------------------- // LLDispatchClassifiedClickThrough //----------------------------------------------------------------------------- // "classifiedclickthrough" // strings[0] = classified_id // strings[1] = teleport_clicks // strings[2] = map_clicks // strings[3] = profile_clicks class LLDispatchClassifiedClickThrough : public LLDispatchHandler { public: virtual bool operator()( const LLDispatcher* dispatcher, const std::string& key, const LLUUID& invoice, const sparam_t& strings) { if (strings.size() != 4) return false; LLUUID classified_id(strings[0]); S32 teleport_clicks = atoi(strings[1].c_str()); S32 map_clicks = atoi(strings[2].c_str()); S32 profile_clicks = atoi(strings[3].c_str()); LLPanelProfileClassified::setClickThrough( classified_id, teleport_clicks, map_clicks, profile_clicks, false); return true; } }; static LLDispatchClassifiedClickThrough sClassifiedClickThrough; //----------------------------------------------------------------------------- // LLPanelProfileClassified //----------------------------------------------------------------------------- static const S32 CB_ITEM_MATURE = 0; static const S32 CB_ITEM_PG = 1; LLPanelProfileClassified::LLPanelProfileClassified() : LLPanelProfilePropertiesProcessorTab() , mInfoLoaded(false) , mTeleportClicksOld(0) , mMapClicksOld(0) , mProfileClicksOld(0) , mTeleportClicksNew(0) , mMapClicksNew(0) , mProfileClicksNew(0) , mPriceForListing(0) , mSnapshotCtrl(NULL) , mPublishFloater(NULL) , mIsNew(false) , mIsNewWithErrors(false) , mCanClose(false) , mEditMode(false) , mEditOnLoad(false) { sAllPanels.push_back(this); } LLPanelProfileClassified::~LLPanelProfileClassified() { sAllPanels.remove(this); gGenericDispatcher.addHandler("classifiedclickthrough", NULL); // deregister our handler } //static LLPanelProfileClassified* LLPanelProfileClassified::create() { LLPanelProfileClassified* panel = new LLPanelProfileClassified(); panel->buildFromFile("panel_profile_classified.xml"); return panel; } bool LLPanelProfileClassified::postBuild() { mScrollContainer = getChild("profile_scroll"); mInfoPanel = getChild("info_panel"); mInfoScroll = getChild("info_scroll_content_panel"); mEditPanel = getChild("edit_panel"); mSnapshotCtrl = getChild("classified_snapshot"); mEditIcon = getChild("edit_icon"); //info mClassifiedNameText = getChild("classified_name"); mClassifiedDescText = getChild("classified_desc"); mLocationText = getChild("classified_location"); mCategoryText = getChild("category"); mContentTypeText = getChild("content_type"); mContentTypeM = getChild("content_type_moderate"); mContentTypeG = getChild("content_type_general"); mPriceText = getChild("price_for_listing"); mAutoRenewText = getChild("auto_renew"); mMapButton = getChild("show_on_map_btn"); mTeleportButton = getChild("teleport_btn"); mEditButton = getChild("edit_btn"); //edit mClassifiedNameEdit = getChild("classified_name_edit"); mClassifiedDescEdit = getChild("classified_desc_edit"); mLocationEdit = getChild("classified_location_edit"); mCategoryCombo = getChild("category_edit"); mContentTypeCombo = getChild("content_type_edit"); mAutoRenewEdit = getChild("auto_renew_edit"); mSaveButton = getChild("save_changes_btn"); mSetLocationButton = getChild("set_to_curr_location_btn"); mCancelButton = getChild("cancel_btn"); mUtilityBtnCnt = getChild("util_buttons_lp"); mPublishBtnsCnt = getChild("publish_layout_panel"); mCancelBtnCnt = getChild("cancel_btn_lp"); mSaveBtnCnt = getChild("save_btn_lp"); mSnapshotCtrl->setOnSelectCallback(boost::bind(&LLPanelProfileClassified::onTextureSelected, this)); mSnapshotCtrl->setMouseEnterCallback(boost::bind(&LLPanelProfileClassified::onTexturePickerMouseEnter, this)); mSnapshotCtrl->setMouseLeaveCallback(boost::bind(&LLPanelProfileClassified::onTexturePickerMouseLeave, this)); mSnapshotCtrl->setAllowLocalTexture(false); mSnapshotCtrl->setBakeTextureEnabled(false); mEditIcon->setVisible(false); mMapButton->setCommitCallback(boost::bind(&LLPanelProfileClassified::onMapClick, this)); mTeleportButton->setCommitCallback(boost::bind(&LLPanelProfileClassified::onTeleportClick, this)); mEditButton->setCommitCallback(boost::bind(&LLPanelProfileClassified::onEditClick, this)); mSaveButton->setCommitCallback(boost::bind(&LLPanelProfileClassified::onSaveClick, this)); mSetLocationButton->setCommitCallback(boost::bind(&LLPanelProfileClassified::onSetLocationClick, this)); mCancelButton->setCommitCallback(boost::bind(&LLPanelProfileClassified::onCancelClick, this)); LLClassifiedInfo::cat_map::iterator iter; for (iter = LLClassifiedInfo::sCategories.begin(); iter != LLClassifiedInfo::sCategories.end(); iter++) { mCategoryCombo->add(LLTrans::getString(iter->second)); } mClassifiedNameEdit->setKeystrokeCallback(boost::bind(&LLPanelProfileClassified::onTitleChange, this), NULL); mClassifiedDescEdit->setKeystrokeCallback(boost::bind(&LLPanelProfileClassified::onChange, this)); mCategoryCombo->setCommitCallback(boost::bind(&LLPanelProfileClassified::onChange, this)); mContentTypeCombo->setCommitCallback(boost::bind(&LLPanelProfileClassified::onChange, this)); mAutoRenewEdit->setCommitCallback(boost::bind(&LLPanelProfileClassified::onChange, this)); return true; } void LLPanelProfileClassified::onOpen(const LLSD& key) { mIsNew = key.isUndefined(); resetData(); resetControls(); scrollToTop(); // classified is not created yet bool is_new = isNew() || isNewWithErrors(); if(is_new) { LLPanelProfilePropertiesProcessorTab::setAvatarId(gAgent.getID()); setPosGlobal(gAgent.getPositionGlobal()); LLUUID snapshot_id = LLUUID::null; std::string desc; LLParcel* parcel = LLViewerParcelMgr::getInstance()->getAgentParcel(); if(parcel) { desc = parcel->getDesc(); snapshot_id = parcel->getSnapshotID(); } std::string region_name = LLTrans::getString("ClassifiedUpdateAfterPublish"); LLViewerRegion* region = gAgent.getRegion(); if (region) { region_name = region->getName(); } setClassifiedName(makeClassifiedName()); setDescription(desc); setSnapshotId(snapshot_id); setClassifiedLocation(createLocationText(getLocationNotice(), region_name, getPosGlobal())); // server will set valid parcel id setParcelId(LLUUID::null); mSaveButton->setLabelArg("[LABEL]", getString("publish_label")); setEditMode(true); enableSave(true); enableEditing(true); resetDirty(); setInfoLoaded(false); } else { LLUUID avatar_id = key["classified_creator_id"]; if(avatar_id.isNull()) { return; } LLPanelProfilePropertiesProcessorTab::setAvatarId(avatar_id); setClassifiedId(key["classified_id"]); setClassifiedName(key["classified_name"]); setFromSearch(key["from_search"]); mEditOnLoad = key["edit"]; LL_INFOS() << "Opening classified [" << getClassifiedName() << "] (" << getClassifiedId() << ")" << LL_ENDL; LLAvatarPropertiesProcessor::getInstance()->sendClassifiedInfoRequest(getClassifiedId()); gGenericDispatcher.addHandler("classifiedclickthrough", &sClassifiedClickThrough); if (gAgent.getRegion()) { // While we're at it let's get the stats from the new table if that // capability exists. std::string url = gAgent.getRegion()->getCapability("SearchStatRequest"); if (!url.empty()) { LL_INFOS() << "Classified stat request via capability" << LL_ENDL; LLSD body; LLUUID classifiedId = getClassifiedId(); body["classified_id"] = classifiedId; LLCoreHttpUtil::HttpCoroutineAdapter::callbackHttpPost(url, body, boost::bind(&LLPanelProfileClassified::handleSearchStatResponse, classifiedId, _1)); } } // Update classified click stats. // *TODO: Should we do this when opening not from search? if (!fromSearch() ) { sendClickMessage("profile"); } setInfoLoaded(false); } bool is_self = getSelfProfile(); getChildView("auto_renew_layout_panel")->setVisible(is_self); getChildView("clickthrough_layout_panel")->setVisible(is_self); updateButtons(); } void LLPanelProfileClassified::processProperties(void* data, EAvatarProcessorType type) { if (APT_CLASSIFIED_INFO != type) { return; } LLAvatarClassifiedInfo* c_info = static_cast(data); if(c_info && getClassifiedId() == c_info->classified_id) { // see LLPanelProfileClassified::sendUpdate() for notes if (mIsNewWithErrors) { // We just published it setEditMode(false); } mIsNewWithErrors = false; mIsNew = false; setClassifiedName(c_info->name); setDescription(c_info->description); setSnapshotId(c_info->snapshot_id); setParcelId(c_info->parcel_id); setPosGlobal(c_info->pos_global); setSimName(c_info->sim_name); setClassifiedLocation(createLocationText(c_info->parcel_name, c_info->sim_name, c_info->pos_global)); mCategoryText->setValue(LLClassifiedInfo::sCategories[c_info->category]); // *HACK see LLPanelProfileClassified::sendUpdate() setCategory(c_info->category - 1); bool mature = is_cf_mature(c_info->flags); setContentType(mature); bool auto_renew = is_cf_auto_renew(c_info->flags); std::string auto_renew_str = auto_renew ? getString("auto_renew_on") : getString("auto_renew_off"); mAutoRenewText->setValue(auto_renew_str); mAutoRenewEdit->setValue(auto_renew); static LLUIString price_str = getString("l$_price"); price_str.setArg("[PRICE]", llformat("%d", c_info->price_for_listing)); mPriceText->setValue(LLSD(price_str)); static std::string date_fmt = getString("date_fmt"); std::string date_str = date_fmt; LLStringUtil::format(date_str, LLSD().with("datetime", (S32) c_info->creation_date)); getChild("creation_date")->setValue(date_str); resetDirty(); setInfoLoaded(true); enableSave(false); enableEditing(true); // for just created classified - in case user opened edit panel before processProperties() callback mSaveButton->setLabelArg("[LABEL]", getString("save_label")); setLoaded(); updateButtons(); if (mEditOnLoad) { setEditMode(true); } } } void LLPanelProfileClassified::setEditMode(bool edit_mode) { mEditMode = edit_mode; mInfoPanel->setVisible(!edit_mode); mEditPanel->setVisible(edit_mode); // snapshot control is common between info and edit, // enable it only when in edit mode mSnapshotCtrl->setEnabled(edit_mode); scrollToTop(); updateButtons(); updateInfoRect(); } void LLPanelProfileClassified::updateButtons() { bool edit_mode = getEditMode(); mUtilityBtnCnt->setVisible(!edit_mode); // cancel button should either delete unpublished // classified or not be there at all mCancelBtnCnt->setVisible(edit_mode && !mIsNew); mPublishBtnsCnt->setVisible(edit_mode); mSaveBtnCnt->setVisible(edit_mode); mEditButton->setVisible(!edit_mode && getSelfProfile()); } void LLPanelProfileClassified::updateInfoRect() { if (getEditMode()) { // info_scroll_content_panel contains both info and edit panel // info panel can be very large and scroll bar will carry over. // Resize info panel to prevent scroll carry over when in edit mode. mInfoScroll->reshape(mInfoScroll->getRect().getWidth(), DEFAULT_EDIT_CLASSIFIED_SCROLL_HEIGHT, false); } else { // Adjust text height to make description scrollable. S32 new_height = mClassifiedDescText->getTextBoundingRect().getHeight(); LLRect visible_rect = mClassifiedDescText->getVisibleDocumentRect(); S32 delta_height = new_height - visible_rect.getHeight() + 5; LLRect rect = mInfoScroll->getRect(); mInfoScroll->reshape(rect.getWidth(), rect.getHeight() + delta_height, false); } } void LLPanelProfileClassified::enableEditing(bool enable) { mEditButton->setEnabled(enable); mClassifiedNameEdit->setEnabled(enable); mClassifiedDescEdit->setEnabled(enable); mSetLocationButton->setEnabled(enable); mCategoryCombo->setEnabled(enable); mContentTypeCombo->setEnabled(enable); mAutoRenewEdit->setEnabled(enable); } void LLPanelProfileClassified::resetControls() { updateButtons(); mCategoryCombo->setCurrentByIndex(0); mContentTypeCombo->setCurrentByIndex(0); mAutoRenewEdit->setValue(false); mPriceForListing = MINIMUM_PRICE_FOR_LISTING; } void LLPanelProfileClassified::onEditClick() { setEditMode(true); } void LLPanelProfileClassified::onCancelClick() { if (isNew()) { mClassifiedNameEdit->setValue(mClassifiedNameText->getValue()); mClassifiedDescEdit->setValue(mClassifiedDescText->getValue()); mLocationEdit->setValue(mLocationText->getValue()); mCategoryCombo->setCurrentByIndex(0); mContentTypeCombo->setCurrentByIndex(0); mAutoRenewEdit->setValue(false); mPriceForListing = MINIMUM_PRICE_FOR_LISTING; } else { updateTabLabel(mClassifiedNameText->getValue()); // Reload data to undo changes to forms LLAvatarPropertiesProcessor::getInstance()->sendClassifiedInfoRequest(getClassifiedId()); } setInfoLoaded(false); setEditMode(false); } void LLPanelProfileClassified::onSaveClick() { mCanClose = false; if(!isValidName()) { notifyInvalidName(); return; } if(isNew() || isNewWithErrors()) { if(gStatusBar->getBalance() < MINIMUM_PRICE_FOR_LISTING) { LLNotificationsUtil::add("ClassifiedInsufficientFunds"); return; } mPublishFloater = LLFloaterReg::findTypedInstance( "publish_classified", LLSD()); if(!mPublishFloater) { mPublishFloater = LLFloaterReg::getTypedInstance( "publish_classified", LLSD()); mPublishFloater->setPublishClickedCallback(boost::bind (&LLPanelProfileClassified::onPublishFloaterPublishClicked, this)); } // set spinner value before it has focus or value wont be set mPublishFloater->setPrice(getPriceForListing()); mPublishFloater->openFloater(mPublishFloater->getKey()); mPublishFloater->center(); } else { doSave(); } } /*static*/ void LLPanelProfileClassified::handleSearchStatResponse(LLUUID classifiedId, LLSD result) { S32 teleport = result["teleport_clicks"].asInteger(); S32 map = result["map_clicks"].asInteger(); S32 profile = result["profile_clicks"].asInteger(); S32 search_teleport = result["search_teleport_clicks"].asInteger(); S32 search_map = result["search_map_clicks"].asInteger(); S32 search_profile = result["search_profile_clicks"].asInteger(); LLPanelProfileClassified::setClickThrough(classifiedId, teleport + search_teleport, map + search_map, profile + search_profile, true); } void LLPanelProfileClassified::resetData() { setClassifiedName(LLStringUtil::null); setDescription(LLStringUtil::null); setClassifiedLocation(LLStringUtil::null); setClassifiedId(LLUUID::null); setSnapshotId(LLUUID::null); setPosGlobal(LLVector3d::zero); setParcelId(LLUUID::null); setSimName(LLStringUtil::null); setFromSearch(false); // reset click stats mTeleportClicksOld = 0; mMapClicksOld = 0; mProfileClicksOld = 0; mTeleportClicksNew = 0; mMapClicksNew = 0; mProfileClicksNew = 0; mPriceForListing = MINIMUM_PRICE_FOR_LISTING; mCategoryText->setValue(LLStringUtil::null); mContentTypeText->setValue(LLStringUtil::null); getChild("click_through_text")->setValue(LLStringUtil::null); mEditButton->setValue(LLStringUtil::null); getChild("creation_date")->setValue(LLStringUtil::null); mContentTypeM->setVisible(false); mContentTypeG->setVisible(false); } void LLPanelProfileClassified::setClassifiedName(const std::string& name) { mClassifiedNameText->setValue(name); mClassifiedNameEdit->setValue(name); } std::string LLPanelProfileClassified::getClassifiedName() { return mClassifiedNameEdit->getValue().asString(); } void LLPanelProfileClassified::setDescription(const std::string& desc) { mClassifiedDescText->setValue(desc); mClassifiedDescEdit->setValue(desc); updateInfoRect(); } std::string LLPanelProfileClassified::getDescription() { return mClassifiedDescEdit->getValue().asString(); } void LLPanelProfileClassified::setClassifiedLocation(const std::string& location) { mLocationText->setValue(location); mLocationEdit->setValue(location); } std::string LLPanelProfileClassified::getClassifiedLocation() { return mLocationText->getValue().asString(); } void LLPanelProfileClassified::setSnapshotId(const LLUUID& id) { mSnapshotCtrl->setValue(id); } LLUUID LLPanelProfileClassified::getSnapshotId() { return mSnapshotCtrl->getValue().asUUID(); } // static void LLPanelProfileClassified::setClickThrough( const LLUUID& classified_id, S32 teleport, S32 map, S32 profile, bool from_new_table) { LL_INFOS() << "Click-through data for classified " << classified_id << " arrived: [" << teleport << ", " << map << ", " << profile << "] (" << (from_new_table ? "new" : "old") << ")" << LL_ENDL; for (panel_list_t::iterator iter = sAllPanels.begin(); iter != sAllPanels.end(); ++iter) { LLPanelProfileClassified* self = *iter; if (self->getClassifiedId() != classified_id) { continue; } // *HACK: Skip LLPanelProfileClassified instances: they don't display clicks data. // Those instances should not be in the list at all. if (typeid(*self) != typeid(LLPanelProfileClassified)) { continue; } LL_INFOS() << "Updating classified info panel" << LL_ENDL; // We need to check to see if the data came from the new stat_table // or the old classified table. We also need to cache the data from // the two separate sources so as to display the aggregate totals. if (from_new_table) { self->mTeleportClicksNew = teleport; self->mMapClicksNew = map; self->mProfileClicksNew = profile; } else { self->mTeleportClicksOld = teleport; self->mMapClicksOld = map; self->mProfileClicksOld = profile; } static LLUIString ct_str = self->getString("click_through_text_fmt"); ct_str.setArg("[TELEPORT]", llformat("%d", self->mTeleportClicksNew + self->mTeleportClicksOld)); ct_str.setArg("[MAP]", llformat("%d", self->mMapClicksNew + self->mMapClicksOld)); ct_str.setArg("[PROFILE]", llformat("%d", self->mProfileClicksNew + self->mProfileClicksOld)); self->getChild("click_through_text")->setValue(ct_str.getString()); // *HACK: remove this when there is enough room for click stats in the info panel self->getChildView("click_through_text")->setToolTip(ct_str.getString()); LL_INFOS() << "teleport: " << llformat("%d", self->mTeleportClicksNew + self->mTeleportClicksOld) << ", map: " << llformat("%d", self->mMapClicksNew + self->mMapClicksOld) << ", profile: " << llformat("%d", self->mProfileClicksNew + self->mProfileClicksOld) << LL_ENDL; } } // static std::string LLPanelProfileClassified::createLocationText( const std::string& original_name, const std::string& sim_name, const LLVector3d& pos_global) { std::string location_text; location_text.append(original_name); if (!sim_name.empty()) { if (!location_text.empty()) location_text.append(", "); location_text.append(sim_name); } if (!location_text.empty()) location_text.append(" "); if (!pos_global.isNull()) { S32 region_x = ll_round((F32)pos_global.mdV[VX]) % REGION_WIDTH_UNITS; S32 region_y = ll_round((F32)pos_global.mdV[VY]) % REGION_WIDTH_UNITS; S32 region_z = ll_round((F32)pos_global.mdV[VZ]); location_text.append(llformat(" (%d, %d, %d)", region_x, region_y, region_z)); } return location_text; } void LLPanelProfileClassified::scrollToTop() { if (mScrollContainer) { mScrollContainer->goToTop(); } } //info // static // *TODO: move out of the panel void LLPanelProfileClassified::sendClickMessage( const std::string& type, bool from_search, const LLUUID& classified_id, const LLUUID& parcel_id, const LLVector3d& global_pos, const std::string& sim_name) { if (gAgent.getRegion()) { // You're allowed to click on your own ads to reassure yourself // that the system is working. LLSD body; body["type"] = type; body["from_search"] = from_search; body["classified_id"] = classified_id; body["parcel_id"] = parcel_id; body["dest_pos_global"] = global_pos.getValue(); body["region_name"] = sim_name; std::string url = gAgent.getRegion()->getCapability("SearchStatTracking"); LL_INFOS() << "Sending click msg via capability (url=" << url << ")" << LL_ENDL; LL_INFOS() << "body: [" << body << "]" << LL_ENDL; LLCoreHttpUtil::HttpCoroutineAdapter::messageHttpPost(url, body, "SearchStatTracking Click report sent.", "SearchStatTracking Click report NOT sent."); } } void LLPanelProfileClassified::sendClickMessage(const std::string& type) { sendClickMessage( type, fromSearch(), getClassifiedId(), getParcelId(), getPosGlobal(), getSimName()); } void LLPanelProfileClassified::onMapClick() { sendClickMessage("map"); LLFloaterWorldMap::getInstance()->trackLocation(getPosGlobal()); LLFloaterReg::showInstance("world_map", "center"); } void LLPanelProfileClassified::onTeleportClick() { if (!getPosGlobal().isExactlyZero()) { sendClickMessage("teleport"); gAgent.teleportViaLocation(getPosGlobal()); LLFloaterWorldMap::getInstance()->trackLocation(getPosGlobal()); } } bool LLPanelProfileClassified::isDirty() const { if(mIsNew) { return true; } bool dirty = false; dirty |= mSnapshotCtrl->isDirty(); dirty |= mClassifiedNameEdit->isDirty(); dirty |= mClassifiedDescEdit->isDirty(); dirty |= mCategoryCombo->isDirty(); dirty |= mContentTypeCombo->isDirty(); dirty |= mAutoRenewEdit->isDirty(); return dirty; } void LLPanelProfileClassified::resetDirty() { mSnapshotCtrl->resetDirty(); mClassifiedNameEdit->resetDirty(); // call blockUndo() to really reset dirty(and make isDirty work as intended) mClassifiedDescEdit->blockUndo(); mClassifiedDescEdit->resetDirty(); mCategoryCombo->resetDirty(); mContentTypeCombo->resetDirty(); mAutoRenewEdit->resetDirty(); } bool LLPanelProfileClassified::canClose() { return mCanClose; } U32 LLPanelProfileClassified::getContentType() { return mContentTypeCombo->getCurrentIndex(); } void LLPanelProfileClassified::setContentType(bool mature) { static std::string mature_str = getString("type_mature"); static std::string pg_str = getString("type_pg"); mContentTypeText->setValue(mature ? mature_str : pg_str); mContentTypeM->setVisible(mature); mContentTypeG->setVisible(!mature); mContentTypeCombo->setCurrentByIndex(mature ? CB_ITEM_MATURE : CB_ITEM_PG); mContentTypeCombo->resetDirty(); } bool LLPanelProfileClassified::getAutoRenew() { return mAutoRenewEdit->getValue().asBoolean(); } void LLPanelProfileClassified::sendUpdate() { LLAvatarClassifiedInfo c_data; if(getClassifiedId().isNull()) { setClassifiedId(LLUUID::generateNewID()); } c_data.agent_id = gAgent.getID(); c_data.classified_id = getClassifiedId(); // *HACK // Categories on server start with 1 while combo-box index starts with 0 c_data.category = getCategory() + 1; c_data.name = getClassifiedName(); c_data.description = getDescription(); c_data.parcel_id = getParcelId(); c_data.snapshot_id = getSnapshotId(); c_data.pos_global = getPosGlobal(); c_data.flags = getFlags(); c_data.price_for_listing = getPriceForListing(); LLAvatarPropertiesProcessor::getInstance()->sendClassifiedInfoUpdate(&c_data); if(isNew()) { // Lets assume there will be some error. // Successful sendClassifiedInfoUpdate will trigger processProperties and // let us know there was no error. mIsNewWithErrors = true; } } U32 LLPanelProfileClassified::getCategory() { return mCategoryCombo->getCurrentIndex(); } void LLPanelProfileClassified::setCategory(U32 category) { mCategoryCombo->setCurrentByIndex(category); mCategoryCombo->resetDirty(); } U8 LLPanelProfileClassified::getFlags() { bool auto_renew = mAutoRenewEdit->getValue().asBoolean(); bool mature = mContentTypeCombo->getCurrentIndex() == CB_ITEM_MATURE; return pack_classified_flags_request(auto_renew, false, mature, false); } void LLPanelProfileClassified::enableSave(bool enable) { mSaveButton->setEnabled(enable); } std::string LLPanelProfileClassified::makeClassifiedName() { std::string name; LLParcel* parcel = LLViewerParcelMgr::getInstance()->getAgentParcel(); if(parcel) { name = parcel->getName(); } if(!name.empty()) { return name; } LLViewerRegion* region = gAgent.getRegion(); if(region) { name = region->getName(); } return name; } void LLPanelProfileClassified::onSetLocationClick() { setPosGlobal(gAgent.getPositionGlobal()); setParcelId(LLUUID::null); std::string region_name = LLTrans::getString("ClassifiedUpdateAfterPublish"); LLViewerRegion* region = gAgent.getRegion(); if (region) { region_name = region->getName(); } setClassifiedLocation(createLocationText(getLocationNotice(), region_name, getPosGlobal())); // mark classified as dirty setValue(LLSD()); onChange(); } void LLPanelProfileClassified::onChange() { enableSave(isDirty()); } void LLPanelProfileClassified::onTitleChange() { updateTabLabel(getClassifiedName()); onChange(); } void LLPanelProfileClassified::doSave() { //*TODO: Fix all of this mCanClose = true; sendUpdate(); updateTabLabel(getClassifiedName()); resetDirty(); if (!canClose()) { return; } if (!isNew() && !isNewWithErrors()) { setEditMode(false); return; } updateButtons(); } void LLPanelProfileClassified::onPublishFloaterPublishClicked() { if (mPublishFloater->getPrice() < MINIMUM_PRICE_FOR_LISTING) { LLSD args; args["MIN_PRICE"] = MINIMUM_PRICE_FOR_LISTING; LLNotificationsUtil::add("MinClassifiedPrice", args); return; } setPriceForListing(mPublishFloater->getPrice()); doSave(); } std::string LLPanelProfileClassified::getLocationNotice() { static std::string location_notice = getString("location_notice"); return location_notice; } bool LLPanelProfileClassified::isValidName() { std::string name = getClassifiedName(); if (name.empty()) { return false; } if (!isalnum(name[0])) { return false; } return true; } void LLPanelProfileClassified::notifyInvalidName() { std::string name = getClassifiedName(); if (name.empty()) { LLNotificationsUtil::add("BlankClassifiedName"); } else if (!isalnum(name[0])) { LLNotificationsUtil::add("ClassifiedMustBeAlphanumeric"); } } void LLPanelProfileClassified::onTexturePickerMouseEnter() { mEditIcon->setVisible(true); } void LLPanelProfileClassified::onTexturePickerMouseLeave() { mEditIcon->setVisible(false); } void LLPanelProfileClassified::onTextureSelected() { setSnapshotId(mSnapshotCtrl->getValue().asUUID()); onChange(); } void LLPanelProfileClassified::updateTabLabel(const std::string& title) { setLabel(title); LLTabContainer* parent = dynamic_cast(getParent()); if (parent) { parent->setCurrentTabName(title); } } //----------------------------------------------------------------------------- // LLPublishClassifiedFloater //----------------------------------------------------------------------------- LLPublishClassifiedFloater::LLPublishClassifiedFloater(const LLSD& key) : LLFloater(key) { } LLPublishClassifiedFloater::~LLPublishClassifiedFloater() { } bool LLPublishClassifiedFloater::postBuild() { LLFloater::postBuild(); childSetAction("publish_btn", boost::bind(&LLFloater::closeFloater, this, false)); childSetAction("cancel_btn", boost::bind(&LLFloater::closeFloater, this, false)); return true; } void LLPublishClassifiedFloater::setPrice(S32 price) { getChild("price_for_listing")->setValue(price); } S32 LLPublishClassifiedFloater::getPrice() { return getChild("price_for_listing")->getValue().asInteger(); } void LLPublishClassifiedFloater::setPublishClickedCallback(const commit_signal_t::slot_type& cb) { getChild("publish_btn")->setClickedCallback(cb); } void LLPublishClassifiedFloater::setCancelClickedCallback(const commit_signal_t::slot_type& cb) { getChild("cancel_btn")->setClickedCallback(cb); }