Update lists of received from server dialog only after it is fully updated.
GitOrigin-RevId: c426df9e04100d04557429e8eb3fb14ba897cf7a
This commit is contained in:
parent
4472fa4530
commit
7fe9f1ca62
@ -10863,6 +10863,7 @@ void MessagesManager::init() {
|
|||||||
list->pinned_dialog_id_orders_.emplace(dialog_id, order);
|
list->pinned_dialog_id_orders_.emplace(dialog_id, order);
|
||||||
}
|
}
|
||||||
std::reverse(list->pinned_dialogs_.begin(), list->pinned_dialogs_.end());
|
std::reverse(list->pinned_dialogs_.begin(), list->pinned_dialogs_.end());
|
||||||
|
list->are_pinned_dialogs_inited_ = true;
|
||||||
|
|
||||||
update_list_last_pinned_dialog_date(*list);
|
update_list_last_pinned_dialog_date(*list);
|
||||||
}
|
}
|
||||||
@ -12136,6 +12137,9 @@ void MessagesManager::set_dialog_last_message_id(Dialog *d, MessageId last_messa
|
|||||||
void MessagesManager::set_dialog_first_database_message_id(Dialog *d, MessageId first_database_message_id,
|
void MessagesManager::set_dialog_first_database_message_id(Dialog *d, MessageId first_database_message_id,
|
||||||
const char *source) {
|
const char *source) {
|
||||||
CHECK(!first_database_message_id.is_scheduled());
|
CHECK(!first_database_message_id.is_scheduled());
|
||||||
|
if (first_database_message_id == d->first_database_message_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LOG(INFO) << "Set " << d->dialog_id << " first database message to " << first_database_message_id << " from "
|
LOG(INFO) << "Set " << d->dialog_id << " first database message to " << first_database_message_id << " from "
|
||||||
<< source;
|
<< source;
|
||||||
@ -12146,6 +12150,9 @@ void MessagesManager::set_dialog_first_database_message_id(Dialog *d, MessageId
|
|||||||
void MessagesManager::set_dialog_last_database_message_id(Dialog *d, MessageId last_database_message_id,
|
void MessagesManager::set_dialog_last_database_message_id(Dialog *d, MessageId last_database_message_id,
|
||||||
const char *source, bool is_loaded_from_database) {
|
const char *source, bool is_loaded_from_database) {
|
||||||
CHECK(!last_database_message_id.is_scheduled());
|
CHECK(!last_database_message_id.is_scheduled());
|
||||||
|
if (last_database_message_id == d->last_database_message_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LOG(INFO) << "Set " << d->dialog_id << " last database message to " << last_database_message_id << " from " << source;
|
LOG(INFO) << "Set " << d->dialog_id << " last database message to " << last_database_message_id << " from " << source;
|
||||||
d->debug_set_dialog_last_database_message_id = source;
|
d->debug_set_dialog_last_database_message_id = source;
|
||||||
@ -12339,6 +12346,9 @@ bool MessagesManager::set_dialog_is_pinned(DialogListId dialog_list_id, Dialog *
|
|||||||
if (list == nullptr) {
|
if (list == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!list->are_pinned_dialogs_inited_) {
|
||||||
|
// return false;
|
||||||
|
}
|
||||||
bool was_pinned = false;
|
bool was_pinned = false;
|
||||||
for (size_t pos = 0; pos < list->pinned_dialogs_.size(); pos++) {
|
for (size_t pos = 0; pos < list->pinned_dialogs_.size(); pos++) {
|
||||||
auto &pinned_dialog = list->pinned_dialogs_[pos];
|
auto &pinned_dialog = list->pinned_dialogs_[pos];
|
||||||
@ -12639,7 +12649,7 @@ void MessagesManager::on_get_dialogs(FolderId folder_id, vector<tl_object_ptr<te
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG(INFO) << "Receive " << dialogs.size() << " dialogs out of " << total_count << " in " << folder_id;
|
LOG(INFO) << "Receive " << dialogs.size() << " chats out of " << total_count << " in " << folder_id;
|
||||||
std::unordered_map<FullMessageId, DialogDate, FullMessageIdHash> full_message_id_to_dialog_date;
|
std::unordered_map<FullMessageId, DialogDate, FullMessageIdHash> full_message_id_to_dialog_date;
|
||||||
std::unordered_map<FullMessageId, tl_object_ptr<telegram_api::Message>, FullMessageIdHash> full_message_id_to_message;
|
std::unordered_map<FullMessageId, tl_object_ptr<telegram_api::Message>, FullMessageIdHash> full_message_id_to_message;
|
||||||
for (auto &message : messages) {
|
for (auto &message : messages) {
|
||||||
@ -12778,6 +12788,7 @@ void MessagesManager::on_get_dialogs(FolderId folder_id, vector<tl_object_ptr<te
|
|||||||
CHECK(d->dialog_id == dialog_id);
|
CHECK(d->dialog_id == dialog_id);
|
||||||
}
|
}
|
||||||
bool is_new = d->last_new_message_id == MessageId();
|
bool is_new = d->last_new_message_id == MessageId();
|
||||||
|
auto positions = get_dialog_positions(d);
|
||||||
|
|
||||||
set_dialog_folder_id(d, FolderId((dialog->flags_ & DIALOG_FLAG_HAS_FOLDER_ID) != 0 ? dialog->folder_id_ : 0));
|
set_dialog_folder_id(d, FolderId((dialog->flags_ & DIALOG_FLAG_HAS_FOLDER_ID) != 0 ? dialog->folder_id_ : 0));
|
||||||
|
|
||||||
@ -12924,7 +12935,10 @@ void MessagesManager::on_get_dialogs(FolderId folder_id, vector<tl_object_ptr<te
|
|||||||
send_update_chat_unread_mention_count(d);
|
send_update_chat_unread_mention_count(d);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
being_added_dialog_id_ = DialogId();
|
being_added_dialog_id_ = DialogId();
|
||||||
|
|
||||||
|
update_dialog_lists(d, std::move(positions), true, false, "on_get_dialogs");
|
||||||
}
|
}
|
||||||
if (from_dialog_list) {
|
if (from_dialog_list) {
|
||||||
CHECK(total_count >= 0);
|
CHECK(total_count >= 0);
|
||||||
@ -12946,7 +12960,7 @@ void MessagesManager::on_get_dialogs(FolderId folder_id, vector<tl_object_ptr<te
|
|||||||
auto pinned_dialog_ids = remove_secret_chat_dialog_ids(get_pinned_dialog_ids(DialogListId(folder_id)));
|
auto pinned_dialog_ids = remove_secret_chat_dialog_ids(get_pinned_dialog_ids(DialogListId(folder_id)));
|
||||||
std::reverse(pinned_dialog_ids.begin(), pinned_dialog_ids.end());
|
std::reverse(pinned_dialog_ids.begin(), pinned_dialog_ids.end());
|
||||||
if (pinned_dialog_ids != added_dialog_ids) {
|
if (pinned_dialog_ids != added_dialog_ids) {
|
||||||
LOG(INFO) << "Repair pinned dialogs order from " << format::as_array(pinned_dialog_ids) << " to "
|
LOG(INFO) << "Update pinned chats order from " << format::as_array(pinned_dialog_ids) << " to "
|
||||||
<< format::as_array(added_dialog_ids);
|
<< format::as_array(added_dialog_ids);
|
||||||
std::unordered_set<DialogId, DialogIdHash> old_pinned_dialog_ids(pinned_dialog_ids.begin(),
|
std::unordered_set<DialogId, DialogIdHash> old_pinned_dialog_ids(pinned_dialog_ids.begin(),
|
||||||
pinned_dialog_ids.end());
|
pinned_dialog_ids.end());
|
||||||
@ -13593,7 +13607,7 @@ bool MessagesManager::have_dialog(DialogId dialog_id) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MessagesManager::load_dialogs(vector<DialogId> dialog_ids, Promise<Unit> &&promise) {
|
void MessagesManager::load_dialogs(vector<DialogId> dialog_ids, Promise<Unit> &&promise) {
|
||||||
LOG(INFO) << "Load dialogs " << format::as_array(dialog_ids);
|
LOG(INFO) << "Load chats " << format::as_array(dialog_ids);
|
||||||
|
|
||||||
Dependencies dependencies;
|
Dependencies dependencies;
|
||||||
for (auto dialog_id : dialog_ids) {
|
for (auto dialog_id : dialog_ids) {
|
||||||
@ -13830,6 +13844,12 @@ vector<DialogId> MessagesManager::get_dialogs(DialogListId dialog_list_id, Dialo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!list.are_pinned_dialogs_inited_) {
|
||||||
|
// reload_pinned_dialogs(dialog_list_id, std::move(promise));
|
||||||
|
// return result;
|
||||||
|
}
|
||||||
|
|
||||||
bool need_reload_pinned_dialogs = false;
|
bool need_reload_pinned_dialogs = false;
|
||||||
if (!list.pinned_dialogs_.empty() && offset < list.pinned_dialogs_.back() && limit > 0) {
|
if (!list.pinned_dialogs_.empty() && offset < list.pinned_dialogs_.back() && limit > 0) {
|
||||||
for (auto &pinned_dialog : list.pinned_dialogs_) {
|
for (auto &pinned_dialog : list.pinned_dialogs_) {
|
||||||
@ -13951,7 +13971,7 @@ void MessagesManager::load_folder_dialog_list(FolderId folder_id, int32 limit, b
|
|||||||
load_folder_dialog_list_from_database(folder_id, limit, multipromise.get_promise());
|
load_folder_dialog_list_from_database(folder_id, limit, multipromise.get_promise());
|
||||||
is_query_sent = true;
|
is_query_sent = true;
|
||||||
} else {
|
} else {
|
||||||
LOG(INFO) << "Get dialogs from " << folder.last_server_dialog_date_;
|
LOG(INFO) << "Get chats from " << folder.last_server_dialog_date_;
|
||||||
reload_pinned_dialogs(DialogListId(folder_id), multipromise.get_promise());
|
reload_pinned_dialogs(DialogListId(folder_id), multipromise.get_promise());
|
||||||
if (folder.folder_last_dialog_date_ == folder.last_server_dialog_date_) {
|
if (folder.folder_last_dialog_date_ == folder.last_server_dialog_date_) {
|
||||||
send_closure(
|
send_closure(
|
||||||
@ -14097,7 +14117,7 @@ vector<DialogId> MessagesManager::get_pinned_dialog_ids(DialogListId dialog_list
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto *list = get_dialog_list(dialog_list_id);
|
auto *list = get_dialog_list(dialog_list_id);
|
||||||
if (list == nullptr) {
|
if (list == nullptr || !list->are_pinned_dialogs_inited_) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return transform(list->pinned_dialogs_, [](auto &pinned_dialog) { return pinned_dialog.get_dialog_id(); });
|
return transform(list->pinned_dialogs_, [](auto &pinned_dialog) { return pinned_dialog.get_dialog_id(); });
|
||||||
@ -14106,7 +14126,7 @@ vector<DialogId> MessagesManager::get_pinned_dialog_ids(DialogListId dialog_list
|
|||||||
void MessagesManager::reload_pinned_dialogs(DialogListId dialog_list_id, Promise<Unit> &&promise) {
|
void MessagesManager::reload_pinned_dialogs(DialogListId dialog_list_id, Promise<Unit> &&promise) {
|
||||||
if (td_->auth_manager_->is_bot()) {
|
if (td_->auth_manager_->is_bot()) {
|
||||||
// just in case
|
// just in case
|
||||||
return;
|
return promise.set_error(Status::Error(500, "Request aborted"));
|
||||||
}
|
}
|
||||||
if (G()->close_flag()) {
|
if (G()->close_flag()) {
|
||||||
return promise.set_error(Status::Error(500, "Request aborted"));
|
return promise.set_error(Status::Error(500, "Request aborted"));
|
||||||
@ -14117,6 +14137,7 @@ void MessagesManager::reload_pinned_dialogs(DialogListId dialog_list_id, Promise
|
|||||||
}
|
}
|
||||||
if (dialog_list_id.is_filter()) {
|
if (dialog_list_id.is_filter()) {
|
||||||
schedule_dialog_filters_reload(0.0);
|
schedule_dialog_filters_reload(0.0);
|
||||||
|
dialog_filter_reload_queries_.push_back(std::move(promise));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14175,7 +14196,12 @@ void MessagesManager::on_get_dialog_filters(Result<vector<tl_object_ptr<telegram
|
|||||||
if (G()->close_flag()) {
|
if (G()->close_flag()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
auto promises = std::move(dialog_filter_reload_queries_);
|
||||||
|
dialog_filter_reload_queries_.clear();
|
||||||
if (r_filters.is_error()) {
|
if (r_filters.is_error()) {
|
||||||
|
for (auto &promise : promises) {
|
||||||
|
promise.set_error(r_filters.error().clone());
|
||||||
|
}
|
||||||
LOG(WARNING) << "Receive error " << r_filters.error() << " for GetDialogFiltersQuery";
|
LOG(WARNING) << "Receive error " << r_filters.error() << " for GetDialogFiltersQuery";
|
||||||
need_dialog_filters_reload_ = false;
|
need_dialog_filters_reload_ = false;
|
||||||
schedule_dialog_filters_reload(Random::fast(60, 5 * 60));
|
schedule_dialog_filters_reload(Random::fast(60, 5 * 60));
|
||||||
@ -14299,6 +14325,9 @@ void MessagesManager::on_get_dialog_filters(Result<vector<tl_object_ptr<telegram
|
|||||||
if (need_synchronize_dialog_filters()) {
|
if (need_synchronize_dialog_filters()) {
|
||||||
synchronize_dialog_filters();
|
synchronize_dialog_filters();
|
||||||
}
|
}
|
||||||
|
for (auto &promise : promises) {
|
||||||
|
promise.set_value(Unit());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MessagesManager::need_synchronize_dialog_filters() const {
|
bool MessagesManager::need_synchronize_dialog_filters() const {
|
||||||
@ -15688,6 +15717,7 @@ void MessagesManager::add_dialog_filter(unique_ptr<DialogFilter> dialog_filter,
|
|||||||
list.pinned_dialog_id_orders_.emplace(dialog_id, order);
|
list.pinned_dialog_id_orders_.emplace(dialog_id, order);
|
||||||
}
|
}
|
||||||
std::reverse(list.pinned_dialogs_.begin(), list.pinned_dialogs_.end());
|
std::reverse(list.pinned_dialogs_.begin(), list.pinned_dialogs_.end());
|
||||||
|
list.are_pinned_dialogs_inited_ = true;
|
||||||
|
|
||||||
update_list_last_pinned_dialog_date(list);
|
update_list_last_pinned_dialog_date(list);
|
||||||
update_list_last_dialog_date(list);
|
update_list_last_dialog_date(list);
|
||||||
@ -15741,6 +15771,7 @@ void MessagesManager::edit_dialog_filter(unique_ptr<DialogFilter> new_dialog_fil
|
|||||||
new_list.pinned_dialog_id_orders_.emplace(dialog_id, order);
|
new_list.pinned_dialog_id_orders_.emplace(dialog_id, order);
|
||||||
}
|
}
|
||||||
std::reverse(new_list.pinned_dialogs_.begin(), new_list.pinned_dialogs_.end());
|
std::reverse(new_list.pinned_dialogs_.begin(), new_list.pinned_dialogs_.end());
|
||||||
|
new_list.are_pinned_dialogs_inited_ = true;
|
||||||
|
|
||||||
update_list_last_pinned_dialog_date(new_list, true);
|
update_list_last_pinned_dialog_date(new_list, true);
|
||||||
update_list_last_dialog_date(new_list, true);
|
update_list_last_dialog_date(new_list, true);
|
||||||
@ -25638,7 +25669,7 @@ void MessagesManager::on_update_pinned_dialogs(FolderId folder_id) {
|
|||||||
// TODO logevent + delete_logevent_promise
|
// TODO logevent + delete_logevent_promise
|
||||||
|
|
||||||
auto *list = get_dialog_list(DialogListId(folder_id));
|
auto *list = get_dialog_list(DialogListId(folder_id));
|
||||||
if (list == nullptr) {
|
if (list == nullptr || !list->are_pinned_dialogs_inited_) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// preload all pinned dialogs
|
// preload all pinned dialogs
|
||||||
@ -30540,6 +30571,11 @@ void MessagesManager::update_dialog_lists(
|
|||||||
|
|
||||||
CHECK(d != nullptr);
|
CHECK(d != nullptr);
|
||||||
auto dialog_id = d->dialog_id;
|
auto dialog_id = d->dialog_id;
|
||||||
|
if (being_added_dialog_id_ == dialog_id) {
|
||||||
|
// do not try to update dialog lists, while the dialog isn't inited
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LOG(INFO) << "Update lists of " << dialog_id << " from " << source;
|
LOG(INFO) << "Update lists of " << dialog_id << " from " << source;
|
||||||
|
|
||||||
if (d->order == DEFAULT_ORDER) {
|
if (d->order == DEFAULT_ORDER) {
|
||||||
@ -30550,6 +30586,7 @@ void MessagesManager::update_dialog_lists(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (d->folder_id != FolderId::main()) {
|
if (d->folder_id != FolderId::main()) {
|
||||||
|
LOG(INFO) << "Change folder of " << dialog_id << " to " << FolderId::main();
|
||||||
d->folder_id = FolderId::main();
|
d->folder_id = FolderId::main();
|
||||||
d->is_folder_id_inited = true;
|
d->is_folder_id_inited = true;
|
||||||
on_dialog_updated(dialog_id, "update_dialog_lists");
|
on_dialog_updated(dialog_id, "update_dialog_lists");
|
||||||
@ -30678,6 +30715,9 @@ void MessagesManager::update_list_last_pinned_dialog_date(DialogList &list, bool
|
|||||||
if (list.last_pinned_dialog_date_ == MAX_DIALOG_DATE) {
|
if (list.last_pinned_dialog_date_ == MAX_DIALOG_DATE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!list.are_pinned_dialogs_inited_) {
|
||||||
|
// return;
|
||||||
|
}
|
||||||
|
|
||||||
DialogDate max_dialog_date = MIN_DIALOG_DATE;
|
DialogDate max_dialog_date = MIN_DIALOG_DATE;
|
||||||
for (auto &pinned_dialog : list.pinned_dialogs_) {
|
for (auto &pinned_dialog : list.pinned_dialogs_) {
|
||||||
@ -30691,6 +30731,8 @@ void MessagesManager::update_list_last_pinned_dialog_date(DialogList &list, bool
|
|||||||
max_dialog_date = MAX_DIALOG_DATE;
|
max_dialog_date = MAX_DIALOG_DATE;
|
||||||
}
|
}
|
||||||
if (list.last_pinned_dialog_date_ < max_dialog_date) {
|
if (list.last_pinned_dialog_date_ < max_dialog_date) {
|
||||||
|
LOG(INFO) << "Update last pinned dialog date in " << list.dialog_list_id << " from "
|
||||||
|
<< list.last_pinned_dialog_date_ << " to " << max_dialog_date;
|
||||||
list.last_pinned_dialog_date_ = max_dialog_date;
|
list.last_pinned_dialog_date_ = max_dialog_date;
|
||||||
update_list_last_dialog_date(list, only_update);
|
update_list_last_dialog_date(list, only_update);
|
||||||
}
|
}
|
||||||
@ -30709,6 +30751,8 @@ void MessagesManager::update_list_last_dialog_date(DialogList &list, bool only_u
|
|||||||
if (list.list_last_dialog_date_ != new_last_dialog_date) {
|
if (list.list_last_dialog_date_ != new_last_dialog_date) {
|
||||||
auto old_dialog_total_count = get_dialog_total_count(list);
|
auto old_dialog_total_count = get_dialog_total_count(list);
|
||||||
auto old_last_dialog_date = list.list_last_dialog_date_;
|
auto old_last_dialog_date = list.list_last_dialog_date_;
|
||||||
|
LOG(INFO) << "Update last dialog date in " << list.dialog_list_id << " from " << old_last_dialog_date << " to "
|
||||||
|
<< new_last_dialog_date;
|
||||||
LOG_CHECK(old_last_dialog_date < new_last_dialog_date)
|
LOG_CHECK(old_last_dialog_date < new_last_dialog_date)
|
||||||
<< list.dialog_list_id << " " << old_last_dialog_date << " " << list.last_pinned_dialog_date_ << " "
|
<< list.dialog_list_id << " " << old_last_dialog_date << " " << list.last_pinned_dialog_date_ << " "
|
||||||
<< get_dialog_folder(FolderId::main())->folder_last_dialog_date_ << " "
|
<< get_dialog_folder(FolderId::main())->folder_last_dialog_date_ << " "
|
||||||
|
@ -1265,6 +1265,7 @@ class MessagesManager : public Actor {
|
|||||||
|
|
||||||
std::unordered_map<DialogId, int64, DialogIdHash> pinned_dialog_id_orders_;
|
std::unordered_map<DialogId, int64, DialogIdHash> pinned_dialog_id_orders_;
|
||||||
vector<DialogDate> pinned_dialogs_;
|
vector<DialogDate> pinned_dialogs_;
|
||||||
|
bool are_pinned_dialogs_inited_ = false;
|
||||||
|
|
||||||
DialogDate last_pinned_dialog_date_ = MIN_DIALOG_DATE; // in memory
|
DialogDate last_pinned_dialog_date_ = MIN_DIALOG_DATE; // in memory
|
||||||
|
|
||||||
@ -1325,11 +1326,12 @@ class MessagesManager : public Actor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DialogListViewIterator begin() {
|
DialogListViewIterator begin() {
|
||||||
return DialogListViewIterator(messages_manager_, &dialog_list_ids_[0]);
|
return DialogListViewIterator(messages_manager_, dialog_list_ids_.empty() ? nullptr : &dialog_list_ids_[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
DialogListViewIterator end() {
|
DialogListViewIterator end() {
|
||||||
return DialogListViewIterator(messages_manager_, &dialog_list_ids_[0] + dialog_list_ids_.size());
|
return DialogListViewIterator(
|
||||||
|
messages_manager_, dialog_list_ids_.empty() ? nullptr : &dialog_list_ids_[0] + dialog_list_ids_.size());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2924,6 +2926,7 @@ class MessagesManager : public Actor {
|
|||||||
vector<unique_ptr<DialogFilter>> server_dialog_filters_;
|
vector<unique_ptr<DialogFilter>> server_dialog_filters_;
|
||||||
vector<unique_ptr<DialogFilter>> dialog_filters_;
|
vector<unique_ptr<DialogFilter>> dialog_filters_;
|
||||||
vector<RecommendedDialogFilter> recommended_dialog_filters_;
|
vector<RecommendedDialogFilter> recommended_dialog_filters_;
|
||||||
|
vector<Promise<Unit>> dialog_filter_reload_queries_;
|
||||||
|
|
||||||
std::unordered_map<DialogId, string, DialogIdHash> active_get_channel_differencies_;
|
std::unordered_map<DialogId, string, DialogIdHash> active_get_channel_differencies_;
|
||||||
std::unordered_map<DialogId, uint64, DialogIdHash> get_channel_difference_to_logevent_id_;
|
std::unordered_map<DialogId, uint64, DialogIdHash> get_channel_difference_to_logevent_id_;
|
||||||
|
Loading…
Reference in New Issue
Block a user