diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings
index 74b1518ab..11b1a75e8 100644
--- a/Telegram/Resources/langs/lang.strings
+++ b/Telegram/Resources/langs/lang.strings
@@ -734,6 +734,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_settings_sensitive_disable_filtering" = "Disable filtering";
 "lng_settings_sensitive_about" = "Display sensitive media in public channels on all your Telegram devices.";
 "lng_settings_security_bots" = "Bots and websites";
+"lng_settings_file_confirmations" = "File open confirmations";
+"lng_settings_edit_extensions" = "Extensions whitelist";
+"lng_settings_edit_extensions_about" = "Open files with the following extensions without additional confirmation.";
+"lng_settings_edit_ip_confirm" = "IP reveal warning";
+"lng_settings_edit_ip_confirm_about" = "Show confirmation when opening files that may reveal your IP address.";
 "lng_settings_clear_payment_info" = "Clear Payment and Shipping Info";
 "lng_settings_logged_in" = "Connected websites";
 "lng_settings_logged_in_title" = "Logged in with Telegram";
@@ -4480,10 +4485,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 "lng_translate_settings_about" = "The 'Translate' button will appear when you open a context menu on a text message.";
 "lng_translate_settings_one" = "Please choose at least one language so that it can be used as the \"Translate to\" language.";
 
-"lng_launch_exe_warning" = "This file has a {extension} extension.\nAre you sure you want to run it?";
+"lng_launch_exe_warning" = "This file has {extension} extension.\nAre you sure you want to run it?";
+"lng_launch_other_warning" = "This file has {extension} extension.\nAre you sure you want to open it?";
 "lng_launch_svg_warning" = "Opening this file can potentially expose your IP address to its sender. Continue?";
 "lng_launch_exe_sure" = "Run";
+"lng_launch_other_sure" = "Open";
 "lng_launch_exe_dont_ask" = "Don't ask me again";
+"lng_launch_dont_ask" = "Remember for this file type";
+"lng_launch_dont_ask_settings" = "You can later edit trusted file types in Settings > Privacy and Security > File open confirmations.";
 
 "lng_polls_anonymous" = "Anonymous Poll";
 "lng_polls_public" = "Poll";
