Allow setting no default account
This allows the user to select an account to use every time they launch an instance.
This commit is contained in:
		| @@ -219,6 +219,8 @@ gui/dialogs/CustomMessageBox.h | |||||||
| gui/dialogs/CustomMessageBox.cpp | gui/dialogs/CustomMessageBox.cpp | ||||||
| gui/dialogs/AccountListDialog.h | gui/dialogs/AccountListDialog.h | ||||||
| gui/dialogs/AccountListDialog.cpp | gui/dialogs/AccountListDialog.cpp | ||||||
|  | gui/dialogs/AccountSelectDialog.h | ||||||
|  | gui/dialogs/AccountSelectDialog.cpp | ||||||
|  |  | ||||||
| # GUI - widgets | # GUI - widgets | ||||||
| gui/widgets/InstanceDelegate.h | gui/widgets/InstanceDelegate.h | ||||||
| @@ -374,6 +376,7 @@ gui/dialogs/LegacyModEditDialog.ui | |||||||
| gui/dialogs/OneSixModEditDialog.ui | gui/dialogs/OneSixModEditDialog.ui | ||||||
| gui/dialogs/EditNotesDialog.ui | gui/dialogs/EditNotesDialog.ui | ||||||
| gui/dialogs/AccountListDialog.ui | gui/dialogs/AccountListDialog.ui | ||||||
|  | gui/dialogs/AccountSelectDialog.ui | ||||||
|  |  | ||||||
| # Widgets/other | # Widgets/other | ||||||
| gui/widgets/MCModInfoFrame.ui | gui/widgets/MCModInfoFrame.ui | ||||||
|   | |||||||
| @@ -58,6 +58,7 @@ | |||||||
| #include "gui/dialogs/EditNotesDialog.h" | #include "gui/dialogs/EditNotesDialog.h" | ||||||
| #include "gui/dialogs/CopyInstanceDialog.h" | #include "gui/dialogs/CopyInstanceDialog.h" | ||||||
| #include "gui/dialogs/AccountListDialog.h" | #include "gui/dialogs/AccountListDialog.h" | ||||||
|  | #include "gui/dialogs/AccountSelectDialog.h" | ||||||
|  |  | ||||||
| #include "gui/ConsoleWindow.h" | #include "gui/ConsoleWindow.h" | ||||||
|  |  | ||||||
| @@ -627,19 +628,21 @@ void MainWindow::doLogin(const QString &errorMsg) | |||||||
| 	} | 	} | ||||||
| 	else if (account.get() == nullptr) | 	else if (account.get() == nullptr) | ||||||
| 	{ | 	{ | ||||||
| 		// Tell the user they need to log in at least one account in order to play. | 		// If no default account is set, ask the user which one to use. | ||||||
| 		auto reply = CustomMessageBox::selectable(this, tr("No Account Selected"), | 		AccountSelectDialog selectDialog(tr("Which account would you like to use?"), | ||||||
| 			tr("You don't have an account selected as an active account." | 				AccountSelectDialog::GlobalDefaultCheckbox, this); | ||||||
| 				"Would you like to open the account manager to select one now?"), |  | ||||||
| 			QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)->exec(); |  | ||||||
|  |  | ||||||
| 		if (reply == QMessageBox::Yes) | 		selectDialog.exec(); | ||||||
| 		{ |  | ||||||
| 			// Open the account manager. | 		// Launch the instance with the selected account. | ||||||
| 			on_actionManageAccounts_triggered(); | 		account = selectDialog.selectedAccount(); | ||||||
| 		} |  | ||||||
|  | 		// If the user said to use the account as default, do that. | ||||||
|  | 		if (selectDialog.useAsGlobalDefault() && account.get() != nullptr) | ||||||
|  | 			accounts->setActiveAccount(account->username()); | ||||||
| 	} | 	} | ||||||
| 	else |  | ||||||
|  | 	if (account.get() != nullptr) | ||||||
| 	{ | 	{ | ||||||
| 		// We'll need to validate the access token to make sure the account is still logged in. | 		// We'll need to validate the access token to make sure the account is still logged in. | ||||||
| 		// TODO: Do that ^ | 		// TODO: Do that ^ | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ | |||||||
|  |  | ||||||
| #include <gui/dialogs/LoginDialog.h> | #include <gui/dialogs/LoginDialog.h> | ||||||
| #include <gui/dialogs/ProgressDialog.h> | #include <gui/dialogs/ProgressDialog.h> | ||||||
|  | #include <gui/dialogs/AccountSelectDialog.h> | ||||||
|  |  | ||||||
| #include <MultiMC.h> | #include <MultiMC.h> | ||||||
|  |  | ||||||
| @@ -37,6 +38,14 @@ AccountListDialog::AccountListDialog(QWidget *parent) : | |||||||
| 	m_accounts = MMC->accounts(); | 	m_accounts = MMC->accounts(); | ||||||
| 	// TODO: Make the "Active?" column show checkboxes or radio buttons. | 	// TODO: Make the "Active?" column show checkboxes or radio buttons. | ||||||
| 	ui->listView->setModel(m_accounts.get()); | 	ui->listView->setModel(m_accounts.get()); | ||||||
|  |  | ||||||
|  | 	QItemSelectionModel* selectionModel = ui->listView->selectionModel(); | ||||||
|  | 	connect(selectionModel, &QItemSelectionModel::selectionChanged,  | ||||||
|  | 			[this] (const QItemSelection& sel, const QItemSelection& dsel) { updateButtonStates(); }); | ||||||
|  | 	connect(m_accounts.get(), &MojangAccountList::listChanged, | ||||||
|  | 			[this] () { updateButtonStates(); }); | ||||||
|  |  | ||||||
|  | 	updateButtonStates(); | ||||||
| } | } | ||||||
|  |  | ||||||
| AccountListDialog::~AccountListDialog() | AccountListDialog::~AccountListDialog() | ||||||
| @@ -67,7 +76,7 @@ void AccountListDialog::on_editAccountBtn_clicked() | |||||||
| 	// TODO | 	// TODO | ||||||
| } | } | ||||||
|  |  | ||||||
| void AccountListDialog::on_setActiveBtn_clicked() | void AccountListDialog::on_setDefaultBtn_clicked() | ||||||
| { | { | ||||||
| 	QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); | 	QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); | ||||||
| 	if (selection.size() > 0) | 	if (selection.size() > 0) | ||||||
| @@ -80,11 +89,28 @@ void AccountListDialog::on_setActiveBtn_clicked() | |||||||
| 	}	 | 	}	 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void AccountListDialog::on_noDefaultBtn_clicked() | ||||||
|  | { | ||||||
|  | 	m_accounts->setActiveAccount(""); | ||||||
|  | } | ||||||
|  |  | ||||||
| void AccountListDialog::on_closeBtnBox_rejected() | void AccountListDialog::on_closeBtnBox_rejected() | ||||||
| { | { | ||||||
| 	close(); | 	close(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void AccountListDialog::updateButtonStates() | ||||||
|  | { | ||||||
|  | 	// If there is no selection, disable buttons that require something selected. | ||||||
|  | 	QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); | ||||||
|  |  | ||||||
|  | 	ui->rmAccountBtn->setEnabled(selection.size() > 0); | ||||||
|  | 	ui->editAccountBtn->setEnabled(selection.size() > 0); | ||||||
|  | 	ui->setDefaultBtn->setEnabled(selection.size() > 0); | ||||||
|  | 	 | ||||||
|  | 	ui->noDefaultBtn->setDown(m_accounts->activeAccount().get() == nullptr); | ||||||
|  | } | ||||||
|  |  | ||||||
| void AccountListDialog::doLogin(const QString& errMsg) | void AccountListDialog::doLogin(const QString& errMsg) | ||||||
| { | { | ||||||
| 	// TODO: We can use the login dialog for this for now, but we'll have to make something better for it eventually. | 	// TODO: We can use the login dialog for this for now, but we'll have to make something better for it eventually. | ||||||
|   | |||||||
| @@ -42,11 +42,16 @@ slots: | |||||||
|  |  | ||||||
| 	void on_editAccountBtn_clicked(); | 	void on_editAccountBtn_clicked(); | ||||||
|  |  | ||||||
| 	void on_setActiveBtn_clicked(); | 	void on_setDefaultBtn_clicked(); | ||||||
|  |  | ||||||
|  | 	void on_noDefaultBtn_clicked(); | ||||||
|  |  | ||||||
| 	// This will be sent when the "close" button is clicked. | 	// This will be sent when the "close" button is clicked. | ||||||
| 	void on_closeBtnBox_rejected(); | 	void on_closeBtnBox_rejected(); | ||||||
|  |  | ||||||
|  | 	//! Updates the states of the dialog's buttons. | ||||||
|  | 	void updateButtonStates(); | ||||||
|  |  | ||||||
| signals: | signals: | ||||||
| 	void activeAccountChanged(); | 	void activeAccountChanged(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -66,12 +66,22 @@ | |||||||
|         </spacer> |         </spacer> | ||||||
|        </item> |        </item> | ||||||
|        <item> |        <item> | ||||||
|         <widget class="QPushButton" name="setActiveBtn"> |         <widget class="QPushButton" name="setDefaultBtn"> | ||||||
|          <property name="toolTip"> |          <property name="toolTip"> | ||||||
|           <string><html><head/><body><p>Set the currently selected account as the active account. The active account is the account that is used to log in (unless it is overridden in an instance-specific setting).</p></body></html></string> |           <string><html><head/><body><p>Set the currently selected account as the active account. The active account is the account that is used to log in (unless it is overridden in an instance-specific setting).</p></body></html></string> | ||||||
|          </property> |          </property> | ||||||
|          <property name="text"> |          <property name="text"> | ||||||
|           <string>&Set Active</string> |           <string>&Set Default</string> | ||||||
|  |          </property> | ||||||
|  |         </widget> | ||||||
|  |        </item> | ||||||
|  |        <item> | ||||||
|  |         <widget class="QPushButton" name="noDefaultBtn"> | ||||||
|  |          <property name="toolTip"> | ||||||
|  |           <string>Set no default account. This will cause MultiMC to prompt you to select an account every time you launch an instance that doesn't have its own default set.</string> | ||||||
|  |          </property> | ||||||
|  |          <property name="text"> | ||||||
|  |           <string>&No Default</string> | ||||||
|          </property> |          </property> | ||||||
|         </widget> |         </widget> | ||||||
|        </item> |        </item> | ||||||
|   | |||||||
							
								
								
									
										89
									
								
								gui/dialogs/AccountSelectDialog.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								gui/dialogs/AccountSelectDialog.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | /* Copyright 2013 MultiMC Contributors | ||||||
|  |  * | ||||||
|  |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  * you may not use this file except in compliance with the License. | ||||||
|  |  * You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  * | ||||||
|  |  * Unless required by applicable law or agreed to in writing, software | ||||||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  * See the License for the specific language governing permissions and | ||||||
|  |  * limitations under the License. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | #include "AccountSelectDialog.h" | ||||||
|  | #include "ui_AccountSelectDialog.h" | ||||||
|  |  | ||||||
|  | #include <QItemSelectionModel> | ||||||
|  |  | ||||||
|  | #include <logger/QsLog.h> | ||||||
|  |  | ||||||
|  | #include <logic/auth/AuthenticateTask.h> | ||||||
|  |  | ||||||
|  | #include <gui/dialogs/LoginDialog.h> | ||||||
|  | #include <gui/dialogs/ProgressDialog.h> | ||||||
|  |  | ||||||
|  | #include <MultiMC.h> | ||||||
|  |  | ||||||
|  | AccountSelectDialog::AccountSelectDialog(const QString& message, int flags, QWidget *parent) : | ||||||
|  | 	QDialog(parent), | ||||||
|  | 	ui(new Ui::AccountSelectDialog) | ||||||
|  | { | ||||||
|  | 	ui->setupUi(this); | ||||||
|  |  | ||||||
|  | 	m_accounts = MMC->accounts(); | ||||||
|  | 	ui->listView->setModel(m_accounts.get()); | ||||||
|  | 	ui->listView->hideColumn(MojangAccountList::ActiveColumn); | ||||||
|  |  | ||||||
|  | 	// Set the message label. | ||||||
|  | 	ui->msgLabel->setVisible(!message.isEmpty()); | ||||||
|  | 	ui->msgLabel->setText(message); | ||||||
|  |  | ||||||
|  | 	// Flags... | ||||||
|  | 	ui->globalDefaultCheck->setVisible(flags & GlobalDefaultCheckbox); | ||||||
|  | 	ui->instDefaultCheck->setVisible(flags & InstanceDefaultCheckbox); | ||||||
|  | 	QLOG_DEBUG() << flags; | ||||||
|  |  | ||||||
|  | 	// Select the first entry in the list. | ||||||
|  | 	ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | AccountSelectDialog::~AccountSelectDialog() | ||||||
|  | { | ||||||
|  | 	delete ui; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | MojangAccountPtr AccountSelectDialog::selectedAccount() const | ||||||
|  | { | ||||||
|  | 	return m_selected; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool AccountSelectDialog::useAsGlobalDefault() const | ||||||
|  | { | ||||||
|  | 	return ui->globalDefaultCheck->isChecked(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool AccountSelectDialog::useAsInstDefaullt() const | ||||||
|  | { | ||||||
|  | 	return ui->instDefaultCheck->isChecked(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AccountSelectDialog::on_buttonBox_accepted() | ||||||
|  | { | ||||||
|  | 	QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); | ||||||
|  | 	if (selection.size() > 0) | ||||||
|  | 	{ | ||||||
|  | 		QModelIndex selected = selection.first(); | ||||||
|  | 		MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value<MojangAccountPtr>(); | ||||||
|  | 		m_selected = account; | ||||||
|  | 	} | ||||||
|  | 	close(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AccountSelectDialog::on_buttonBox_rejected() | ||||||
|  | { | ||||||
|  | 	close(); | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										90
									
								
								gui/dialogs/AccountSelectDialog.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								gui/dialogs/AccountSelectDialog.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | /* Copyright 2013 MultiMC Contributors | ||||||
|  |  * | ||||||
|  |  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
|  |  * you may not use this file except in compliance with the License. | ||||||
|  |  * You may obtain a copy of the License at | ||||||
|  |  * | ||||||
|  |  *     http://www.apache.org/licenses/LICENSE-2.0 | ||||||
|  |  * | ||||||
|  |  * Unless required by applicable law or agreed to in writing, software | ||||||
|  |  * distributed under the License is distributed on an "AS IS" BASIS, | ||||||
|  |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
|  |  * See the License for the specific language governing permissions and | ||||||
|  |  * limitations under the License. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <QDialog> | ||||||
|  |  | ||||||
|  | #include <memory> | ||||||
|  |  | ||||||
|  | #include "logic/lists/MojangAccountList.h" | ||||||
|  |  | ||||||
|  | namespace Ui { | ||||||
|  | class AccountSelectDialog; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | class AccountSelectDialog : public QDialog | ||||||
|  | { | ||||||
|  | Q_OBJECT | ||||||
|  | public: | ||||||
|  | 	enum Flags | ||||||
|  | 	{ | ||||||
|  | 		NoFlags=0, | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 		 * Shows a check box on the dialog that allows the user to specify that the account | ||||||
|  | 		 * they've selected should be used as the global default for all instances. | ||||||
|  | 		 */ | ||||||
|  | 		GlobalDefaultCheckbox, | ||||||
|  |  | ||||||
|  | 		/*! | ||||||
|  | 		 * Shows a check box on the dialog that allows the user to specify that the account | ||||||
|  | 		 * they've selected should be used as the default for the instance they are currently launching. | ||||||
|  | 		 * This is not currently implemented. | ||||||
|  | 		 */ | ||||||
|  | 		InstanceDefaultCheckbox, | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	/*! | ||||||
|  | 	 * Constructs a new account select dialog with the given parent and message. | ||||||
|  | 	 * The message will be shown at the top of the dialog. It is an empty string by default. | ||||||
|  | 	 */ | ||||||
|  | 	explicit AccountSelectDialog(const QString& message="", int flags=0, QWidget *parent = 0); | ||||||
|  | 	~AccountSelectDialog(); | ||||||
|  |  | ||||||
|  | 	/*! | ||||||
|  | 	 * Gets a pointer to the account that the user selected. | ||||||
|  | 	 * This is null if the user clicked cancel or hasn't clicked OK yet. | ||||||
|  | 	 */ | ||||||
|  | 	MojangAccountPtr selectedAccount() const; | ||||||
|  |  | ||||||
|  | 	/*! | ||||||
|  | 	 * Returns true if the user checked the "use as global default" checkbox. | ||||||
|  | 	 * If the checkbox wasn't shown, this function returns false. | ||||||
|  | 	 */ | ||||||
|  | 	bool useAsGlobalDefault() const; | ||||||
|  |  | ||||||
|  | 	/*! | ||||||
|  | 	 * Returns true if the user checked the "use as instance default" checkbox. | ||||||
|  | 	 * If the checkbox wasn't shown, this function returns false. | ||||||
|  | 	 */ | ||||||
|  | 	bool useAsInstDefaullt() const; | ||||||
|  |  | ||||||
|  | public | ||||||
|  | slots: | ||||||
|  | 	void on_buttonBox_accepted(); | ||||||
|  | 	 | ||||||
|  | 	void on_buttonBox_rejected(); | ||||||
|  |  | ||||||
|  | protected: | ||||||
|  | 	std::shared_ptr<MojangAccountList> m_accounts; | ||||||
|  |  | ||||||
|  | 	//! The account that was selected when the user clicked OK. | ||||||
|  | 	MojangAccountPtr m_selected; | ||||||
|  |  | ||||||
|  | private: | ||||||
|  | 	Ui::AccountSelectDialog *ui; | ||||||
|  | }; | ||||||
|  |  | ||||||
							
								
								
									
										56
									
								
								gui/dialogs/AccountSelectDialog.ui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								gui/dialogs/AccountSelectDialog.ui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <ui version="4.0"> | ||||||
|  |  <class>AccountSelectDialog</class> | ||||||
|  |  <widget class="QDialog" name="AccountSelectDialog"> | ||||||
|  |   <property name="geometry"> | ||||||
|  |    <rect> | ||||||
|  |     <x>0</x> | ||||||
|  |     <y>0</y> | ||||||
|  |     <width>413</width> | ||||||
|  |     <height>300</height> | ||||||
|  |    </rect> | ||||||
|  |   </property> | ||||||
|  |   <property name="windowTitle"> | ||||||
|  |    <string>Select an Account</string> | ||||||
|  |   </property> | ||||||
|  |   <layout class="QVBoxLayout" name="verticalLayout"> | ||||||
|  |    <item> | ||||||
|  |     <widget class="QLabel" name="msgLabel"> | ||||||
|  |      <property name="text"> | ||||||
|  |       <string>Select an account.</string> | ||||||
|  |      </property> | ||||||
|  |     </widget> | ||||||
|  |    </item> | ||||||
|  |    <item> | ||||||
|  |     <widget class="QTreeView" name="listView"/> | ||||||
|  |    </item> | ||||||
|  |    <item> | ||||||
|  |     <layout class="QHBoxLayout" name="horizontalLayout"> | ||||||
|  |      <item> | ||||||
|  |       <widget class="QCheckBox" name="globalDefaultCheck"> | ||||||
|  |        <property name="text"> | ||||||
|  |         <string>Use as default?</string> | ||||||
|  |        </property> | ||||||
|  |       </widget> | ||||||
|  |      </item> | ||||||
|  |      <item> | ||||||
|  |       <widget class="QCheckBox" name="instDefaultCheck"> | ||||||
|  |        <property name="text"> | ||||||
|  |         <string>Use as default for this instance only?</string> | ||||||
|  |        </property> | ||||||
|  |       </widget> | ||||||
|  |      </item> | ||||||
|  |     </layout> | ||||||
|  |    </item> | ||||||
|  |    <item> | ||||||
|  |     <widget class="QDialogButtonBox" name="buttonBox"> | ||||||
|  |      <property name="standardButtons"> | ||||||
|  |       <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> | ||||||
|  |      </property> | ||||||
|  |     </widget> | ||||||
|  |    </item> | ||||||
|  |   </layout> | ||||||
|  |  </widget> | ||||||
|  |  <resources/> | ||||||
|  |  <connections/> | ||||||
|  | </ui> | ||||||
| @@ -93,9 +93,16 @@ MojangAccountPtr MojangAccountList::activeAccount() const | |||||||
| void MojangAccountList::setActiveAccount(const QString& username) | void MojangAccountList::setActiveAccount(const QString& username) | ||||||
| { | { | ||||||
| 	beginResetModel(); | 	beginResetModel(); | ||||||
| 	for (MojangAccountPtr account : m_accounts) | 	if (username.isEmpty()) | ||||||
| 		if (account->username() == username) | 	{ | ||||||
| 			m_activeAccount = username; | 		m_activeAccount = ""; | ||||||
|  | 	} | ||||||
|  | 	else | ||||||
|  | 	{ | ||||||
|  | 		for (MojangAccountPtr account : m_accounts) | ||||||
|  | 			if (account->username() == username) | ||||||
|  | 				m_activeAccount = username; | ||||||
|  | 	} | ||||||
| 	endResetModel(); | 	endResetModel(); | ||||||
| 	onListChanged(); | 	onListChanged(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -122,6 +122,7 @@ public: | |||||||
|  |  | ||||||
| 	/*! | 	/*! | ||||||
| 	 * Sets the given account as the current active account. | 	 * Sets the given account as the current active account. | ||||||
|  | 	 * If the username given is an empty string, sets the active account to nothing. | ||||||
| 	 */ | 	 */ | ||||||
| 	virtual void setActiveAccount(const QString& username); | 	virtual void setActiveAccount(const QString& username); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user