From 6557471697522cffa4fcfce1814d946f307e0582 Mon Sep 17 00:00:00 2001 From: Harry Bond Date: Tue, 28 Jan 2025 19:21:51 +0000 Subject: [PATCH 1/4] [generator][core] Add Mastodon and Bluesky Signed-off-by: Harry Bond --- generator/osm2meta.cpp | 2 + indexer/feature_meta.cpp | 6 + indexer/feature_meta.hpp | 2 + .../validate_and_format_contacts_test.cpp | 66 ++++++ indexer/validate_and_format_contacts.cpp | 188 +++++++++++++++++- indexer/validate_and_format_contacts.hpp | 4 + 6 files changed, 257 insertions(+), 11 deletions(-) diff --git a/generator/osm2meta.cpp b/generator/osm2meta.cpp index 216b379c70..e92a93a85d 100644 --- a/generator/osm2meta.cpp +++ b/generator/osm2meta.cpp @@ -534,6 +534,8 @@ void MetadataTagProcessor::operator()(std::string const & k, std::string const & case Metadata::FMD_CONTACT_TWITTER: valid = osm::ValidateAndFormat_twitter(v); break; case Metadata::FMD_CONTACT_VK: valid = osm::ValidateAndFormat_vk(v); break; case Metadata::FMD_CONTACT_LINE: valid = osm::ValidateAndFormat_contactLine(v); break; + case Metadata::FMD_CONTACT_FEDIVERSE: valid = osm::ValidateAndFormat_fediverse(v); break; + case Metadata::FMD_CONTACT_BLUESKY: valid = osm::ValidateAndFormat_bluesky(v); break; case Metadata::FMD_INTERNET: valid = ValidateAndFormat_internet(v); break; case Metadata::FMD_ELE: valid = ValidateAndFormat_ele(v); break; case Metadata::FMD_DESTINATION: valid = ValidateAndFormat_destination(v); break; diff --git a/indexer/feature_meta.cpp b/indexer/feature_meta.cpp index 983492cffe..c8057befea 100644 --- a/indexer/feature_meta.cpp +++ b/indexer/feature_meta.cpp @@ -95,6 +95,10 @@ bool Metadata::TypeFromString(string_view k, Metadata::EType & outType) outType = Metadata::FMD_CONTACT_VK; else if (k == "contact:line") outType = Metadata::FMD_CONTACT_LINE; + else if (k == "contact:mastodon") + outType = Metadata::FMD_CONTACT_FEDIVERSE; + else if (k == "contact:bluesky") + outType = Metadata::FMD_CONTACT_BLUESKY; else if (k == "internet_access" || k == "wifi") outType = Metadata::FMD_INTERNET; else if (k == "ele") @@ -264,6 +268,8 @@ string ToString(Metadata::EType type) case Metadata::FMD_CONTACT_TWITTER: return "contact:twitter"; case Metadata::FMD_CONTACT_VK: return "contact:vk"; case Metadata::FMD_CONTACT_LINE: return "contact:line"; + case Metadata::FMD_CONTACT_FEDIVERSE: return "contact:mastodon"; + case Metadata::FMD_CONTACT_BLUESKY: return "contact:bluesky"; case Metadata::FMD_DESTINATION: return "destination"; case Metadata::FMD_DESTINATION_REF: return "destination:ref"; case Metadata::FMD_JUNCTION_REF: return "junction:ref"; diff --git a/indexer/feature_meta.hpp b/indexer/feature_meta.hpp index 17312b69a2..ad024ebd0e 100644 --- a/indexer/feature_meta.hpp +++ b/indexer/feature_meta.hpp @@ -155,6 +155,8 @@ public: FMD_SELF_SERVICE = 47, FMD_OUTDOOR_SEATING = 48, FMD_NETWORK = 49, + FMD_CONTACT_FEDIVERSE = 50, + FMD_CONTACT_BLUESKY = 51, FMD_COUNT }; diff --git a/indexer/indexer_tests/validate_and_format_contacts_test.cpp b/indexer/indexer_tests/validate_and_format_contacts_test.cpp index 54ffd1a030..2896c52d85 100644 --- a/indexer/indexer_tests/validate_and_format_contacts_test.cpp +++ b/indexer/indexer_tests/validate_and_format_contacts_test.cpp @@ -108,6 +108,39 @@ UNIT_TEST(EditableMapObject_ValidateAndFormat_contactLine) TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://line.com/ti/p/invalid-domain"), "", ()); } +UNIT_TEST(EditableMapObject_ValidateAndFormat_fediverse) +{ + TEST_EQUAL(osm::ValidateAndFormat_fediverse("https://fosstodon.org/@organicmaps"), "organicmaps@fosstodon.org", ()); + TEST_EQUAL(osm::ValidateAndFormat_fediverse("https://fosstodon.org/users/organicmaps"), "organicmaps@fosstodon.org", ()); + TEST_EQUAL(osm::ValidateAndFormat_fediverse("http://fosstodon.org/users/organicmaps"), "organicmaps@fosstodon.org", ()); + TEST_EQUAL(osm::ValidateAndFormat_fediverse("fosstodon.org/users/organicmaps"), "organicmaps@fosstodon.org", ()); + TEST_EQUAL(osm::ValidateAndFormat_fediverse("organicmaps@fosstodon.org"), "organicmaps@fosstodon.org", ()); + TEST_EQUAL(osm::ValidateAndFormat_fediverse("@organicmaps@fosstodon.org"), "organicmaps@fosstodon.org", ()); + TEST_EQUAL(osm::ValidateAndFormat_fediverse("@organicmaps@fosstodon.org.uk"), "organicmaps@fosstodon.org.uk", ()); + TEST_EQUAL(osm::ValidateAndFormat_fediverse("pub.mastodon.org.uk/@organicmaps"), "organicmaps@pub.mastodon.org.uk", ()); + TEST_EQUAL(osm::ValidateAndFormat_fediverse("pub.mastodon.org.uk/users/@organicmaps"), "organicmaps@pub.mastodon.org.uk", ()); + + TEST_EQUAL(osm::ValidateAndFormat_fediverse("organicmaps@fosstodon@mastodon.org"), "", ()); + TEST_EQUAL(osm::ValidateAndFormat_fediverse("orga$nicmaps@mastodon.social"), "", ()); + TEST_EQUAL(osm::ValidateAndFormat_fediverse("pub.mastodon.org.uk/organicmaps"), "", ()); + TEST_EQUAL(osm::ValidateAndFormat_fediverse("pub.mastodon.org.uk/users/"), "", ()); +} + +UNIT_TEST(EditableMapObject_ValidateAndFormat_bluesky) +{ + TEST_EQUAL(osm::ValidateAndFormat_bluesky("organicmaps.bsky.social"), "organicmaps.bsky.social", ()); + TEST_EQUAL(osm::ValidateAndFormat_bluesky("@organicmaps.bsky.social"), "organicmaps.bsky.social", ()); + TEST_EQUAL(osm::ValidateAndFormat_bluesky("https://bsky.app/profile/organicmaps.bsky.social"), "organicmaps.bsky.social", ()); + TEST_EQUAL(osm::ValidateAndFormat_bluesky("https://bsky.app/profile/@organicmaps.bsky.social"), "organicmaps.bsky.social", ()); + TEST_EQUAL(osm::ValidateAndFormat_bluesky("http://bsky.app/profile/organicmaps.bsky.social"), "organicmaps.bsky.social", ()); + TEST_EQUAL(osm::ValidateAndFormat_bluesky("bsky.app/profile/organicmaps.bsky.social"), "organicmaps.bsky.social", ()); + TEST_EQUAL(osm::ValidateAndFormat_bluesky("https://bsky.app/profile/organicmaps.bsky.social"), "organicmaps.bsky.social", ()); + + TEST_EQUAL(osm::ValidateAndFormat_bluesky("https://bsky.app/profile/organicmap$.bsky.social"), "", ()); + TEST_EQUAL(osm::ValidateAndFormat_bluesky("https://bsky.app/profile/organicmaps.bsky.social$"), "", ()); + TEST_EQUAL(osm::ValidateAndFormat_bluesky("https://bsky.app/pineapple/organicmaps.bsky.social"), "", ()); +} + UNIT_TEST(EditableMapObject_ValidateFacebookPage) { TEST(osm::ValidateFacebookPage(""), ()); @@ -262,6 +295,39 @@ UNIT_TEST(EditableMapObject_ValidateLinePage) TEST(!osm::ValidateLinePage("https://line.com/ti/p/invalid-domain"), ()); } +UNIT_TEST(EditableMapObject_ValidateFediversePage) +{ + TEST(osm::ValidateFediversePage("https://fosstodon.org/@organicmaps"), ()); + TEST(osm::ValidateFediversePage("https://fosstodon.org/users/organicmaps"), ()); + TEST(osm::ValidateFediversePage("http://fosstodon.org/users/organicmaps"), ()); + TEST(osm::ValidateFediversePage("fosstodon.org/users/organicmaps"), ()); + TEST(osm::ValidateFediversePage("organicmaps@fosstodon.org"), ()); + TEST(osm::ValidateFediversePage("@organicmaps@fosstodon.org"), ()); + TEST(osm::ValidateFediversePage("@organicmaps@fosstodon.org.uk"), ()); + TEST(osm::ValidateFediversePage("pub.mastodon.org.uk/@organicmaps"), ()); + TEST(osm::ValidateFediversePage("pub.mastodon.org.uk/users/@organicmaps"), ()); + + TEST(!osm::ValidateFediversePage("organicmaps@fosstodon@mastodon.org"), ()); + TEST(!osm::ValidateFediversePage("orga$nicmaps@mastodon.social"), ()); + TEST(!osm::ValidateFediversePage("pub.mastodon.org.uk/organicmaps"), ()); + TEST(!osm::ValidateFediversePage("pub.mastodon.org.uk/users/"), ()); +} + +UNIT_TEST(EditableMapObject_ValidateBlueskyPage) +{ + TEST(osm::ValidateBlueskyPage("organicmaps.bsky.social"), ()); + TEST(osm::ValidateBlueskyPage("@organicmaps.bsky.social"), ()); + TEST(osm::ValidateBlueskyPage("https://bsky.app/profile/organicmaps.bsky.social"), ()); + TEST(osm::ValidateBlueskyPage("https://bsky.app/profile/@organicmaps.bsky.social"), ()); + TEST(osm::ValidateBlueskyPage("http://bsky.app/profile/organicmaps.bsky.social"), ()); + TEST(osm::ValidateBlueskyPage("bsky.app/profile/organicmaps.bsky.social"), ()); + TEST(osm::ValidateBlueskyPage("https://bsky.app/profile/organicmaps.bsky.social"), ()); + + TEST(!osm::ValidateBlueskyPage("https://bsky.app/profile/organicmap$.bsky.social"), ()); + TEST(!osm::ValidateBlueskyPage("https://bsky.app/profile/organicmaps.bsky.social$"), ()); + TEST(!osm::ValidateBlueskyPage("https://bsky.app/pineapple/organicmaps.bsky.social"), ()); +} + UNIT_TEST(EditableMapObject_socialContactToURL) { TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_INSTAGRAM, "some_page_name"), "https://instagram.com/some_page_name", ()); diff --git a/indexer/validate_and_format_contacts.cpp b/indexer/validate_and_format_contacts.cpp index 3904ae5803..7defbe2740 100644 --- a/indexer/validate_and_format_contacts.cpp +++ b/indexer/validate_and_format_contacts.cpp @@ -16,12 +16,16 @@ static auto const s_twitterRegex = regex(R"(^@?[A-Za-z0-9_]{1,15}$)"); static auto const s_badVkRegex = regex(R"(^\d\d\d.+$)"); static auto const s_goodVkRegex = regex(R"(^[A-Za-z0-9_.]{5,32}$)"); static auto const s_lineRegex = regex(R"(^[a-z0-9-_.]{4,20}$)"); +static auto const s_fediverseRegex = regex(R"(^@?[a-zA-Z0-9_]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$)"); +static auto const s_blueskyRegex = regex(R"(^@?[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$)"); constexpr string_view kFacebook{"contact:facebook"}; constexpr string_view kInstagram{"contact:instagram"}; constexpr string_view kTwitter{"contact:twitter"}; constexpr string_view kVk{"contact:vk"}; constexpr string_view kLine{"contact:line"}; +constexpr string_view kFediverse{"contact:mastodon"}; +constexpr string_view kBluesky{"contact:bluesky"}; constexpr string_view kProfilePhp{"profile.php"}; @@ -41,6 +45,7 @@ constexpr string_view kDotVkontakteRu{".vkontakte.ru"}; constexpr string_view kLineMe{"line.me"}; constexpr string_view kPageLineMe{"page.line.me"}; constexpr string_view kDotLineMe{".line.me"}; +constexpr string_view kBskyApp{"bsky.app"}; // URLs constants constexpr string_view kUrlFacebook{"https://facebook.com/"}; @@ -48,6 +53,7 @@ constexpr string_view kUrlInstagram{"https://instagram.com/"}; constexpr string_view kUrlTwitter{"https://twitter.com/"}; constexpr string_view kUrlVk{"https://vk.com/"}; constexpr string_view kUrlLine{"https://line.me/R/ti/p/@"}; +constexpr string_view kUrlBluesky{"https://bsky.app/profile/"}; constexpr string_view kHttp{"http://"}; constexpr string_view kHttps{"https://"}; @@ -65,6 +71,13 @@ bool IsProtocolSpecified(string const & website) return 0 != GetProtocolNameLength(website); } +string fediverseHandleToUrl(string_view handle) +{ + // Convert stored username@domain.name to https://domain.name/username + vector const handleElements = strings::Tokenize(handle, "@"); + return string{kHttps}.append(handleElements[1]).append("/@").append(handleElements[0]); +} + // TODO: Current implementation looks only for restricted symbols from ASCII block ignoring // unicode. Need to find all restricted *Unicode* symbols // from https://www.facebook.com/pages/create page and verify those symbols @@ -239,16 +252,16 @@ string ValidateAndFormat_vk(string const & vkPage) return {}; } -// Strip '%40' and `@` chars from Line ID start. -string stripAtSymbol(string const & lineId) +// Strip '%40' and `@` chars from string start if they exist. +string stripAtSymbol(string const & inputString) { - if (lineId.empty()) - return lineId; - if (lineId.front() == '@') - return lineId.substr(1); - if (lineId.starts_with("%40")) - return lineId.substr(3); - return lineId; + if (inputString.empty()) + return inputString; + if (inputString.front() == '@') + return inputString.substr(1); + if (inputString.starts_with("%40")) + return inputString.substr(3); + return inputString; } string ValidateAndFormat_contactLine(string const & linePage) @@ -318,6 +331,79 @@ string ValidateAndFormat_contactLine(string const & linePage) return {}; } +string ValidateAndFormat_fediverse(string const & fediPage) +{ + if (fediPage.empty()) + return {}; + + // Parse {@?}{username}@{domain.name} format + if (regex_match(fediPage, s_fediverseRegex)) + return stripAtSymbol(fediPage); + + // If it doesn't match the above format, it can only be an URL format. + if (!ValidateWebsite(fediPage)) + return {}; + + // Parse https://{domain.name}{@ || /users/}{username} formats + url::Url const parsedUrl = url::Url::FromString(fediPage); + string const parsedDomain = strings::MakeLowerCase(parsedUrl.GetHost()); + string path = parsedUrl.GetPath(); + path.erase(path.find_last_not_of('/') + 1); // Strip any trailing '/' symbol + + // Could be /users/ type - check and remove to be left with just username. + if (path.starts_with("users/")) // first slash is already removed by GetPath() + { + path.erase(0, 6); + path = stripAtSymbol(path); // handle technically wrong but parseable domain/users/@username + } + // domain.name/@username - username has to start with @ + else if (path.starts_with("@")) + path = stripAtSymbol(path); + // unknown/invalid format + else + return {}; + + // Then construct the final username@domain.name format + path.append("@").append(parsedDomain); + // and make sure it's valid + if (regex_match(path, s_fediverseRegex)) + return path; + else + return {}; +} + +string ValidateAndFormat_bluesky(string const & bskyPage) +{ + if (bskyPage.empty()) + return {}; + + // Try matching {@?}{user/domain.name} format to avoid doing the other stuff + if (regex_match(bskyPage, s_blueskyRegex)) + return stripAtSymbol(bskyPage); + + // If not, it must match the URL format + if (ValidateWebsite(bskyPage)) + { + // Match https://bsky.app/profile/{username/domain.name} + url::Url const pageUrl = url::Url::FromString(bskyPage); + string_view const domain = pageUrl.GetHost(); + string path = pageUrl.GetPath(); + + // First remove url bits if they exist + if (domain.starts_with(kBskyApp) && path.starts_with("profile/")) + { + path.erase(0, 8); // Strip "profile/" part + path.erase(path.find_last_not_of('/') + 1); // Strip last '/' symbol if exists + + // Then make sure it matches {@?}{user/domain.name} + if (regex_match(path, s_blueskyRegex)) + return stripAtSymbol(path); + } + } + + return {}; +} + bool ValidateWebsite(string const & site) { if (site.empty()) @@ -449,9 +535,79 @@ bool ValidateLinePage(string const & page) return (domain == kLineMe || domain.ends_with(kDotLineMe)); } +bool ValidateFediversePage(string const & page) +{ + if (page.empty()) + return true; + + // Match @username@instance.name format + if (regex_match(page, s_fediverseRegex)) + return true; + + // If it doesn't match the above format, it can only be an URL format. + if (!ValidateWebsite(page)) + return false; + + // Try to match https://{domain.name}{@ || /users/}{username} formats + url::Url const pageUrl = url::Url::FromString(page); + string_view const domain = pageUrl.GetHost(); + string path = pageUrl.GetPath(); + + // Could be /users/ type - check and remove to be left with just username. + if (path.starts_with("users/")) // first slash is already removed by GetPath() + { + path.erase(0, 6); + path = stripAtSymbol(path); // handle technically wrong but parseable domain/users/@username + } + // domain.name/@username - username has to start with @ + else if (path.starts_with("@")) + path = stripAtSymbol(path); + // unknown/invalid format + else + return false; + + path.erase(path.find_last_not_of('/') + 1); // Strip any trailing '/' symbol + // Then construct the username@domain.name format + path.append("@").append(domain); + // And return if it's valid or not + return regex_match(path, s_fediverseRegex); +} + +bool ValidateBlueskyPage(string const & page) +{ + // A valid username can be any domain name, so the username rules don't apply. + if (page.empty()) + return true; + + // Match {@?}{user/domain.name} format + if (regex_match(page, s_blueskyRegex)) + return true; + + // Has to be an url format now + if (!ValidateWebsite(page)) + return false; + + // Match https://bsky.app/profile/{username/domain.name} + url::Url const pageUrl = url::Url::FromString(page); + string_view const domain = pageUrl.GetHost(); + string path = pageUrl.GetPath(); + + // First remove url bits if they exist + if (domain.starts_with(kBskyApp) && path.starts_with("profile/")) + { + path.erase(0, 8); // Strip "profile/" part + path.erase(path.find_last_not_of('/') + 1); // Strip last '/' symbol if exists + // Then try to parse the remaining text as a username again + if (regex_match(path, s_blueskyRegex)) + return true; + } + + return false; +} + bool isSocialContactTag(string_view tag) { - return tag == kInstagram || tag == kFacebook || tag == kTwitter || tag == kVk || tag == kLine; + return tag == kInstagram || tag == kFacebook || tag == kTwitter || tag == kVk || tag == kLine || tag == kFediverse || tag == kBluesky; } bool isSocialContactTag(MapObject::MetadataID const metaID) @@ -460,7 +616,9 @@ bool isSocialContactTag(MapObject::MetadataID const metaID) metaID == MapObject::MetadataID::FMD_CONTACT_FACEBOOK || metaID == MapObject::MetadataID::FMD_CONTACT_TWITTER || metaID == MapObject::MetadataID::FMD_CONTACT_VK || - metaID == MapObject::MetadataID::FMD_CONTACT_LINE; + metaID == MapObject::MetadataID::FMD_CONTACT_LINE || + metaID == MapObject::MetadataID::FMD_CONTACT_FEDIVERSE || + metaID == MapObject::MetadataID ::FMD_CONTACT_BLUESKY; } // Functions ValidateAndFormat_{facebook,instagram,twitter,vk}(...) by default strip domain name @@ -477,6 +635,10 @@ string socialContactToURL(string_view tag, string_view value) return string{kUrlTwitter}.append(value); if (tag == kVk) return string{kUrlVk}.append(value); + if (tag == kFediverse) + return fediverseHandleToUrl(value); + if (tag == kBluesky) // In future + return string{kUrlBluesky}.append(value); if (tag == kLine) { if (value.find('/') == string::npos) // 'value' is a username. @@ -502,6 +664,10 @@ string socialContactToURL(MapObject::MetadataID metaID, string_view value) return string{kUrlTwitter}.append(value); case MapObject::MetadataID::FMD_CONTACT_VK: return string{kUrlVk}.append(value); + case MapObject::MetadataID::FMD_CONTACT_FEDIVERSE: + return fediverseHandleToUrl(value); + case MapObject::MetadataID::FMD_CONTACT_BLUESKY: + return string{kUrlBluesky}.append(value); case MapObject::MetadataID::FMD_CONTACT_LINE: if (value.find('/') == string::npos) // 'value' is a username. return string{kUrlLine}.append(value); diff --git a/indexer/validate_and_format_contacts.hpp b/indexer/validate_and_format_contacts.hpp index 7e430c1656..ab337e68b2 100644 --- a/indexer/validate_and_format_contacts.hpp +++ b/indexer/validate_and_format_contacts.hpp @@ -12,6 +12,8 @@ std::string ValidateAndFormat_instagram(std::string const & v); std::string ValidateAndFormat_twitter(std::string const & v); std::string ValidateAndFormat_vk(std::string const & v); std::string ValidateAndFormat_contactLine(std::string const & v); +std::string ValidateAndFormat_fediverse(std::string const & v); +std::string ValidateAndFormat_bluesky(std::string const & v); bool ValidateWebsite(std::string const & site); bool ValidateFacebookPage(std::string const & v); @@ -19,6 +21,8 @@ bool ValidateInstagramPage(std::string const & v); bool ValidateTwitterPage(std::string const & v); bool ValidateVkPage(std::string const & v); bool ValidateLinePage(std::string const & v); +bool ValidateFediversePage(std::string const & v); +bool ValidateBlueskyPage(std::string const & v); bool isSocialContactTag(std::string_view tag); bool isSocialContactTag(osm::MapObject::MetadataID const metaID); -- 2.45.3 From 445dbc8264e101f263ada0998b89facdc2210292 Mon Sep 17 00:00:00 2001 From: Harry Bond Date: Fri, 31 Jan 2025 14:14:35 +0000 Subject: [PATCH 2/4] [core][qt] Handle Mastodon and Bluesky Signed-off-by: Harry Bond --- data/editor.config | 10 ++++++++++ editor/editor_config.cpp | 2 ++ editor/editor_tests/editor_config_test.cpp | 2 ++ feature_list/feature_list.cpp | 6 ++++-- indexer/editable_map_object.cpp | 4 ++++ qt/place_page_dialog_developer.cpp | 2 ++ qt/place_page_dialog_user.cpp | 2 ++ 7 files changed, 26 insertions(+), 2 deletions(-) diff --git a/data/editor.config b/data/editor.config index 77633aed06..f45ad351f9 100644 --- a/data/editor.config +++ b/data/editor.config @@ -81,6 +81,12 @@ + + + + + + @@ -181,6 +187,8 @@ + + @@ -197,6 +205,8 @@ + + diff --git a/editor/editor_config.cpp b/editor/editor_config.cpp index 476e00ceb0..262af04278 100644 --- a/editor/editor_config.cpp +++ b/editor/editor_config.cpp @@ -22,6 +22,8 @@ static std::unordered_map const kNamesToFMD = { {"website", EType::FMD_WEBSITE}, {"contact_facebook", EType::FMD_CONTACT_FACEBOOK}, {"contact_instagram", EType::FMD_CONTACT_INSTAGRAM}, + {"contact_fediverse", EType::FMD_CONTACT_FEDIVERSE}, + {"contact_bluesky", EType::FMD_CONTACT_BLUESKY}, {"contact_twitter", EType::FMD_CONTACT_TWITTER}, {"contact_vk", EType::FMD_CONTACT_VK}, {"contact_line", EType::FMD_CONTACT_LINE}, diff --git a/editor/editor_tests/editor_config_test.cpp b/editor/editor_tests/editor_config_test.cpp index d01c1c1b5c..15e041369a 100644 --- a/editor/editor_tests/editor_config_test.cpp +++ b/editor/editor_tests/editor_config_test.cpp @@ -22,6 +22,8 @@ UNIT_TEST(EditorConfig_TypeDescription) EType::FMD_CONTACT_TWITTER, EType::FMD_CONTACT_VK, EType::FMD_CONTACT_LINE, + EType::FMD_CONTACT_FEDIVERSE, + EType::FMD_CONTACT_BLUESKY, }; pugi::xml_document doc; diff --git a/feature_list/feature_list.cpp b/feature_list/feature_list.cpp index b41700b24b..85a98f71a1 100644 --- a/feature_list/feature_list.cpp +++ b/feature_list/feature_list.cpp @@ -261,6 +261,8 @@ public: string const contact_twitter(meta.Get(feature::Metadata::FMD_CONTACT_TWITTER)); string const contact_vk(meta.Get(feature::Metadata::FMD_CONTACT_VK)); string const contact_line(meta.Get(feature::Metadata::FMD_CONTACT_LINE)); + string const contact_fediverse(meta.Get(feature::Metadata::FMD_CONTACT_FEDIVERSE)); + string const contact_bluesky(meta.Get(feature::Metadata::FMD_CONTACT_BLUESKY)); string const stars(meta.Get(feature::Metadata::FMD_STARS)); string const internet(meta.Get(feature::Metadata::FMD_INTERNET)); string const denomination(meta.Get(feature::Metadata::FMD_DENOMINATION)); @@ -276,7 +278,7 @@ public: osmId, uid, lat, lon, mwmName, category, name, std::string(city), addrStreet, addrHouse, phone, website, stars, std::string(metaOperator), internet, denomination, wheelchair, opening_hours, wikipedia, floor, fee, atm, contact_facebook, - contact_instagram, contact_twitter, contact_vk, contact_line, wikimedia_commons}; + contact_instagram, contact_twitter, contact_vk, contact_line, contact_fediverse, contact_bluesky, wikimedia_commons}; AppendNames(f, columns); PrintAsCSV(columns, ';', cout); @@ -290,7 +292,7 @@ void PrintHeader() "phone", "website", "cuisines", "stars", "operator", "internet", "denomination", "wheelchair", "opening_hours", "wikipedia", "floor", "fee", "atm", "contact_facebook", "contact_instagram", - "contact_twitter", "contact_vk", "contact_line", "wikimedia_commons"}; + "contact_twitter", "contact_vk", "contact_line", "contact_fediverse", "contact_bluesky", "wikimedia_commons"}; // Append all supported name languages in order. for (uint8_t idx = 1; idx < kLangCount; idx++) columns.push_back("name_" + string(StringUtf8Multilang::GetLangByCode(idx))); diff --git a/indexer/editable_map_object.cpp b/indexer/editable_map_object.cpp index 6310d397d7..aca8149139 100644 --- a/indexer/editable_map_object.cpp +++ b/indexer/editable_map_object.cpp @@ -249,6 +249,8 @@ bool EditableMapObject::IsValidMetadata(MetadataID type, std::string const & val case MetadataID::FMD_CONTACT_TWITTER: return ValidateTwitterPage(value); case MetadataID::FMD_CONTACT_VK: return ValidateVkPage(value); case MetadataID::FMD_CONTACT_LINE: return ValidateLinePage(value); + case MetadataID::FMD_CONTACT_FEDIVERSE: return ValidateFediversePage(value); + case MetadataID::FMD_CONTACT_BLUESKY: return ValidateBlueskyPage(value); case MetadataID::FMD_STARS: { @@ -284,6 +286,8 @@ void EditableMapObject::SetMetadata(MetadataID type, std::string value) case MetadataID::FMD_CONTACT_TWITTER: value = ValidateAndFormat_twitter(value); break; case MetadataID::FMD_CONTACT_VK: value = ValidateAndFormat_vk(value); break; case MetadataID::FMD_CONTACT_LINE: value = ValidateAndFormat_contactLine(value); break; + case MetadataID::FMD_CONTACT_FEDIVERSE: value = ValidateAndFormat_fediverse(value); break; + case MetadataID::FMD_CONTACT_BLUESKY: value = ValidateAndFormat_bluesky(value); break; default: break; } diff --git a/qt/place_page_dialog_developer.cpp b/qt/place_page_dialog_developer.cpp index bf19b93531..4038024ea1 100644 --- a/qt/place_page_dialog_developer.cpp +++ b/qt/place_page_dialog_developer.cpp @@ -113,6 +113,8 @@ PlacePageDialogDeveloper::PlacePageDialogDeveloper(QWidget * parent, place_page: case PropID::FMD_CONTACT_TWITTER: case PropID::FMD_CONTACT_VK: case PropID::FMD_CONTACT_LINE: + case PropID::FMD_CONTACT_FEDIVERSE: + case PropID::FMD_CONTACT_BLUESKY: case PropID::FMD_WIKIPEDIA: case PropID::FMD_WIKIMEDIA_COMMONS: isLink = true; diff --git a/qt/place_page_dialog_user.cpp b/qt/place_page_dialog_user.cpp index b792cf305b..37a72a2efb 100644 --- a/qt/place_page_dialog_user.cpp +++ b/qt/place_page_dialog_user.cpp @@ -218,6 +218,8 @@ PlacePageDialogUser::PlacePageDialogUser(QWidget * parent, place_page::Info cons addSocialNetworkWidget("Twitter", feature::Metadata::EType::FMD_CONTACT_TWITTER); addSocialNetworkWidget("VK", feature::Metadata::EType::FMD_CONTACT_VK); addSocialNetworkWidget("Line", feature::Metadata::EType::FMD_CONTACT_LINE); + addSocialNetworkWidget("Mastodon", feature::Metadata::EType::FMD_CONTACT_FEDIVERSE); + addSocialNetworkWidget("Bluesky", feature::Metadata::EType::FMD_CONTACT_BLUESKY); } if (auto wikimedia_commons = info.GetMetadata(feature::Metadata::EType::FMD_WIKIMEDIA_COMMONS); !wikimedia_commons.empty()) -- 2.45.3 From 5cd6340616c193d7884cfb3d2ba09f1d0dc343e5 Mon Sep 17 00:00:00 2001 From: Harry Bond Date: Fri, 31 Jan 2025 15:09:19 +0000 Subject: [PATCH 3/4] [android] Handle Mastodon and Bluesky Signed-off-by: Harry Bond --- .../organicmaps/bookmarks/data/Metadata.java | 4 ++- .../organicmaps/editor/EditorFragment.java | 8 ++++++ .../sections/PlacePageLinksFragment.java | 23 ++++++++++++++++- .../main/res/drawable/ic_bluesky_white.xml | 10 ++++++++ .../main/res/drawable/ic_mastodon_white.xml | 10 ++++++++ .../src/main/res/layout/fragment_editor.xml | 6 +++++ .../main/res/layout/place_page_bluesky.xml | 25 +++++++++++++++++++ .../main/res/layout/place_page_fediverse.xml | 25 +++++++++++++++++++ .../res/layout/place_page_links_fragment.xml | 2 ++ 9 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/res/drawable/ic_bluesky_white.xml create mode 100644 android/app/src/main/res/drawable/ic_mastodon_white.xml create mode 100644 android/app/src/main/res/layout/place_page_bluesky.xml create mode 100644 android/app/src/main/res/layout/place_page_fediverse.xml diff --git a/android/app/src/main/java/app/organicmaps/bookmarks/data/Metadata.java b/android/app/src/main/java/app/organicmaps/bookmarks/data/Metadata.java index e414cbb793..e2078103bc 100644 --- a/android/app/src/main/java/app/organicmaps/bookmarks/data/Metadata.java +++ b/android/app/src/main/java/app/organicmaps/bookmarks/data/Metadata.java @@ -67,7 +67,9 @@ public class Metadata implements Parcelable FMD_WEBSITE_MENU(46), FMD_SELF_SERVICE(47), FMD_OUTDOOR_SEATING(48), - FMD_NETWORK(49); + FMD_NETWORK(49), + FMD_CONTACT_FEDIVERSE(50), + FMD_CONTACT_BLUESKY(51); private final int mMetaType; MetadataType(int metadataType) diff --git a/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java b/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java index 86ab7b1e7d..0374e5d33a 100644 --- a/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java +++ b/android/app/src/main/java/app/organicmaps/editor/EditorFragment.java @@ -189,11 +189,13 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe initMetadataEntry(Metadata.MetadataType.FMD_WEBSITE, R.string.error_enter_correct_web); initMetadataEntry(Metadata.MetadataType.FMD_WEBSITE_MENU, R.string.error_enter_correct_web); initMetadataEntry(Metadata.MetadataType.FMD_EMAIL, R.string.error_enter_correct_email); + initMetadataEntry(Metadata.MetadataType.FMD_CONTACT_FEDIVERSE, R.string.error_enter_correct_fediverse_page); initMetadataEntry(Metadata.MetadataType.FMD_CONTACT_FACEBOOK, R.string.error_enter_correct_facebook_page); initMetadataEntry(Metadata.MetadataType.FMD_CONTACT_INSTAGRAM, R.string.error_enter_correct_instagram_page); initMetadataEntry(Metadata.MetadataType.FMD_CONTACT_TWITTER, R.string.error_enter_correct_twitter_page); initMetadataEntry(Metadata.MetadataType.FMD_CONTACT_VK, R.string.error_enter_correct_vk_page); initMetadataEntry(Metadata.MetadataType.FMD_CONTACT_LINE, R.string.error_enter_correct_line_page); + initMetadataEntry(Metadata.MetadataType.FMD_CONTACT_BLUESKY, R.string.error_enter_correct_bluesky_page); mCuisine.setText(Editor.nativeGetFormattedCuisine()); String selfServiceMetadata = Editor.nativeGetMetadata(Metadata.MetadataType.FMD_SELF_SERVICE.toInt()); @@ -444,6 +446,8 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe R.drawable.ic_website_menu, R.string.website_menu, InputType.TYPE_TEXT_VARIATION_URI); View emailBlock = initBlock(view, Metadata.MetadataType.FMD_EMAIL, R.id.block_email, R.drawable.ic_email, R.string.email, InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); + View fediverseContactBlock = initBlock(view, Metadata.MetadataType.FMD_CONTACT_FEDIVERSE, R.id.block_fediverse, + R.drawable.ic_mastodon_white, R.string.mastodon, InputType.TYPE_TEXT_VARIATION_URI); View facebookContactBlock = initBlock(view, Metadata.MetadataType.FMD_CONTACT_FACEBOOK, R.id.block_facebook, R.drawable.ic_facebook_white, R.string.facebook, InputType.TYPE_TEXT_VARIATION_URI); View instagramContactBlock = initBlock(view, Metadata.MetadataType.FMD_CONTACT_INSTAGRAM, R.id.block_instagram, @@ -454,6 +458,8 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe R.drawable.ic_vk_white, R.string.vk, InputType.TYPE_TEXT_VARIATION_URI); View lineContactBlock = initBlock(view, Metadata.MetadataType.FMD_CONTACT_LINE, R.id.block_line, R.drawable.ic_line_white, R.string.editor_line_social_network, InputType.TYPE_TEXT_VARIATION_URI); + View blueskyContactBlock = initBlock(view, Metadata.MetadataType.FMD_CONTACT_BLUESKY, R.id.block_bluesky, + R.drawable.ic_bluesky_white, R.string.bluesky, InputType.TYPE_TEXT_VARIATION_URI); View operatorBlock = initBlock(view, Metadata.MetadataType.FMD_OPERATOR, R.id.block_operator, R.drawable.ic_operator, R.string.editor_operator, 0); @@ -499,11 +505,13 @@ public class EditorFragment extends BaseMwmFragment implements View.OnClickListe mDetailsBlocks.put(Metadata.MetadataType.FMD_EMAIL, emailBlock); mDetailsBlocks.put(Metadata.MetadataType.FMD_OPERATOR, operatorBlock); + mSocialMediaBlocks.put(Metadata.MetadataType.FMD_CONTACT_FEDIVERSE, fediverseContactBlock); mSocialMediaBlocks.put(Metadata.MetadataType.FMD_CONTACT_FACEBOOK, facebookContactBlock); mSocialMediaBlocks.put(Metadata.MetadataType.FMD_CONTACT_INSTAGRAM, instagramContactBlock); mSocialMediaBlocks.put(Metadata.MetadataType.FMD_CONTACT_TWITTER, twitterContactBlock); mSocialMediaBlocks.put(Metadata.MetadataType.FMD_CONTACT_VK, vkContactBlock); mSocialMediaBlocks.put(Metadata.MetadataType.FMD_CONTACT_LINE, lineContactBlock); + mSocialMediaBlocks.put(Metadata.MetadataType.FMD_CONTACT_BLUESKY, blueskyContactBlock); } private static TextInputEditText findInput(View blockWithInput) diff --git a/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageLinksFragment.java b/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageLinksFragment.java index b6754928f8..0cf56b9c19 100644 --- a/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageLinksFragment.java +++ b/android/app/src/main/java/app/organicmaps/widget/placepage/sections/PlacePageLinksFragment.java @@ -41,6 +41,10 @@ public class PlacePageLinksFragment extends Fragment implements Observer mMapObject.getWebsiteUrl(false /* strip */, Metadata.MetadataType.FMD_WEBSITE_MENU); - case FMD_CONTACT_FACEBOOK, FMD_CONTACT_INSTAGRAM, FMD_CONTACT_TWITTER, FMD_CONTACT_VK, FMD_CONTACT_LINE -> + case FMD_CONTACT_FACEBOOK, FMD_CONTACT_INSTAGRAM, FMD_CONTACT_TWITTER, + FMD_CONTACT_FEDIVERSE, FMD_CONTACT_BLUESKY, FMD_CONTACT_VK, FMD_CONTACT_LINE -> { if (TextUtils.isEmpty(mMapObject.getMetadata(type))) yield ""; @@ -153,6 +158,16 @@ public class PlacePageLinksFragment extends Fragment implements Observer openUrl(Metadata.MetadataType.FMD_CONTACT_INSTAGRAM)); mInstagramPage.setOnLongClickListener((v) -> copyUrl(mInstagramPage, Metadata.MetadataType.FMD_CONTACT_INSTAGRAM)); + mFediversePage = mFrame.findViewById(R.id.ll__place_fediverse); + mTvFediversePage = mFrame.findViewById(R.id.tv__place_fediverse_page); + mFediversePage.setOnClickListener((v) -> openUrl(Metadata.MetadataType.FMD_CONTACT_FEDIVERSE)); + mFediversePage.setOnLongClickListener((v) -> copyUrl(mFediversePage, Metadata.MetadataType.FMD_CONTACT_FEDIVERSE)); + + mBlueskyPage = mFrame.findViewById(R.id.ll__place_bluesky); + mTvBlueskyPage = mFrame.findViewById(R.id.tv__place_bluesky_page); + mBlueskyPage.setOnClickListener((v) -> openUrl(Metadata.MetadataType.FMD_CONTACT_BLUESKY)); + mBlueskyPage.setOnLongClickListener((v) -> copyUrl(mBlueskyPage, Metadata.MetadataType.FMD_CONTACT_BLUESKY)); + mTwitterPage = mFrame.findViewById(R.id.ll__place_twitter); mTvTwitterPage = mFrame.findViewById(R.id.tv__place_twitter_page); mTwitterPage.setOnClickListener((v) -> openUrl(Metadata.MetadataType.FMD_CONTACT_TWITTER)); @@ -216,6 +231,12 @@ public class PlacePageLinksFragment extends Fragment implements Observer + + diff --git a/android/app/src/main/res/drawable/ic_mastodon_white.xml b/android/app/src/main/res/drawable/ic_mastodon_white.xml new file mode 100644 index 0000000000..d3dc1b2c61 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_mastodon_white.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/layout/fragment_editor.xml b/android/app/src/main/res/layout/fragment_editor.xml index 9ea65f5cb9..7a2f818a3e 100644 --- a/android/app/src/main/res/layout/fragment_editor.xml +++ b/android/app/src/main/res/layout/fragment_editor.xml @@ -331,6 +331,9 @@ android:textAppearance="@style/MwmTextAppearance.Body3" tools:ignore="UnusedAttribute"/> + @@ -346,6 +349,9 @@ + diff --git a/android/app/src/main/res/layout/place_page_bluesky.xml b/android/app/src/main/res/layout/place_page_bluesky.xml new file mode 100644 index 0000000000..43f5219457 --- /dev/null +++ b/android/app/src/main/res/layout/place_page_bluesky.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/android/app/src/main/res/layout/place_page_fediverse.xml b/android/app/src/main/res/layout/place_page_fediverse.xml new file mode 100644 index 0000000000..c54d57fc1a --- /dev/null +++ b/android/app/src/main/res/layout/place_page_fediverse.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/android/app/src/main/res/layout/place_page_links_fragment.xml b/android/app/src/main/res/layout/place_page_links_fragment.xml index d7f1f99647..0db6e7c254 100644 --- a/android/app/src/main/res/layout/place_page_links_fragment.xml +++ b/android/app/src/main/res/layout/place_page_links_fragment.xml @@ -13,11 +13,13 @@ android:layout_height="wrap_content" tools:layout="@layout/place_page_phone_fragment" /> + + \ No newline at end of file -- 2.45.3 From 59c569a022ad223f8de3e7687ee4de312299bbf5 Mon Sep 17 00:00:00 2001 From: Harry Bond Date: Fri, 31 Jan 2025 14:55:00 +0000 Subject: [PATCH 4/4] [strings] Add Mastodon and Bluesky strings Signed-off-by: Harry Bond --- android/app/src/main/res/values/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 36eb211979..ab1c547c29 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -286,6 +286,8 @@ Instagram VK + + Bluesky LINE @@ -613,6 +615,8 @@ Enter a valid Twitter username or web address Enter a valid VK username or web address Enter a valid LINE ID or web address + Enter a valid Mastodon username or web address + Enter a valid Bluesky username or web address Add Place to OpenStreetMap Do you want to send it to all users? -- 2.45.3