diff --git a/Telegram/SourceFiles/boxes/ringtones_box.cpp b/Telegram/SourceFiles/boxes/ringtones_box.cpp
index d8097222e..4c299c162 100644
--- a/Telegram/SourceFiles/boxes/ringtones_box.cpp
+++ b/Telegram/SourceFiles/boxes/ringtones_box.cpp
@@ -90,7 +90,7 @@ QString ExtractRingtoneName(not_null<DocumentData*> document) {
 	}
 	const auto name = document->filename();
 	if (!name.isEmpty()) {
-		const auto extension = Data::FileExtension(name);
+		const auto extension = Core::FileExtension(name);
 		if (extension.isEmpty()) {
 			return name;
 		} else if (name.size() > extension.size() + 1) {
diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp
index b4f770876..faa41445f 100644
--- a/Telegram/SourceFiles/core/core_settings.cpp
+++ b/Telegram/SourceFiles/core/core_settings.cpp
@@ -156,6 +156,10 @@ QByteArray Settings::serialize() const {
 	const auto &recentEmojiPreloadData = _recentEmojiPreload.empty()
 		? recentEmojiPreloadGenerated
 		: _recentEmojiPreload;
+	const auto noWarningExtensions = QStringList(
+		begin(_noWarningExtensions),
+		end(_noWarningExtensions)
+	).join(' ');
 
 	auto size = Serialize::bytearraySize(themesAccentColors)
 		+ sizeof(qint32) * 5
@@ -212,7 +216,8 @@ QByteArray Settings::serialize() const {
 		+ Serialize::stringSize(_captureDeviceId.current())
 		+ Serialize::stringSize(_callPlaybackDeviceId.current())
 		+ Serialize::stringSize(_callCaptureDeviceId.current())
-		+ Serialize::bytearraySize(ivPosition);
+		+ Serialize::bytearraySize(ivPosition)
+		+ Serialize::stringSize(noWarningExtensions);
 
 	auto result = QByteArray();
 	result.reserve(size);
@@ -252,7 +257,7 @@ QByteArray Settings::serialize() const {
 			<< qint32(_sendSubmitWay)
 			<< qint32(_includeMutedCounter ? 1 : 0)
 			<< qint32(_countUnreadMessages ? 1 : 0)
-			<< qint32(_exeLaunchWarning ? 1 : 0)
+			<< qint32(1) // legacy exe launch warning
 			<< qint32(_notifyAboutPinned.current() ? 1 : 0)
 			<< qint32(_loopAnimatedStickers ? 1 : 0)
 			<< qint32(_largeEmoji.current() ? 1 : 0)
@@ -357,7 +362,8 @@ QByteArray Settings::serialize() const {
 			<< _captureDeviceId.current()
 			<< _callPlaybackDeviceId.current()
 			<< _callCaptureDeviceId.current()
-			<< ivPosition;
+			<< ivPosition
+			<< noWarningExtensions;
 	}
 
 	Ensures(result.size() == size);
@@ -406,7 +412,8 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 	qint32 sendSubmitWay = static_cast<qint32>(_sendSubmitWay);
 	qint32 includeMutedCounter = _includeMutedCounter ? 1 : 0;
 	qint32 countUnreadMessages = _countUnreadMessages ? 1 : 0;
-	qint32 exeLaunchWarning = _exeLaunchWarning ? 1 : 0;
+	std::optional<QString> noWarningExtensions;
+	qint32 legacyExeLaunchWarning = 1;
 	qint32 notifyAboutPinned = _notifyAboutPinned.current() ? 1 : 0;
 	qint32 loopAnimatedStickers = _loopAnimatedStickers ? 1 : 0;
 	qint32 largeEmoji = _largeEmoji.current() ? 1 : 0;
@@ -513,7 +520,7 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 			>> sendSubmitWay
 			>> includeMutedCounter
 			>> countUnreadMessages
-			>> exeLaunchWarning
+			>> legacyExeLaunchWarning
 			>> notifyAboutPinned
 			>> loopAnimatedStickers
 			>> largeEmoji
@@ -755,6 +762,10 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 	if (!stream.atEnd()) {
 		stream >> ivPosition;
 	}
+	if (!stream.atEnd()) {
+		noWarningExtensions = QString();
+		stream >> *noWarningExtensions;
+	}
 	if (stream.status() != QDataStream::Ok) {
 		LOG(("App Error: "
 			"Bad data for Core::Settings::constructFromSerialized()"));
@@ -817,7 +828,12 @@ void Settings::addFromSerialized(const QByteArray &serialized) {
 	}
 	_includeMutedCounter = (includeMutedCounter == 1);
 	_countUnreadMessages = (countUnreadMessages == 1);
-	_exeLaunchWarning = (exeLaunchWarning == 1);
+	if (noWarningExtensions) {
+		const auto list = noWarningExtensions->mid(0, 10240)
+			.split(' ', Qt::SkipEmptyParts)
+			.mid(0, 1024);
+		_noWarningExtensions = base::flat_set<QString>(list.begin(), list.end());
+	}
 	_ipRevealWarning = (ipRevealWarning == 1);
 	_notifyAboutPinned = (notifyAboutPinned == 1);
 	_loopAnimatedStickers = (loopAnimatedStickers == 1);
@@ -1283,7 +1299,7 @@ void Settings::resetOnLastLogout() {
 	//_sendSubmitWay = Ui::InputSubmitSettings::Enter;
 	_soundOverrides = {};
 
-	_exeLaunchWarning = true;
+	_noWarningExtensions.clear();
 	_ipRevealWarning = true;
 	_loopAnimatedStickers = true;
 	_largeEmoji = true;
diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h
index 12b7fa431..cded2ec20 100644
--- a/Telegram/SourceFiles/core/core_settings.h
+++ b/Telegram/SourceFiles/core/core_settings.h
@@ -398,11 +398,12 @@ public:
 	}
 	[[nodiscard]] QString getSoundPath(const QString &key) const;
 
-	[[nodiscard]] bool exeLaunchWarning() const {
-		return _exeLaunchWarning;
+	[[nodiscard]] auto noWarningExtensions() const
+	-> const base::flat_set<QString> & {
+		return _noWarningExtensions;
 	}
-	void setExeLaunchWarning(bool warning) {
-		_exeLaunchWarning = warning;
+	void setNoWarningExtensions(base::flat_set<QString> extensions) {
+		_noWarningExtensions = std::move(extensions);
 	}
 	[[nodiscard]] bool ipRevealWarning() const {
 		return _ipRevealWarning;
@@ -933,7 +934,7 @@ private:
 	Ui::SendFilesWay _sendFilesWay = Ui::SendFilesWay();
 	Ui::InputSubmitSettings _sendSubmitWay = Ui::InputSubmitSettings();
 	base::flat_map<QString, QString> _soundOverrides;
-	bool _exeLaunchWarning = true;
+	base::flat_set<QString> _noWarningExtensions;
 	bool _ipRevealWarning = true;
 	bool _loopAnimatedStickers = true;
 	rpl::variable<bool> _largeEmoji = true;
diff --git a/Telegram/SourceFiles/core/mime_type.cpp b/Telegram/SourceFiles/core/mime_type.cpp
index 8ba1b6494..7467145cf 100644
--- a/Telegram/SourceFiles/core/mime_type.cpp
+++ b/Telegram/SourceFiles/core/mime_type.cpp
@@ -37,6 +37,12 @@ namespace {
 		&& data->hasImage();
 }
 
+[[nodiscard]] base::flat_set<QString> SplitExtensions(
+		const QString &joined) {
+	const auto list = joined.split(' ');
+	return base::flat_set<QString>(list.begin(), list.end());
+}
+
 } // namespace
 
 MimeType::MimeType(const QMimeType &type) : _typeStruct(type) {
@@ -162,22 +168,9 @@ bool IsMimeAcceptedForPhotoVideoAlbum(const QString &mime) {
 }
 
 bool FileIsImage(const QString &name, const QString &mime) {
-	QString lowermime = mime.toLower(), namelower = name.toLower();
-	if (lowermime.startsWith(u"image/"_q)) {
-		return true;
-	} else if (namelower.endsWith(u".bmp"_q)
-		|| namelower.endsWith(u".jpg"_q)
-		|| namelower.endsWith(u".jpeg"_q)
-		|| namelower.endsWith(u".gif"_q)
-		|| namelower.endsWith(u".webp"_q)
-		|| namelower.endsWith(u".tga"_q)
-		|| namelower.endsWith(u".tiff"_q)
-		|| namelower.endsWith(u".tif"_q)
-		|| namelower.endsWith(u".psd"_q)
-		|| namelower.endsWith(u".png"_q)) {
-		return true;
-	}
-	return false;
+	return name.isEmpty()
+		? mime.toLower().startsWith(u"image/"_q)
+		: (DetectNameType(name) == NameType::Image);
 }
 
 std::shared_ptr<QMimeData> ShareMimeMediaData(
@@ -194,10 +187,10 @@ std::shared_ptr<QMimeData> ShareMimeMediaData(
 		result->setData(u"application/x-td-use-jpeg"_q, "1");
 		result->setData(u"image/jpeg"_q, original->data(u"image/jpeg"_q));
 	}
-	if (auto list = Core::ReadMimeUrls(original); !list.isEmpty()) {
+	if (auto list = ReadMimeUrls(original); !list.isEmpty()) {
 		result->setUrls(std::move(list));
 	}
-	result->setText(Core::ReadMimeText(original));
+	result->setText(ReadMimeText(original));
 	return result;
 }
 
@@ -240,4 +233,116 @@ bool CanSendFiles(not_null<const QMimeData*> data) {
 	return false;
 }
 
+QString FileExtension(const QString &filepath) {
+	const auto reversed = ranges::views::reverse(filepath);
+	const auto last = ranges::find_first_of(reversed, ".\\/");
+	if (last == reversed.end() || *last != '.') {
+		return QString();
+	}
+	return QString(last.base(), last - reversed.begin());
+}
+
+NameType DetectNameType(const QString &filepath) {
+	static const auto kImage = SplitExtensions(u"\
+afdesign ai avif bmp dng gif heic icns ico jfif jpeg jpg jpg-large nef png \
+png-large psd raw sketch svg tga tif tiff webp"_q);
+	static const auto kVideo = SplitExtensions(u"\
+3g2 3gp 3gpp aep avi flv h264 m4s m4v mkv mov mp4 mpeg mpg ogv srt tgs tgv \
+vob webm wmv"_q);
+	static const auto kAudio = SplitExtensions(u"\
+aac ac3 aif amr caf cda cue flac m4a m4b mid midi mp3 ogg opus wav wma"_q);
+	static const auto kDocument = SplitExtensions(u"\
+pdf doc docx ppt pptx pps ppsx xls xlsx txt rtf odt ods odp csv text log tl \
+tex xspf xml djvu diag ps ost kml pub epub mobi cbr cbz fb2 prc ris pem p7b \
+m3u m3u8 wpd wpl htm html xhtml key"_q);
+	static const auto kArchive = SplitExtensions(u"\
+7z arj bz2 gz rar tar xz z zip zst"_q);
+	static const auto kThemeFile = SplitExtensions(u"\
+tdesktop-theme tdesktop-palette tgios-theme attheme"_q);
+	static const auto kOtherBenign = SplitExtensions(u"\
+c cc cpp cxx h m mm swift cs ts class java css ninja cmake patch diff plist \
+gyp gitignore strings asoundrc torrent csr json xaml md keylayout sql \
+sln xib mk \
+\
+dmg img iso vcd \
+\
+pdb eot ics ips ipa core mem pcap ovpn part pcapng dmp pkpass dat zxp crash \
+file bak gbr plain dlc fon fnt otf ttc ttf gpx db rss cur \
+\
+tdesktop-endpoints"_q);
+
+	static const auto kExecutable = SplitExtensions(
+#ifdef Q_OS_WIN
+		u"\
+ad ade adp ahk app application appref-ms asp aspx asx bas bat bin cab cdxml \
+cer cfg cgi chi chm cmd cnt com conf cpl crt csh der diagcab dll drv eml \
+exe fon fxp gadget grp hlp hpj hta htt inf ini ins inx isp isu its jar jnlp \
+job js jse jsp key ksh lexe library-ms lnk local lua mad maf mag mam \
+manifest maq mar mas mat mau mav maw mcf mda mdb mde mdt mdw mdz mht mhtml \
+mjs mmc mof msc msg msh msh1 msh2 msh1xml msh2xml mshxml msi msp mst ops \
+osd paf pcd phar php php3 php4 php5 php7 phps php-s pht phtml pif pl plg pm \
+pod prf prg ps1 ps2 ps1xml ps2xml psc1 psc2 psd1 psm1 pssc pst py py3 pyc \
+pyd pyi pyo pyw pyzw pyz rb reg rgs scf scr sct search-ms settingcontent-ms \
+sh shb shs slk sys swf t tmp u3p url vb vbe vbp vbs vbscript vdx vsmacros \
+vsd vsdm vsdx vss vssm vssx vst vstm vstx vsw vsx vtx website wlua ws wsc \
+wsf wsh xbap xll xlsm xnk xs"_q
+#elif defined Q_OS_MAC // Q_OS_MAC
+		u"\
+applescript action app bin command csh osx workflow terminal url caction \
+mpkg pkg scpt scptd xhtm xhtml webarchive"_q
+#else // Q_OS_WIN || Q_OS_MAC
+		u"bin csh deb desktop ksh out pet pkg pup rpm run sh shar slp zsh"_q
+#endif // !Q_OS_WIN && !Q_OS_MAC
+	);
+
+	const auto extension = FileExtension(filepath).toLower();
+	if (kExecutable.contains(extension)) {
+		return NameType::Executable;
+	} else if (kImage.contains(extension)) {
+		return NameType::Image;
+	} else if (kVideo.contains(extension)) {
+		return NameType::Video;
+	} else if (kAudio.contains(extension)) {
+		return NameType::Audio;
+	} else if (kDocument.contains(extension)) {
+		return NameType::Document;
+	} else if (kArchive.contains(extension)) {
+		return NameType::Archive;
+	} else if (kThemeFile.contains(extension)) {
+		return NameType::ThemeFile;
+	} else if (kOtherBenign.contains(extension)) {
+		return NameType::OtherBenign;
+	}
+	return NameType::Unknown;
+}
+
+bool NameTypeAllowsThumbnail(NameType type) {
+	return type == NameType::Image
+		|| type == NameType::Video
+		|| type == NameType::Audio
+		|| type == NameType::Document
+		|| type == NameType::ThemeFile;
+}
+
+bool IsIpRevealingPath(const QString &filepath) {
+	static const auto kExtensions = [] {
+		const auto joined = u"htm html svg m4v m3u8 xhtml"_q;
+		const auto list = joined.split(' ');
+		return base::flat_set<QString>(list.begin(), list.end());
+	}();
+	static const auto kMimeTypes = [] {
+		const auto joined = u"text/html image/svg+xml"_q;
+		const auto list = joined.split(' ');
+		return base::flat_set<QString>(list.begin(), list.end());
+	}();
+
+	return ranges::binary_search(
+		kExtensions,
+		FileExtension(filepath).toLower()
+	) || ranges::binary_search(
+		kMimeTypes,
+		QMimeDatabase().mimeTypeForFile(QFileInfo(filepath)).name()
+	);
+}
+
 } // namespace Core
diff --git a/Telegram/SourceFiles/core/mime_type.h b/Telegram/SourceFiles/core/mime_type.h
index 3271adafe..ebf4db64b 100644
--- a/Telegram/SourceFiles/core/mime_type.h
+++ b/Telegram/SourceFiles/core/mime_type.h
@@ -69,4 +69,21 @@ struct MimeImageData {
 [[nodiscard]] QList<QUrl> ReadMimeUrls(not_null<const QMimeData*> data);
 [[nodiscard]] bool CanSendFiles(not_null<const QMimeData*> data);
 
+enum class NameType : uchar {
+	Unknown,
+	Executable,
+	Image,
+	Video,
+	Audio,
+	Document,
+	Archive,
+	ThemeFile,
+	OtherBenign,
+};
+
+[[nodiscard]] QString FileExtension(const QString &filepath);
+[[nodiscard]] NameType DetectNameType(const QString &filepath);
+[[nodiscard]] bool NameTypeAllowsThumbnail(NameType type);
+[[nodiscard]] bool IsIpRevealingPath(const QString &filepath);
+
 } // namespace Core
diff --git a/Telegram/SourceFiles/data/data_cloud_file.cpp b/Telegram/SourceFiles/data/data_cloud_file.cpp
index bee90358a..526dd8904 100644
--- a/Telegram/SourceFiles/data/data_cloud_file.cpp
+++ b/Telegram/SourceFiles/data/data_cloud_file.cpp
@@ -22,6 +22,14 @@ CloudFile::~CloudFile() {
 	base::take(loader);
 }
 
+void CloudFile::clear() {
+	location = {};
+	base::take(loader);
+	byteSize = 0;
+	progressivePartSize = 0;
+	flags = {};
+}
+
 CloudImage::CloudImage() = default;
 
 CloudImage::CloudImage(
diff --git a/Telegram/SourceFiles/data/data_cloud_file.h b/Telegram/SourceFiles/data/data_cloud_file.h
index e71cbe462..b44a35c7d 100644
--- a/Telegram/SourceFiles/data/data_cloud_file.h
+++ b/Telegram/SourceFiles/data/data_cloud_file.h
@@ -37,6 +37,8 @@ struct CloudFile final {
 
 	~CloudFile();
 
+	void clear();
+
 	ImageLocation location;
 	std::unique_ptr<FileLoader> loader;
 	int byteSize = 0;
diff --git a/Telegram/SourceFiles/data/data_document.cpp b/Telegram/SourceFiles/data/data_document.cpp
index afe91b8a1..2bf1a0104 100644
--- a/Telegram/SourceFiles/data/data_document.cpp
+++ b/Telegram/SourceFiles/data/data_document.cpp
@@ -478,6 +478,31 @@ void DocumentData::setattributes(
 		_additional = nullptr;
 	}
 
+	if (!_filename.isEmpty()) {
+		using Type = Core::NameType;
+		if (type == VideoDocument
+			|| type == AnimatedDocument
+			|| type == RoundVideoDocument
+			|| isAnimation()) {
+			if (_nameType != Type::Video) {
+				type = FileDocument;
+				_additional = nullptr;
+			}
+		}
+		if (type == SongDocument || type == VoiceDocument || isAudioFile()) {
+			if (_nameType != Type::Audio) {
+				type = FileDocument;
+				_additional = nullptr;
+			}
+		}
+		if (!Core::NameTypeAllowsThumbnail(_nameType)) {
+			_inlineThumbnailBytes = {};
+			_flags &= ~Flag::InlineThumbnailIsPath;
+			_thumbnail.clear();
+			_videoThumbnail.clear();
+		}
+	}
+
 	if (isAudioFile()
 		|| isAnimation()
 		|| isVoiceMessage()
@@ -530,6 +555,10 @@ void DocumentData::updateThumbnails(
 		const ImageWithLocation &thumbnail,
 		const ImageWithLocation &videoThumbnail,
 		bool isPremiumSticker) {
+	if (!_filename.isEmpty()
+		&& !Core::NameTypeAllowsThumbnail(Core::DetectNameType(_filename))) {
+		return;
+	}
 	if (!inlineThumbnail.bytes.isEmpty()
 		&& _inlineThumbnailBytes.isEmpty()) {
 		_inlineThumbnailBytes = inlineThumbnail.bytes;
@@ -919,6 +948,7 @@ void DocumentData::setFileName(const QString &remoteFileName) {
 	for (const auto &ch : controls) {
 		_filename = std::move(_filename).replace(ch, "_");
 	}
+	_nameType = Core::DetectNameType(_filename);
 }
 
 void DocumentData::setLoadedInMediaCacheLocation() {
@@ -1460,6 +1490,10 @@ QString DocumentData::filename() const {
 	return _filename;
 }
 
+Core::NameType DocumentData::nameType() const {
+	return _nameType;
+}
+
 QString DocumentData::mimeString() const {
 	return _mimeString;
 }
@@ -1527,7 +1561,10 @@ bool DocumentData::isVideoMessage() const {
 bool DocumentData::isAnimation() const {
 	return (type == AnimatedDocument)
 		|| isVideoMessage()
-		|| (hasMimeType(u"image/gif"_q)
+		|| ((_filename.isEmpty()
+			|| _nameType == Core::NameType::Image
+			|| _nameType == Core::NameType::Video)
+			&& hasMimeType(u"image/gif"_q)
 			&& !(_flags & Flag::StreamingPlaybackFailed));
 }
 
@@ -1537,9 +1574,11 @@ bool DocumentData::isGifv() const {
 }
 
 bool DocumentData::isTheme() const {
-	return hasMimeType(u"application/x-tgtheme-tdesktop"_q)
-		|| _filename.endsWith(u".tdesktop-theme"_q, Qt::CaseInsensitive)
-		|| _filename.endsWith(u".tdesktop-palette"_q, Qt::CaseInsensitive);
+	return _filename.endsWith(u".tdesktop-theme"_q, Qt::CaseInsensitive)
+		|| _filename.endsWith(u".tdesktop-palette"_q, Qt::CaseInsensitive)
+		|| (hasMimeType(u"application/x-tgtheme-tdesktop"_q)
+			&& (_filename.isEmpty()
+				|| _nameType == Core::NameType::ThemeFile));
 }
 
 bool DocumentData::isSong() const {
@@ -1562,6 +1601,10 @@ bool DocumentData::isAudioFile() const {
 			return true;
 		}
 		return false;
+	} else if (!_filename.isEmpty()
+		&& _nameType != Core::NameType::Audio
+		&& _nameType != Core::NameType::Video) {
+		return false;
 	}
 	const auto left = _mimeString.mid(prefix.size());
 	const auto types = { u"x-wav"_q, u"wav"_q, u"mp4"_q };
diff --git a/Telegram/SourceFiles/data/data_document.h b/Telegram/SourceFiles/data/data_document.h
index c33c9cbd0..299a6caf1 100644
--- a/Telegram/SourceFiles/data/data_document.h
+++ b/Telegram/SourceFiles/data/data_document.h
@@ -20,6 +20,10 @@ namespace Images {
 class Source;
 } // namespace Images
 
+namespace Core {
+enum class NameType : uchar;
+} // namespace Core
+
 namespace Storage {
 namespace Cache {
 struct Key;
@@ -255,6 +259,7 @@ public:
 	void collectLocalData(not_null<DocumentData*> local);
 
 	[[nodiscard]] QString filename() const;
+	[[nodiscard]] Core::NameType nameType() const;
 	[[nodiscard]] QString mimeString() const;
 	[[nodiscard]] bool hasMimeType(const QString &mime) const;
 	void setMimeString(const QString &mime);
@@ -369,6 +374,7 @@ private:
 	std::unique_ptr<DocumentAdditionalData> _additional;
 	mutable Flags _flags = kStreamingSupportedUnknown;
 	GoodThumbnailState _goodThumbnailState = GoodThumbnailState();
+	Core::NameType _nameType = Core::NameType();
 	std::unique_ptr<FileLoader> _loader;
 
 };
diff --git a/Telegram/SourceFiles/data/data_document_media.cpp b/Telegram/SourceFiles/data/data_document_media.cpp
index 101f0f8cc..e61030302 100644
--- a/Telegram/SourceFiles/data/data_document_media.cpp
+++ b/Telegram/SourceFiles/data/data_document_media.cpp
@@ -22,6 +22,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "window/themes/window_theme_preview.h"
 #include "core/core_settings.h"
 #include "core/application.h"
+#include "core/mime_type.h"
 #include "storage/file_download.h"
 #include "ui/chat/attach/attach_prepare.h"
 
@@ -295,10 +296,12 @@ void DocumentMedia::automaticLoad(
 		// No automatic download in this case.
 		return;
 	}
+	const auto indata = _owner->filename();
 	const auto filename = toCache
 		? QString()
 		: DocumentFileNameForSave(_owner);
-	const auto shouldLoadFromCloud = !Data::IsExecutableName(filename)
+	const auto shouldLoadFromCloud = (indata.isEmpty()
+		|| Core::DetectNameType(indata) != Core::NameType::Executable)
 		&& (item
 			? Data::AutoDownload::Should(
 				_owner->session().settings().autoDownload(),
diff --git a/Telegram/SourceFiles/data/data_document_resolver.cpp b/Telegram/SourceFiles/data/data_document_resolver.cpp
index be68c2476..575d695cf 100644
--- a/Telegram/SourceFiles/data/data_document_resolver.cpp
+++ b/Telegram/SourceFiles/data/data_document_resolver.cpp
@@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/chat/chat_theme.h"
 #include "ui/text/text_utilities.h"
 #include "ui/widgets/checkbox.h"
+#include "ui/wrap/slide_wrap.h"
 #include "window/window_session_controller.h"
 #include "styles/style_layers.h"
 
@@ -46,11 +47,12 @@ base::options::toggle OptionExternalVideoPlayer({
 void ConfirmDontWarnBox(
 		not_null<Ui::GenericBox*> box,
 		rpl::producer<TextWithEntities> &&text,
+		rpl::producer<QString> &&check,
 		rpl::producer<QString> &&confirm,
 		Fn<void(bool)> callback) {
 	auto checkbox = object_ptr<Ui::Checkbox>(
 		box.get(),
-		tr::lng_launch_exe_dont_ask(),
+		std::move(check),
 		false,
 		st::defaultBoxCheckbox);
 	const auto weak = Ui::MakeWeak(checkbox.data());
@@ -67,29 +69,43 @@ void ConfirmDontWarnBox(
 	auto padding = st::boxPadding;
 	padding.setTop(padding.bottom());
 	box->addRow(std::move(checkbox), std::move(padding));
+	box->addRow(object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
+		box,
+		object_ptr<Ui::FlatLabel>(
+			box,
+			tr::lng_launch_dont_ask_settings(),
+			st::boxLabel)
+	))->toggleOn(weak->checkedValue());
 }
 
 void LaunchWithWarning(
 		// not_null<Window::Controller*> controller,
 		const QString &name,
 		HistoryItem *item) {
-	const auto isExecutable = Data::IsExecutableName(name);
-	const auto isIpReveal = Data::IsIpRevealingName(name);
+	const auto nameType = Core::DetectNameType(name);
+	const auto isIpReveal = (nameType != Core::NameType::Executable)
+		&& Core::IsIpRevealingPath(name);
+	const auto extension = Core::FileExtension(name).toLower();
+
 	auto &app = Core::App();
+	auto &settings = app.settings();
 	const auto warn = [&] {
 		if (item && item->history()->peer->isVerified()) {
 			return false;
 		}
-		return (isExecutable && app.settings().exeLaunchWarning())
-			|| (isIpReveal && app.settings().ipRevealWarning());
+		return (isIpReveal && settings.ipRevealWarning())
+			|| ((nameType == Core::NameType::Executable
+				|| nameType == Core::NameType::Unknown)
+				&& !settings.noWarningExtensions().contains(extension));
 	}();
-	const auto extension = '.' + Data::FileExtension(name);
-	if (Platform::IsWindows() && extension == u"."_q) {
+	if (extension.isEmpty()) {
 		// If you launch a file without extension, like "test", in case
 		// there is an executable file with the same name in this folder,
 		// like "test.bat", the executable file will be launched.
 		//
 		// Now we always force an Open With dialog box for such files.
+		//
+		// Let's force it for all platforms for files without extension.
 		crl::on_main([=] {
 			Platform::File::UnsafeShowOpenWith(name);
 		});
@@ -98,27 +114,38 @@ void LaunchWithWarning(
 		File::Launch(name);
 		return;
 	}
-	const auto callback = [=, &app](bool checked) {
+	const auto callback = [=, &app, &settings](bool checked) {
 		if (checked) {
-			if (isExecutable) {
-				app.settings().setExeLaunchWarning(false);
-			} else if (isIpReveal) {
-				app.settings().setIpRevealWarning(false);
+			if (isIpReveal) {
+				settings.setIpRevealWarning(false);
+			} else {
+				auto copy = settings.noWarningExtensions();
+				copy.emplace(extension);
+				settings.setNoWarningExtensions(std::move(copy));
 			}
 			app.saveSettingsDelayed();
 		}
 		File::Launch(name);
 	};
-	auto text = isExecutable
-		? tr::lng_launch_exe_warning(
-			lt_extension,
-			rpl::single(Ui::Text::Bold(extension)),
-			Ui::Text::WithEntities)
-		: tr::lng_launch_svg_warning(Ui::Text::WithEntities);
+	auto text = isIpReveal
+		? tr::lng_launch_svg_warning(Ui::Text::WithEntities)
+		: ((nameType == Core::NameType::Executable)
+			? tr::lng_launch_exe_warning
+			: tr::lng_launch_other_warning)(
+				lt_extension,
+				rpl::single(Ui::Text::Bold('.' + extension)),
+				Ui::Text::WithEntities);
+	auto check = (isIpReveal
+		? tr::lng_launch_exe_dont_ask
+		: tr::lng_launch_dont_ask)();
+	auto confirm = ((nameType == Core::NameType::Executable)
+		? tr::lng_launch_exe_sure
+		: tr::lng_launch_other_sure)();
 	Ui::show(Box(
 		ConfirmDontWarnBox,
 		std::move(text),
-		(isExecutable ? tr::lng_launch_exe_sure : tr::lng_continue)(),
+		std::move(check),
+		std::move(confirm),
 		callback));
 }
 
@@ -126,91 +153,6 @@ void LaunchWithWarning(
 
 const char kOptionExternalVideoPlayer[] = "external-video-player";
 
-QString FileExtension(const QString &filepath) {
-	const auto reversed = ranges::views::reverse(filepath);
-	const auto last = ranges::find_first_of(reversed, ".\\/");
-	if (last == reversed.end() || *last != '.') {
-		return QString();
-	}
-	return QString(last.base(), last - reversed.begin());
-}
-
-#if 0
-bool IsValidMediaFile(const QString &filepath) {
-	static const auto kExtensions = [] {
-		const auto list = qsl("\
-16svx 2sf 3g2 3gp 8svx aac aaf aif aifc aiff amr amv ape asf ast au aup \
-avchd avi brstm bwf cam cdda cust dat divx drc dsh dsf dts dtshd dtsma \
-dvr-ms dwd evo f4a f4b f4p f4v fla flac flr flv gif gifv gsf gsm gym iff \
-ifo it jam la ly m1v m2p m2ts m2v m4a m4p m4v mcf mid mk3d mka mks mkv mng \
-mov mp1 mp2 mp3 mp4 minipsf mod mpc mpe mpeg mpg mpv mscz mt2 mus mxf mxl \
-niff nsf nsv off ofr ofs ogg ogv opus ots pac ps psf psf2 psflib ptb qsf \
-qt ra raw rka rm rmj rmvb roq s3m shn sib sid smi smp sol spc spx ssf svi \
-swa swf tak ts tta txm usf vgm vob voc vox vqf wav webm wma wmv wrap wtv \
-wv xm xml ym yuv").split(' ');
-		return base::flat_set<QString>(list.begin(), list.end());
-	}();
-
-	return ranges::binary_search(
-		kExtensions,
-		FileExtension(filepath).toLower());
-}
-#endif
-
-bool IsExecutableName(const QString &filepath) {
-	static const auto kExtensions = [] {
-		const auto joined =
-#ifdef Q_OS_WIN
-			u"\
-ad ade adp app application appref-ms asp asx bas bat bin cab cdxml cer cfg \
-chi chm cmd cnt com cpl crt csh der diagcab dll drv eml exe fon fxp gadget \
-grp hlp hpj hta htt inf ini ins inx isp isu its jar jnlp job js jse key ksh \
-lexe lnk local lua mad maf mag mam manifest maq mar mas mat mau mav maw mcf \
-mda mdb mde mdt mdw mdz mht mhtml mjs mmc mof msc msg msh msh1 msh2 msh1xml \
-msh2xml mshxml msi msp mst ops osd paf pcd phar php php3 php4 php5 php7 phps \
-php-s pht phtml pif pl plg pm pod prf prg ps1 ps2 ps1xml ps2xml psc1 psc2 \
-psd1 psm1 pssc pst py py3 pyc pyd pyi pyo pyw pyzw pyz rb reg rgs scf scr \
-sct search-ms settingcontent-ms sh shb shs slk sys t tmp u3p url vb vbe vbp \
-vbs vbscript vdx vsmacros vsd vsdm vsdx vss vssm vssx vst vstm vstx vsw vsx \
-vtx website wlua ws wsc wsf wsh xbap xll xnk xs"_q;
-#elif defined Q_OS_MAC // Q_OS_MAC
-			u"\
-applescript action app bin command csh osx workflow terminal url caction \
-mpkg pkg scpt scptd xhtm webarchive"_q;
-#else // Q_OS_WIN || Q_OS_MAC
-			u"bin csh deb desktop ksh out pet pkg pup rpm run sh shar \
-slp zsh"_q;
-#endif // !Q_OS_WIN && !Q_OS_MAC
-		const auto list = joined.split(' ');
-		return base::flat_set<QString>(list.begin(), list.end());
-	}();
-
-	return ranges::binary_search(
-		kExtensions,
-		FileExtension(filepath).toLower());
-}
-
-bool IsIpRevealingName(const QString &filepath) {
-	static const auto kExtensions = [] {
-		const auto joined = u"htm html svg m4v m3u8"_q;
-		const auto list = joined.split(' ');
-		return base::flat_set<QString>(list.begin(), list.end());
-	}();
-	static const auto kMimeTypes = [] {
-		const auto joined = u"text/html image/svg+xml"_q;
-		const auto list = joined.split(' ');
-		return base::flat_set<QString>(list.begin(), list.end());
-	}();
-
-	return ranges::binary_search(
-		kExtensions,
-		FileExtension(filepath).toLower()
-	) || ranges::binary_search(
-		kMimeTypes,
-		QMimeDatabase().mimeTypeForFile(QFileInfo(filepath)).name()
-	);
-}
-
 base::binary_guard ReadBackgroundImageAsync(
 		not_null<Data::DocumentMedia*> media,
 		FnMut<QImage(QImage)> postprocess,
diff --git a/Telegram/SourceFiles/data/data_document_resolver.h b/Telegram/SourceFiles/data/data_document_resolver.h
index 9931da3e8..4988297aa 100644
--- a/Telegram/SourceFiles/data/data_document_resolver.h
+++ b/Telegram/SourceFiles/data/data_document_resolver.h
@@ -22,10 +22,6 @@ class DocumentMedia;
 
 extern const char kOptionExternalVideoPlayer[];
 
-[[nodiscard]] QString FileExtension(const QString &filepath);
-// [[nodiscard]] bool IsValidMediaFile(const QString &filepath);
-[[nodiscard]] bool IsExecutableName(const QString &filepath);
-[[nodiscard]] bool IsIpRevealingName(const QString &filepath);
 base::binary_guard ReadBackgroundImageAsync(
 	not_null<Data::DocumentMedia*> media,
 	FnMut<QImage(QImage)> postprocess,
diff --git a/Telegram/SourceFiles/history/view/media/history_view_document.cpp b/Telegram/SourceFiles/history/view/media/history_view_document.cpp
index d884580d5..b7714d579 100644
--- a/Telegram/SourceFiles/history/view/media/history_view_document.cpp
+++ b/Telegram/SourceFiles/history/view/media/history_view_document.cpp
@@ -380,12 +380,9 @@ void Document::createComponents(bool caption) {
 		mask |= HistoryDocumentVoice::Bit();
 	} else {
 		mask |= HistoryDocumentNamed::Bit();
-		if (_data->hasThumbnail()) {
-			if (!_data->isSong()
-				&& !Data::IsExecutableName(_data->filename())) {
-				_data->loadThumbnail(_realParent->fullId());
-				mask |= HistoryDocumentThumbed::Bit();
-			}
+		if (_data->hasThumbnail() && !_data->isSong()) {
+			_data->loadThumbnail(_realParent->fullId());
+			mask |= HistoryDocumentThumbed::Bit();
 		}
 	}
 	if (caption) {
diff --git a/Telegram/SourceFiles/main/main_session_settings.cpp b/Telegram/SourceFiles/main/main_session_settings.cpp
index 239bc7cf6..7806dea9f 100644
--- a/Telegram/SourceFiles/main/main_session_settings.cpp
+++ b/Telegram/SourceFiles/main/main_session_settings.cpp
@@ -140,7 +140,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) {
 	qint32 supportChatsTimeSlice = _supportChatsTimeSlice.current();
 	qint32 appIncludeMutedCounter = app.includeMutedCounter() ? 1 : 0;
 	qint32 appCountUnreadMessages = app.countUnreadMessages() ? 1 : 0;
-	qint32 appExeLaunchWarning = app.exeLaunchWarning() ? 1 : 0;
+	qint32 legacyAppExeLaunchWarning = 1;
 	QByteArray autoDownload;
 	qint32 supportAllSearchResults = _supportAllSearchResults.current() ? 1 : 0;
 	qint32 archiveCollapsed = _archiveCollapsed.current() ? 1 : 0;
@@ -262,7 +262,7 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) {
 			stream >> appCountUnreadMessages;
 		}
 		if (!stream.atEnd()) {
-			stream >> appExeLaunchWarning;
+			stream >> legacyAppExeLaunchWarning;
 		}
 	}
 	if (!stream.atEnd()) {
@@ -509,7 +509,6 @@ void SessionSettings::addFromSerialized(const QByteArray &serialized) {
 		}
 		app.setIncludeMutedCounter(appIncludeMutedCounter == 1);
 		app.setCountUnreadMessages(appCountUnreadMessages == 1);
-		app.setExeLaunchWarning(appExeLaunchWarning == 1);
 		app.setNotifyAboutPinned(appNotifyAboutPinned == 1);
 		app.setLoopAnimatedStickers(appLoopAnimatedStickers == 1);
 		app.setLargeEmoji(appLargeEmoji == 1);
diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp
index 69fc9f292..b742a6436 100644
--- a/Telegram/SourceFiles/overview/overview_layout.cpp
+++ b/Telegram/SourceFiles/overview/overview_layout.cpp
@@ -1514,9 +1514,7 @@ bool Document::iconAnimated() const {
 }
 
 bool Document::withThumb() const {
-	return !songLayout()
-		&& _data->hasThumbnail()
-		&& !Data::IsExecutableName(_data->filename());
+	return !songLayout() && _data->hasThumbnail();
 }
 
 bool Document::updateStatusText() {
diff --git a/Telegram/SourceFiles/settings/settings_privacy_security.cpp b/Telegram/SourceFiles/settings/settings_privacy_security.cpp
index 045457819..4849719ff 100644
--- a/Telegram/SourceFiles/settings/settings_privacy_security.cpp
+++ b/Telegram/SourceFiles/settings/settings_privacy_security.cpp
@@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 #include "ui/toast/toast.h"
 #include "ui/wrap/slide_wrap.h"
 #include "ui/wrap/fade_wrap.h"
+#include "ui/widgets/fields/input_field.h"
 #include "ui/widgets/shadow.h"
 #include "ui/widgets/checkbox.h"
 #include "ui/vertical_list.h"
@@ -114,6 +115,56 @@ void AddPremiumStar(
 	}, badge->lifetime());
 }
 
+void OpenFileConfirmationsBox(not_null<Ui::GenericBox*> box) {
+	box->setTitle(tr::lng_settings_file_confirmations());
+
+	const auto settings = &Core::App().settings();
+	const auto &list = settings->noWarningExtensions();
+	const auto text = QStringList(begin(list), end(list)).join(' ');
+	const auto layout = box->verticalLayout();
+	const auto extensions = box->addRow(
+		object_ptr<Ui::InputField>(
+			box,
+			st::defaultInputField,
+			Ui::InputField::Mode::MultiLine,
+			tr::lng_settings_edit_extensions(),
+			TextWithTags{ text }),
+		st::boxRowPadding + QMargins(0, 0, 0, st::settingsPrivacySkip));
+	Ui::AddDividerText(layout, tr::lng_settings_edit_extensions_about());
+	Ui::AddSkip(layout);
+	const auto ip = layout->add(object_ptr<Ui::SettingsButton>(
+		box,
+		tr::lng_settings_edit_ip_confirm(),
+		st::settingsButtonNoIcon
+	))->toggleOn(rpl::single(settings->ipRevealWarning()));
+	Ui::AddSkip(layout);
+	Ui::AddDividerText(layout, tr::lng_settings_edit_ip_confirm_about());
+
+	box->setFocusCallback([=] {
+		extensions->setFocusFast();
+	});
+
+	box->addButton(tr::lng_settings_save(), [=] {
+		const auto extensionsList = extensions->getLastText()
+			.mid(0, 10240)
+			.split(' ', Qt::SkipEmptyParts)
+			.mid(0, 1024);
+		auto extensions = base::flat_set<QString>(
+			extensionsList.begin(),
+			extensionsList.end());
+		const auto ipRevealWarning = ip->toggled();
+		if (extensions != settings->noWarningExtensions()
+			|| ipRevealWarning != settings->ipRevealWarning()) {
+			settings->setNoWarningExtensions(std::move(extensions));
+			settings->setIpRevealWarning(ipRevealWarning);
+			Core::App().saveSettingsDelayed();
+		}
+		box->closeBox();
+
+	});
+	box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
+}
+
 QString PrivacyBase(Privacy::Key key, const Privacy::Rule &rule) {
 	using Key = Privacy::Key;
 	using Option = Privacy::Option;
@@ -645,6 +696,30 @@ void SetupBotsAndWebsites(
 	});
 
 	Ui::AddSkip(container);
+	Ui::AddDivider(container);
+}
+
+void SetupConfirmationExtensions(
+		not_null<Window::SessionController*> controller,
+		not_null<Ui::VerticalLayout*> container) {
+	if (Core::App().settings().noWarningExtensions().empty()
+		&& Core::App().settings().ipRevealWarning()) {
+		return;
+	}
+
+	Ui::AddSkip(container);
+	Ui::AddSubsectionTitle(container, tr::lng_settings_file_confirmations());
+
+	container->add(object_ptr<Button>(
+		container,
+		tr::lng_settings_edit_extensions(),
+		st::settingsButtonNoIcon
+	))->addClickHandler([=] {
+		controller->show(Box(OpenFileConfirmationsBox));
+	});
+
+	Ui::AddSkip(container);
+	Ui::AddDividerText(container, tr::lng_settings_edit_extensions_about());
 }
 
 void SetupBlockedList(
@@ -996,8 +1071,8 @@ void PrivacySecurity::setupContent(
 	AddDivider(content);
 #endif // !OS_MAC_STORE && !OS_WIN_STORE
 	SetupArchiveAndMute(controller, content);
+	SetupConfirmationExtensions(controller, content);
 	SetupBotsAndWebsites(controller, content);
-	AddDivider(content);
 	SetupSelfDestruction(controller, content, trigger());
 
 	Ui::ResizeFitChild(this, content);