406 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			406 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| This file is part of Telegram Desktop,
 | |
| the official desktop application for the Telegram messaging service.
 | |
| 
 | |
| For license and copyright information please follow this link:
 | |
| https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 | |
| */
 | |
| #include "settings/settings_information.h"
 | |
| 
 | |
| #include "editor/photo_editor_layer_widget.h"
 | |
| #include "settings/settings_common.h"
 | |
| #include "ui/wrap/vertical_layout.h"
 | |
| #include "ui/wrap/padding_wrap.h"
 | |
| #include "ui/widgets/labels.h"
 | |
| #include "ui/widgets/buttons.h"
 | |
| #include "ui/widgets/input_fields.h"
 | |
| #include "ui/widgets/popup_menu.h"
 | |
| #include "ui/widgets/box_content_divider.h"
 | |
| #include "ui/special_buttons.h"
 | |
| #include "core/application.h"
 | |
| #include "core/core_settings.h"
 | |
| #include "chat_helpers/emoji_suggestions_widget.h"
 | |
| #include "boxes/add_contact_box.h"
 | |
| #include "ui/boxes/confirm_box.h"
 | |
| #include "boxes/change_phone_box.h"
 | |
| #include "boxes/username_box.h"
 | |
| #include "data/data_user.h"
 | |
| #include "info/profile/info_profile_values.h"
 | |
| #include "lang/lang_keys.h"
 | |
| #include "main/main_session.h"
 | |
| #include "window/window_session_controller.h"
 | |
| #include "apiwrap.h"
 | |
| #include "api/api_peer_photo.h"
 | |
| #include "core/file_utilities.h"
 | |
| #include "base/call_delayed.h"
 | |
| #include "styles/style_layers.h"
 | |
| #include "styles/style_settings.h"
 | |
| 
 | |
| #include <QtGui/QGuiApplication>
 | |
| #include <QtGui/QClipboard>
 | |
| 
 | |
