1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "main/main_session_settings.h"
9 
10 #include "chat_helpers/tabbed_selector.h"
11 #include "ui/widgets/input_fields.h"
12 #include "ui/chat/attach/attach_send_files_way.h"
13 #include "window/section_widget.h"
14 #include "support/support_common.h"
15 #include "storage/serialize_common.h"
16 #include "boxes/send_files_box.h"
17 #include "core/application.h"
18 #include "core/core_settings.h"
19 
20 namespace Main {
21 namespace {
22 
23 constexpr auto kLegacyCallsPeerToPeerNobody = 4;
24 constexpr auto kVersionTag = -1;
25 constexpr auto kVersion = 2;
26 constexpr auto kMaxSavedPlaybackPositions = 16;
27 
28 } // namespace
29 
SessionSettings()30 SessionSettings::SessionSettings()
31 : _selectorTab(ChatHelpers::SelectorTab::Emoji)
32 , _supportSwitch(Support::SwitchSettings::Next) {
33 }
34 
serialize() const35 QByteArray SessionSettings::serialize() const {
36 	const auto autoDownload = _autoDownload.serialize();
37 	auto size = sizeof(qint32) * 38;
38 	size += _groupStickersSectionHidden.size() * sizeof(quint64);
39 	size += _mediaLastPlaybackPosition.size() * 2 * sizeof(quint64);
40 	size += Serialize::bytearraySize(autoDownload);
41 	size += sizeof(qint32) + _hiddenPinnedMessages.size() * (sizeof(quint64) + sizeof(qint32));
42 
43 	auto result = QByteArray();
44 	result.reserve(size);
45 	{
46 		QDataStream stream(&result, QIODevice::WriteOnly);
47 		stream.setVersion(QDataStream::Qt_5_1);
48 		stream << qint32(kVersionTag) << qint32(kVersion);
49 		stream << static_cast<qint32>(_selectorTab);
50 		stream << qint32(_groupStickersSectionHidden.size());
51 		for (const auto &peerId : _groupStickersSectionHidden) {
52 			stream << SerializePeerId(peerId);
53 		}
54 		stream << qint32(_supportSwitch);
55 		stream << qint32(_supportFixChatsOrder ? 1 : 0);
56 		stream << qint32(_supportTemplatesAutocomplete ? 1 : 0);
57 		stream << qint32(_supportChatsTimeSlice.current());
58 		stream << autoDownload;
59 		stream << qint32(_supportAllSearchResults.current() ? 1 : 0);
60 		stream << qint32(_archiveCollapsed.current() ? 1 : 0);
61 		stream << qint32(_archiveInMainMenu.current() ? 1 : 0);
62 		stream << qint32(_skipArchiveInSearch.current() ? 1 : 0);
63 		stream << qint32(_mediaLastPlaybackPosition.size());
64 		for (const auto &[id, time] : _mediaLastPlaybackPosition) {
65 			stream << quint64(id) << qint64(time);
66 		}
67 		stream << qint32(0);
68 		stream << qint32(_dialogsFiltersEnabled ? 1 : 0);
69 		stream << qint32(_supportAllSilent ? 1 : 0);
70 		stream << qint32(_photoEditorHintShowsCount);
71 		stream << qint32(_hiddenPinnedMessages.size());
72 		for (const auto &[key, value] : _hiddenPinnedMessages) {
73 			stream << SerializePeerId(key) << qint64(value.bare);
74 		}
75 	}
76 	return result;
77 }
78 
addFromSerialized(const QByteArray & serialized)79 void SessionSettings::addFromSerialized(const QByteArray &serialized) {
80 	if (serialized.isEmpty()) {
81 		return;
82 	}
83 
84 	auto &app = Core::App().settings();
85 
86 	QDataStream stream(serialized);
87 	stream.setVersion(QDataStream::Qt_5_1);
88 	qint32 versionTag = 0;
89 	qint32 version = 0;
90 	qint32 selectorTab = static_cast<qint32>(ChatHelpers::SelectorTab::Emoji);
91 	qint32 appLastSeenWarningSeen = app.lastSeenWarningSeen() ? 1 : 0;
92 	qint32 appTabbedSelectorSectionEnabled = 1;
93 	qint32 legacyTabbedSelectorSectionTooltipShown = 0;
94 	qint32 appFloatPlayerColumn = static_cast<qint32>(Window::Column::Second);
95 	qint32 appFloatPlayerCorner = static_cast<qint32>(RectPart::TopRight);
96 	base::flat_map<QString, QString> appSoundOverrides;
97 	base::flat_set<PeerId> groupStickersSectionHidden;
98 	qint32 appThirdSectionInfoEnabled = 0;
99 	qint32 legacySmallDialogsList = 0;
100 	float64 appDialogsWidthRatio = app.dialogsWidthRatio();
101 	int appThirdColumnWidth = app.thirdColumnWidth();
102 	int appThirdSectionExtendedBy = app.thirdSectionExtendedBy();
103 	qint32 appSendFilesWay = app.sendFilesWay().serialize();
104 	qint32 legacyCallsPeerToPeer = qint32(0);
105 	qint32 appSendSubmitWay = static_cast<qint32>(app.sendSubmitWay());
106 	qint32 supportSwitch = static_cast<qint32>(_supportSwitch);
107 	qint32 supportFixChatsOrder = _supportFixChatsOrder ? 1 : 0;
108 	qint32 supportTemplatesAutocomplete = _supportTemplatesAutocomplete ? 1 : 0;
109 	qint32 supportChatsTimeSlice = _supportChatsTimeSlice.current();
110 	qint32 appIncludeMutedCounter = app.includeMutedCounter() ? 1 : 0;
111 	qint32 appCountUnreadMessages = app.countUnreadMessages() ? 1 : 0;
112 	qint32 appExeLaunchWarning = app.exeLaunchWarning() ? 1 : 0;
113 	QByteArray autoDownload;
114 	qint32 supportAllSearchResults = _supportAllSearchResults.current() ? 1 : 0;
115 	qint32 archiveCollapsed = _archiveCollapsed.current() ? 1 : 0;
116 	qint32 appNotifyAboutPinned = app.notifyAboutPinned() ? 1 : 0;
117 	qint32 archiveInMainMenu = _archiveInMainMenu.current() ? 1 : 0;
118 	qint32 skipArchiveInSearch = _skipArchiveInSearch.current() ? 1 : 0;
119 	qint32 legacyAutoplayGifs = 1;
120 	qint32 appLoopAnimatedStickers = app.loopAnimatedStickers() ? 1 : 0;
121 	qint32 appLargeEmoji = app.largeEmoji() ? 1 : 0;
122 	qint32 appReplaceEmoji = app.replaceEmoji() ? 1 : 0;
123 	qint32 appSuggestEmoji = app.suggestEmoji() ? 1 : 0;
124 	qint32 appSuggestStickersByEmoji = app.suggestStickersByEmoji() ? 1 : 0;
125 	qint32 appSpellcheckerEnabled = app.spellcheckerEnabled() ? 1 : 0;
126 	std::vector<std::pair<DocumentId, crl::time>> mediaLastPlaybackPosition;
127 	qint32 appVideoPlaybackSpeed = Core::Settings::SerializePlaybackSpeed(app.videoPlaybackSpeed());
128 	QByteArray appVideoPipGeometry = app.videoPipGeometry();
129 	std::vector<int> appDictionariesEnabled;
130 	qint32 appAutoDownloadDictionaries = app.autoDownloadDictionaries() ? 1 : 0;
131 	base::flat_map<PeerId, MsgId> hiddenPinnedMessages;
132 	qint32 dialogsFiltersEnabled = _dialogsFiltersEnabled ? 1 : 0;
133 	qint32 supportAllSilent = _supportAllSilent ? 1 : 0;
134 	qint32 photoEditorHintShowsCount = _photoEditorHintShowsCount;
135 
136 	stream >> versionTag;
137 	if (versionTag == kVersionTag) {
138 		stream >> version;
139 		stream >> selectorTab;
140 	} else {
141 		selectorTab = versionTag;
142 	}
143 	if (version < 2) {
144 		stream >> appLastSeenWarningSeen;
145 		if (!stream.atEnd()) {
146 			stream >> appTabbedSelectorSectionEnabled;
147 		}
148 		if (!stream.atEnd()) {
149 			auto count = qint32(0);
150 			stream >> count;
151 			if (stream.status() == QDataStream::Ok) {
152 				for (auto i = 0; i != count; ++i) {
153 					QString key, value;
154 					stream >> key >> value;
155 					if (stream.status() != QDataStream::Ok) {
156 						LOG(("App Error: "
157 							"Bad data for SessionSettings::addFromSerialized()"));
158 						return;
159 					}
160 					appSoundOverrides.emplace(key, value);
161 				}
162 			}
163 		}
164 		if (!stream.atEnd()) {
165 			stream >> legacyTabbedSelectorSectionTooltipShown;
166 		}
167 		if (!stream.atEnd()) {
168 			stream >> appFloatPlayerColumn >> appFloatPlayerCorner;
169 		}
170 	}
171 	if (!stream.atEnd()) {
172 		auto count = qint32(0);
173 		stream >> count;
174 		if (stream.status() == QDataStream::Ok) {
175 			for (auto i = 0; i != count; ++i) {
176 				quint64 peerId;
177 				stream >> peerId;
178 				if (stream.status() != QDataStream::Ok) {
179 					LOG(("App Error: "
180 						"Bad data for SessionSettings::addFromSerialized()"));
181 					return;
182 				}
183 				groupStickersSectionHidden.emplace(
184 					DeserializePeerId(peerId));
185 			}
186 		}
187 	}
188 	if (version < 2) {
189 		if (!stream.atEnd()) {
190 			stream >> appThirdSectionInfoEnabled;
191 			stream >> legacySmallDialogsList;
192 		}
193 		if (!stream.atEnd()) {
194 			qint32 value = 0;
195 			stream >> value;
196 			appDialogsWidthRatio = std::clamp(value / 1000000., 0., 1.);
197 
198 			stream >> value;
199 			appThirdColumnWidth = value;
200 
201 			stream >> value;
202 			appThirdSectionExtendedBy = value;
203 		}
204 		if (!stream.atEnd()) {
205 			stream >> appSendFilesWay;
206 		}
207 		if (!stream.atEnd()) {
208 			stream >> legacyCallsPeerToPeer;
209 		}
210 	}
211 	if (!stream.atEnd()) {
212 		if (version < 2) {
213 			stream >> appSendSubmitWay;
214 		}
215 		stream >> supportSwitch;
216 		stream >> supportFixChatsOrder;
217 	}
218 	if (!stream.atEnd()) {
219 		stream >> supportTemplatesAutocomplete;
220 	}
221 	if (!stream.atEnd()) {
222 		stream >> supportChatsTimeSlice;
223 	}
224 	if (version < 2) {
225 		if (!stream.atEnd()) {
226 			stream >> appIncludeMutedCounter;
227 			stream >> appCountUnreadMessages;
228 		}
229 		if (!stream.atEnd()) {
230 			stream >> appExeLaunchWarning;
231 		}
232 	}
233 	if (!stream.atEnd()) {
234 		stream >> autoDownload;
235 	}
236 	if (!stream.atEnd()) {
237 		stream >> supportAllSearchResults;
238 	}
239 	if (!stream.atEnd()) {
240 		stream >> archiveCollapsed;
241 	}
242 	if (version < 2) {
243 		if (!stream.atEnd()) {
244 			stream >> appNotifyAboutPinned;
245 		}
246 	}
247 	if (!stream.atEnd()) {
248 		stream >> archiveInMainMenu;
249 	}
250 	if (!stream.atEnd()) {
251 		stream >> skipArchiveInSearch;
252 	}
253 	if (version < 2) {
254 		if (!stream.atEnd()) {
255 			stream >> legacyAutoplayGifs;
256 			stream >> appLoopAnimatedStickers;
257 			stream >> appLargeEmoji;
258 			stream >> appReplaceEmoji;
259 			stream >> appSuggestEmoji;
260 			stream >> appSuggestStickersByEmoji;
261 		}
262 		if (!stream.atEnd()) {
263 			stream >> appSpellcheckerEnabled;
264 		}
265 	}
266 	if (!stream.atEnd()) {
267 		auto count = qint32(0);
268 		stream >> count;
269 		if (stream.status() == QDataStream::Ok) {
270 			for (auto i = 0; i != count; ++i) {
271 				quint64 documentId;
272 				qint64 time;
273 				stream >> documentId >> time;
274 				if (stream.status() != QDataStream::Ok) {
275 					LOG(("App Error: "
276 						"Bad data for SessionSettings::addFromSerialized()"));
277 					return;
278 				}
279 				mediaLastPlaybackPosition.emplace_back(documentId, time);
280 			}
281 		}
282 	}
283 	if (version < 2) {
284 		if (!stream.atEnd()) {
285 			stream >> appVideoPlaybackSpeed;
286 		}
287 		if (!stream.atEnd()) {
288 			stream >> appVideoPipGeometry;
289 		}
290 		if (!stream.atEnd()) {
291 			auto count = qint32(0);
292 			stream >> count;
293 			if (stream.status() == QDataStream::Ok) {
294 				for (auto i = 0; i != count; ++i) {
295 					qint64 langId;
296 					stream >> langId;
297 					if (stream.status() != QDataStream::Ok) {
298 						LOG(("App Error: "
299 							"Bad data for SessionSettings::addFromSerialized()"));
300 						return;
301 					}
302 					appDictionariesEnabled.emplace_back(langId);
303 				}
304 			}
305 		}
306 		if (!stream.atEnd()) {
307 			stream >> appAutoDownloadDictionaries;
308 		}
309 	}
310 	if (!stream.atEnd()) {
311 		auto count = qint32(0);
312 		stream >> count;
313 		if (stream.status() == QDataStream::Ok) {
314 			// Legacy.
315 			for (auto i = 0; i != count; ++i) {
316 				auto key = quint64();
317 				auto value = qint32();
318 				stream >> key >> value;
319 				if (stream.status() != QDataStream::Ok) {
320 					LOG(("App Error: "
321 						"Bad data for SessionSettings::addFromSerialized()"));
322 					return;
323 				}
324 				hiddenPinnedMessages.emplace(DeserializePeerId(key), value);
325 			}
326 		}
327 	}
328 	if (!stream.atEnd()) {
329 		stream >> dialogsFiltersEnabled;
330 	}
331 	if (!stream.atEnd()) {
332 		stream >> supportAllSilent;
333 	}
334 	if (!stream.atEnd()) {
335 		stream >> photoEditorHintShowsCount;
336 	}
337 	if (!stream.atEnd()) {
338 		auto count = qint32(0);
339 		stream >> count;
340 		if (stream.status() == QDataStream::Ok) {
341 			for (auto i = 0; i != count; ++i) {
342 				auto key = quint64();
343 				auto value = qint64();
344 				stream >> key >> value;
345 				if (stream.status() != QDataStream::Ok) {
346 					LOG(("App Error: "
347 						"Bad data for SessionSettings::addFromSerialized()"));
348 					return;
349 				}
350 				hiddenPinnedMessages.emplace(DeserializePeerId(key), value);
351 			}
352 		}
353 	}
354 	if (stream.status() != QDataStream::Ok) {
355 		LOG(("App Error: "
356 			"Bad data for SessionSettings::addFromSerialized()"));
357 		return;
358 	}
359 	if (!autoDownload.isEmpty()
360 		&& !_autoDownload.setFromSerialized(autoDownload)) {
361 		return;
362 	}
363 	if (!version) {
364 		if (!legacyAutoplayGifs) {
365 			using namespace Data::AutoDownload;
366 			_autoDownload = WithDisabledAutoPlay(_autoDownload);
367 		}
368 	}
369 
370 	auto uncheckedTab = static_cast<ChatHelpers::SelectorTab>(selectorTab);
371 	switch (uncheckedTab) {
372 	case ChatHelpers::SelectorTab::Emoji:
373 	case ChatHelpers::SelectorTab::Stickers:
374 	case ChatHelpers::SelectorTab::Gifs: _selectorTab = uncheckedTab; break;
375 	}
376 	_groupStickersSectionHidden = std::move(groupStickersSectionHidden);
377 	auto uncheckedSupportSwitch = static_cast<Support::SwitchSettings>(
378 		supportSwitch);
379 	switch (uncheckedSupportSwitch) {
380 	case Support::SwitchSettings::None:
381 	case Support::SwitchSettings::Next:
382 	case Support::SwitchSettings::Previous: _supportSwitch = uncheckedSupportSwitch; break;
383 	}
384 	_supportFixChatsOrder = (supportFixChatsOrder == 1);
385 	_supportTemplatesAutocomplete = (supportTemplatesAutocomplete == 1);
386 	_supportChatsTimeSlice = supportChatsTimeSlice;
387 	_hadLegacyCallsPeerToPeerNobody = (legacyCallsPeerToPeer == kLegacyCallsPeerToPeerNobody);
388 	_supportAllSearchResults = (supportAllSearchResults == 1);
389 	_archiveCollapsed = (archiveCollapsed == 1);
390 	_archiveInMainMenu = (archiveInMainMenu == 1);
391 	_skipArchiveInSearch = (skipArchiveInSearch == 1);
392 	_mediaLastPlaybackPosition = std::move(mediaLastPlaybackPosition);
393 	_hiddenPinnedMessages = std::move(hiddenPinnedMessages);
394 	_dialogsFiltersEnabled = (dialogsFiltersEnabled == 1);
395 	_supportAllSilent = (supportAllSilent == 1);
396 	_photoEditorHintShowsCount = std::move(photoEditorHintShowsCount);
397 
398 	if (version < 2) {
399 		app.setLastSeenWarningSeen(appLastSeenWarningSeen == 1);
400 		for (const auto &[key, value] : appSoundOverrides) {
401 			app.setSoundOverride(key, value);
402 		}
403 		if (const auto sendFilesWay = Ui::SendFilesWay::FromSerialized(appSendFilesWay)) {
404 			app.setSendFilesWay(*sendFilesWay);
405 		}
406 		auto uncheckedSendSubmitWay = static_cast<Ui::InputSubmitSettings>(
407 			appSendSubmitWay);
408 		switch (uncheckedSendSubmitWay) {
409 		case Ui::InputSubmitSettings::Enter:
410 		case Ui::InputSubmitSettings::CtrlEnter: app.setSendSubmitWay(uncheckedSendSubmitWay); break;
411 		}
412 		app.setIncludeMutedCounter(appIncludeMutedCounter == 1);
413 		app.setCountUnreadMessages(appCountUnreadMessages == 1);
414 		app.setExeLaunchWarning(appExeLaunchWarning == 1);
415 		app.setNotifyAboutPinned(appNotifyAboutPinned == 1);
416 		app.setLoopAnimatedStickers(appLoopAnimatedStickers == 1);
417 		app.setLargeEmoji(appLargeEmoji == 1);
418 		app.setReplaceEmoji(appReplaceEmoji == 1);
419 		app.setSuggestEmoji(appSuggestEmoji == 1);
420 		app.setSuggestStickersByEmoji(appSuggestStickersByEmoji == 1);
421 		app.setSpellcheckerEnabled(appSpellcheckerEnabled == 1);
422 		app.setVideoPlaybackSpeed(Core::Settings::DeserializePlaybackSpeed(appVideoPlaybackSpeed));
423 		app.setVideoPipGeometry(appVideoPipGeometry);
424 		app.setDictionariesEnabled(std::move(appDictionariesEnabled));
425 		app.setAutoDownloadDictionaries(appAutoDownloadDictionaries == 1);
426 		app.setTabbedSelectorSectionEnabled(appTabbedSelectorSectionEnabled == 1);
427 		auto uncheckedColumn = static_cast<Window::Column>(appFloatPlayerColumn);
428 		switch (uncheckedColumn) {
429 		case Window::Column::First:
430 		case Window::Column::Second:
431 		case Window::Column::Third: app.setFloatPlayerColumn(uncheckedColumn); break;
432 		}
433 		auto uncheckedCorner = static_cast<RectPart>(appFloatPlayerCorner);
434 		switch (uncheckedCorner) {
435 		case RectPart::TopLeft:
436 		case RectPart::TopRight:
437 		case RectPart::BottomLeft:
438 		case RectPart::BottomRight: app.setFloatPlayerCorner(uncheckedCorner); break;
439 		}
440 		app.setThirdSectionInfoEnabled(appThirdSectionInfoEnabled);
441 		app.setDialogsWidthRatio(appDialogsWidthRatio);
442 		app.setThirdColumnWidth(appThirdColumnWidth);
443 		app.setThirdSectionExtendedBy(appThirdSectionExtendedBy);
444 	}
445 }
446 
setSupportChatsTimeSlice(int slice)447 void SessionSettings::setSupportChatsTimeSlice(int slice) {
448 	_supportChatsTimeSlice = slice;
449 }
450 
supportChatsTimeSlice() const451 int SessionSettings::supportChatsTimeSlice() const {
452 	return _supportChatsTimeSlice.current();
453 }
454 
supportChatsTimeSliceValue() const455 rpl::producer<int> SessionSettings::supportChatsTimeSliceValue() const {
456 	return _supportChatsTimeSlice.value();
457 }
458 
setSupportAllSearchResults(bool all)459 void SessionSettings::setSupportAllSearchResults(bool all) {
460 	_supportAllSearchResults = all;
461 }
462 
supportAllSearchResults() const463 bool SessionSettings::supportAllSearchResults() const {
464 	return _supportAllSearchResults.current();
465 }
466 
supportAllSearchResultsValue() const467 rpl::producer<bool> SessionSettings::supportAllSearchResultsValue() const {
468 	return _supportAllSearchResults.value();
469 }
470 
setMediaLastPlaybackPosition(DocumentId id,crl::time time)471 void SessionSettings::setMediaLastPlaybackPosition(DocumentId id, crl::time time) {
472 	auto &map = _mediaLastPlaybackPosition;
473 	const auto i = ranges::find(
474 		map,
475 		id,
476 		&std::pair<DocumentId, crl::time>::first);
477 	if (i != map.end()) {
478 		if (time > 0) {
479 			i->second = time;
480 		} else {
481 			map.erase(i);
482 		}
483 	} else if (time > 0) {
484 		if (map.size() >= kMaxSavedPlaybackPositions) {
485 			map.erase(map.begin());
486 		}
487 		map.emplace_back(id, time);
488 	}
489 }
490 
mediaLastPlaybackPosition(DocumentId id) const491 crl::time SessionSettings::mediaLastPlaybackPosition(DocumentId id) const {
492 	const auto i = ranges::find(
493 		_mediaLastPlaybackPosition,
494 		id,
495 		&std::pair<DocumentId, crl::time>::first);
496 	return (i != _mediaLastPlaybackPosition.end()) ? i->second : 0;
497 }
498 
setArchiveCollapsed(bool collapsed)499 void SessionSettings::setArchiveCollapsed(bool collapsed) {
500 	_archiveCollapsed = collapsed;
501 }
502 
archiveCollapsed() const503 bool SessionSettings::archiveCollapsed() const {
504 	return _archiveCollapsed.current();
505 }
506 
archiveCollapsedChanges() const507 rpl::producer<bool> SessionSettings::archiveCollapsedChanges() const {
508 	return _archiveCollapsed.changes();
509 }
510 
setArchiveInMainMenu(bool inMainMenu)511 void SessionSettings::setArchiveInMainMenu(bool inMainMenu) {
512 	_archiveInMainMenu = inMainMenu;
513 }
514 
archiveInMainMenu() const515 bool SessionSettings::archiveInMainMenu() const {
516 	return _archiveInMainMenu.current();
517 }
518 
archiveInMainMenuChanges() const519 rpl::producer<bool> SessionSettings::archiveInMainMenuChanges() const {
520 	return _archiveInMainMenu.changes();
521 }
522 
setSkipArchiveInSearch(bool skip)523 void SessionSettings::setSkipArchiveInSearch(bool skip) {
524 	_skipArchiveInSearch = skip;
525 }
526 
skipArchiveInSearch() const527 bool SessionSettings::skipArchiveInSearch() const {
528 	return _skipArchiveInSearch.current();
529 }
530 
skipArchiveInSearchChanges() const531 rpl::producer<bool> SessionSettings::skipArchiveInSearchChanges() const {
532 	return _skipArchiveInSearch.changes();
533 }
534 
photoEditorHintShown() const535 bool SessionSettings::photoEditorHintShown() const {
536 	return _photoEditorHintShowsCount < kPhotoEditorHintMaxShowsCount;
537 }
538 
incrementPhotoEditorHintShown()539 void SessionSettings::incrementPhotoEditorHintShown() {
540 	if (photoEditorHintShown()) {
541 		_photoEditorHintShowsCount++;
542 	}
543 }
544 
545 } // namespace Main
546