| namespace Settings {
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kSaveBioTimeout = 1000;
 | |
| 
 | |
| void SetupPhoto(
 | |
| 		not_null<Ui::VerticalLayout*> container,
 | |
| 		not_null<Window::SessionController*> controller,
 | |
| 		not_null<UserData*> self) {
 | |
| 	const auto wrap = container->add(object_ptr<Ui::BoxContentDivider>(
 | |
| 		container,
 | |
| 		st::settingsInfoPhotoHeight));
 | |
| 	const auto photo = Ui::CreateChild<Ui::UserpicButton>(
 | |
| 		wrap,
 | |
| 		controller,
 | |
| 		self,
 | |
| 		Ui::UserpicButton::Role::OpenPhoto,
 | |
| 		st::settingsInfoPhoto);
 | |
| 	const auto upload = Ui::CreateChild<Ui::RoundButton>(
 | |
| 		wrap,
 | |
| 		tr::lng_settings_upload(),
 | |
| 		st::settingsInfoPhotoSet);
 | |
| 	upload->setFullRadius(true);
 | |
| 	upload->addClickHandler([=] {
 | |
| 		auto callback = [=](QImage &&image) {
 | |
| 			self->session().api().peerPhoto().upload(self, std::move(image));
 | |
| 		};
 | |
| 		Editor::PrepareProfilePhoto(
 | |
| 			upload,
 | |
| 			&controller->window(),
 | |
| 			std::move(callback));
 | |
| 	});
 | |
| 	rpl::combine(
 | |
| 		wrap->widthValue(),
 | |
| 		photo->widthValue(),
 | |
| 		upload->widthValue()
 | |
| 	) | rpl::start_with_next([=](int max, int photoWidth, int uploadWidth) {
 | |
| 		photo->moveToLeft(
 | |
| 			(max - photoWidth) / 2,
 | |
| 			st::settingsInfoPhotoTop);
 | |
| 		upload->moveToLeft(
 | |
| 			(max - uploadWidth) / 2,
 | |
| 			(st::settingsInfoPhotoTop
 | |
| 				+ photo->height()
 | |
| 				+ st::settingsInfoPhotoSkip));
 | |
| 	}, photo->lifetime());
 | |
| }
 | |
| 
 | |
| void ShowMenu(
 | |
| 		QWidget *parent,
 | |
| 		const QString ©Button,
 | |
| 		const QString &text) {
 | |
| 	const auto menu = new Ui::PopupMenu(parent);
 | |
| 
 | |
| 	menu->addAction(copyButton, [=] {
 | |
| 		QGuiApplication::clipboard()->setText(text);
 | |
| 	});
 | |
| 	menu->popup(QCursor::pos());
 | |
| }
 | |
| 
 | |
| void AddRow(
 | |
| 		not_null<Ui::VerticalLayout*> container,
 | |
| 		rpl::producer<QString> label,
 | |
| 		rpl::producer<TextWithEntities> value,
 | |
| 		const QString ©Button,
 | |
| 		Fn<void()> edit,
 | |
| 		const style::icon &icon) {
 | |
| 	const auto wrap = AddButton(
 | |
| 		container,
 | |
| 		rpl::single(QString()),
 | |
| 		st::settingsInfoRow,
 | |
| 		&icon);
 | |
| 	const auto forcopy = Ui::CreateChild<QString>(wrap.get());
 | |
| 	wrap->setAcceptBoth();
 | |
| 	wrap->clicks(
 | |
| 	) | rpl::filter([=] {
 | |
| 		return !wrap->isDisabled();
 | |
| 	}) | rpl::start_with_next([=](Qt::MouseButton button) {
 | |
| 		if (button == Qt::LeftButton) {
 | |
| 			edit();
 | |
| 		} else if (!forcopy->isEmpty()) {
 | |
| 			ShowMenu(wrap, copyButton, *forcopy);
 | |
| 		}
 | |
| 	}, wrap->lifetime());
 | |
| 
 | |
| 	auto existing = base::duplicate(
 | |
| 		value
 | |
| 	) | rpl::map([](const TextWithEntities &text) {
 | |
| 		return text.entities.isEmpty();
 | |
| 	});
 | |
| 	base::duplicate(
 | |
| 		value
 | |
| 	) | rpl::filter([](const TextWithEntities &text) {
 | |
| 		return text.entities.isEmpty();
 | |
| 	}) | rpl::start_with_next([=](const TextWithEntities &text) {
 | |
| 		*forcopy = text.text;
 | |
| 	}, wrap->lifetime());
 | |
| 	const auto text = Ui::CreateChild<Ui::FlatLabel>(
 | |
| 		wrap.get(),
 | |
| 		std::move(value),
 | |
| 		st::settingsInfoValue);
 | |
| 	text->setClickHandlerFilter([=](auto&&...) {
 | |
| 		edit();
 | |
| 		return false;
 | |
| 	});
 | |
| 	base::duplicate(
 | |
| 		existing
 | |
| 	) | rpl::start_with_next([=](bool existing) {
 | |
| 		wrap->setDisabled(!existing);
 | |
| 		text->setAttribute(Qt::WA_TransparentForMouseEvents, existing);
 | |
| 		text->setSelectable(existing);
 | |
| 		text->setDoubleClickSelectsParagraph(existing);
 | |
| 	}, text->lifetime());
 | |
| 
 | |
| 	const auto about = Ui::CreateChild<Ui::FlatLabel>(
 | |
| 		wrap.get(),
 | |
| 		std::move(label),
 | |
| 		st::settingsInfoAbout);
 | |
| 	about->setAttribute(Qt::WA_TransparentForMouseEvents);
 | |
| 
 | |
| 	const auto button = Ui::CreateChild<Ui::RpWidget>(wrap.get());
 | |
| 	button->resize(st::settingsInfoEditIconOver.size());
 | |
| 	button->setAttribute(Qt::WA_TransparentForMouseEvents);
 | |
| 	button->paintRequest(
 | |
| 	) | rpl::filter([=] {
 | |
| 		return (wrap->isOver() || wrap->isDown()) && !wrap->isDisabled();
 | |
| 	}) | rpl::start_with_next([=](QRect clip) {
 | |
| 		Painter p(button);
 | |
| 		st::settingsInfoEditIconOver.paint(p, QPoint(), button->width());
 | |
| 	}, button->lifetime());
 | |
| 
 | |
| 	wrap->sizeValue(
 | |
| 	) | rpl::start_with_next([=](QSize size) {
 | |
| 		const auto width = size.width();
 | |
| 		text->resizeToWidth(width
 | |
| 			- st::settingsInfoValuePosition.x()
 | |
| 			- st::settingsInfoRightSkip);
 | |
| 		text->moveToLeft(
 | |
| 			st::settingsInfoValuePosition.x(),
 | |
| 			st::settingsInfoValuePosition.y(),
 | |
| 			width);
 | |
| 		about->resizeToWidth(width
 | |
| 			- st::settingsInfoAboutPosition.x()
 | |
| 			- st::settingsInfoRightSkip);
 | |
| 		about->moveToLeft(
 | |
| 			st::settingsInfoAboutPosition.x(),
 | |
| 			st::settingsInfoAboutPosition.y(),
 | |
| 			width);
 | |
| 		button->moveToRight(
 | |
| 			st::settingsInfoEditRight,
 | |
| 			(size.height() - button->height()) / 2,
 | |
| 			width);
 | |
| 	}, wrap->lifetime());
 | |
| }
 | |
| 
 | |
| void SetupRows(
 | |
| 		not_null<Ui::VerticalLayout*> container,
 | |
| 		not_null<Window::SessionController*> controller,
 | |
| 		not_null<UserData*> self) {
 | |
| 	const auto session = &self->session();
 | |
| 
 | |
| 	AddSkip(container);
 | |
| 
 | |
| 	AddRow(
 | |
| 		container,
 | |
| 		tr::lng_settings_name_label(),
 | |
| 		Info::Profile::NameValue(self),
 | |
| 		tr::lng_profile_copy_fullname(tr::now),
 | |
| 		[=] { controller->show(Box<EditNameBox>(self)); },
 | |
| 		st::settingsInfoName);
 | |
| 
 | |
| 	AddRow(
 | |
| 		container,
 | |
| 		tr::lng_settings_phone_label(),
 | |
| 		Info::Profile::PhoneValue(self),
 | |
| 		tr::lng_profile_copy_phone(tr::now),
 | |
| 		[=] { controller->show(Box<ChangePhoneBox>(controller)); },
 | |
| 		st::settingsInfoPhone);
 | |
| 
 | |
| 	auto username = Info::Profile::UsernameValue(self);
 | |
| 	auto empty = base::duplicate(
 | |
| 		username
 | |
| 	) | rpl::map([](const TextWithEntities &username) {
 | |
| 		return username.text.isEmpty();
 | |
| 	});
 | |
| 	auto label = rpl::combine(
 | |
| 		tr::lng_settings_username_label(),
 | |
| 		std::move(empty)
 | |
| 	) | rpl::map([](const QString &label, bool empty) {
 | |
| 		return empty ? "t.me/username" : label;
 | |
| 	});
 | |
| 	auto value = rpl::combine(
 | |
| 		std::move(username),
 | |
| 		tr::lng_settings_username_add()
 | |
| 	) | rpl::map([](const TextWithEntities &username, const QString &add) {
 | |
| 		if (!username.text.isEmpty()) {
 | |
| 			return username;
 | |
| 		}
 | |
| 		auto result = TextWithEntities{ add };
 | |
| 		result.entities.push_back({
 | |
| 			EntityType::CustomUrl,
 | |
| 			0,
 | |
| 			int(add.size()),
 | |
| 			"internal:edit_username" });
 | |
| 		return result;
 | |
| 	});
 | |
| 	AddRow(
 | |
| 		container,
 | |
| 		std::move(label),
 | |
| 		std::move(value),
 | |
| 		tr::lng_context_copy_mention(tr::now),
 | |
| 		[=] { controller->show(Box<UsernameBox>(session)); },
 | |
| 		st::settingsInfoUsername);
 | |
| 
 | |
| 	AddSkip(container, st::settingsInfoAfterSkip);
 | |
| }
 | |
| 
 | |
| void SetupBio(
 | |
| 		not_null<Ui::VerticalLayout*> container,
 | |
| 		not_null<UserData*> self) {
 | |
| 	AddDivider(container);
 | |
| 	AddSkip(container);
 | |
| 
 | |
| 	const auto bioStyle = [] {
 | |
| 		auto result = st::settingsBio;
 | |
| 		result.textMargins.setRight(st::boxTextFont->spacew
 | |
| 			+ st::boxTextFont->width(QString::number(kMaxBioLength)));
 | |
| 		return result;
 | |
| 	};
 | |
| 	const auto style = Ui::AttachAsChild(container, bioStyle());
 | |
| 	const auto current = Ui::AttachAsChild(container, self->about());
 | |
| 	const auto changed = Ui::CreateChild<rpl::event_stream<bool>>(
 | |
| 		container.get());
 | |
| 	const auto bio = container->add(
 | |
| 		object_ptr<Ui::InputField>(
 | |
| 			container,
 | |
| 			*style,
 | |
| 			Ui::InputField::Mode::MultiLine,
 | |
| 			tr::lng_bio_placeholder(),
 | |
| 			*current),
 | |
| 		st::settingsBioMargins);
 | |
| 
 | |
| 	const auto countdown = Ui::CreateChild<Ui::FlatLabel>(
 | |
| 		container.get(),
 | |
| 		QString(),
 | |
| 		st::settingsBioCountdown);
 | |
| 
 | |
| 	rpl::combine(
 | |
| 		bio->geometryValue(),
 | |
| 		countdown->widthValue()
 | |
| 	) | rpl::start_with_next([=](QRect geometry, int width) {
 | |
| 		countdown->move(
 | |
| 			geometry.x() + geometry.width() - width,
 | |
| 			geometry.y() + style->textMargins.top());
 | |
| 	}, countdown->lifetime());
 | |
| 
 | |
| 	const auto assign = [=](QString text) {
 | |
| 		auto position = bio->textCursor().position();
 | |
| 		bio->setText(text.replace('\n', ' '));
 | |
| 		auto cursor = bio->textCursor();
 | |
| 		cursor.setPosition(position);
 | |
| 		bio->setTextCursor(cursor);
 | |
| 	};
 | |
| 	const auto updated = [=] {
 | |
| 		auto text = bio->getLastText();
 | |
| 		if (text.indexOf('\n') >= 0) {
 | |
| 			assign(text);
 | |
| 			text = bio->getLastText();
 | |
| 		}
 | |
| 		changed->fire(*current != text);
 | |
| 		const auto countLeft = qMax(kMaxBioLength - text.size(), 0);
 | |
| 		countdown->setText(QString::number(countLeft));
 | |
| 	};
 | |
| 	const auto save = [=] {
 | |
| 		self->session().api().saveSelfBio(
 | |
| 			TextUtilities::PrepareForSending(bio->getLastText()));
 | |
| 	};
 | |
| 
 | |
| 	Info::Profile::AboutValue(
 | |
| 		self
 | |
| 	) | rpl::start_with_next([=](const TextWithEntities &text) {
 | |
| 		const auto wasChanged = (*current != bio->getLastText());
 | |
| 		*current = text.text;
 | |
| 		if (wasChanged) {
 | |
| 			changed->fire(*current != bio->getLastText());
 | |
| 		} else {
 | |
| 			assign(text.text);
 | |
| 			*current = bio->getLastText();
 | |
| 		}
 | |
| 	}, bio->lifetime());
 | |
| 
 | |
| 	const auto generation = Ui::CreateChild<int>(bio);
 | |
| 	changed->events(
 | |
| 	) | rpl::start_with_next([=](bool changed) {
 | |
| 		if (changed) {
 | |
| 			const auto saved = *generation = std::abs(*generation) + 1;
 | |
| 			base::call_delayed(kSaveBioTimeout, bio, [=] {
 | |
| 				if (*generation == saved) {
 | |
| 					save();
 | |
| 					*generation = 0;
 | |
| 				}
 | |
| 			});
 | |
| 		} else if (*generation > 0) {
 | |
| 			*generation = -*generation;
 | |
| 		}
 | |
| 	}, bio->lifetime());
 | |
| 
 | |
| 	// We need 'bio' to still exist here as InputField, so we add this
 | |
| 	// to 'container' lifetime, not to the 'bio' lifetime.
 | |
| 	container->lifetime().add([=] {
 | |
| 		if (*generation > 0) {
 | |
| 			save();
 | |
| 		}
 | |
| 	});
 | |
| 
 | |
| 	bio->setMaxLength(kMaxBioLength);
 | |
| 	bio->setSubmitSettings(Ui::InputField::SubmitSettings::Both);
 | |
| 	auto cursor = bio->textCursor();
 | |
| 	cursor.setPosition(bio->getLastText().size());
 | |
| 	bio->setTextCursor(cursor);
 | |
| 	QObject::connect(bio, &Ui::InputField::submitted, [=] {
 | |
| 		save();
 | |
| 	});
 | |
| 	QObject::connect(bio, &Ui::InputField::changed, updated);
 | |
| 	bio->setInstantReplaces(Ui::InstantReplaces::Default());
 | |
| 	bio->setInstantReplacesEnabled(
 | |
| 		Core::App().settings().replaceEmojiValue());
 | |
| 	Ui::Emoji::SuggestionsController::Init(
 | |
| 		container->window(),
 | |
| 		bio,
 | |
| 		&self->session());
 | |
| 	updated();
 | |
| 
 | |
| 	container->add(
 | |
| 		object_ptr<Ui::FlatLabel>(
 | |
| 			container,
 | |
| 			tr::lng_settings_about_bio(),
 | |
| 			st::boxDividerLabel),
 | |
| 		st::settingsBioLabelPadding);
 | |
| 
 | |
| 	AddSkip(container);
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| Information::Information(
 | |
| 	QWidget *parent,
 | |
| 	not_null<Window::SessionController*> controller)
 | |
| : Section(parent) {
 | |
| 	setupContent(controller);
 | |
| }
 | |
| 
 | |
| void Information::setupContent(
 | |
| 		not_null<Window::SessionController*> controller) {
 | |
| 	const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
 | |
| 
 | |
| 	const auto self = controller->session().user();
 | |
| 	SetupPhoto(content, controller, self);
 | |
| 	SetupRows(content, controller, self);
 | |
| 	SetupBio(content, self);
 | |
| 
 | |
| 	Ui::ResizeFitChild(this, content);
 | |
| }
 | |
| 
 | |
| } // namespace Settings
 | 
