diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index d18ded385a..1463b1d67d 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -38,12 +38,6 @@ //--> <uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> - <!-- - Android 13 (API level 33) and higher supports a runtime permission for sending non-exempt (including Foreground - Services (FGS)) notifications from an app. - //--> - <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> - <queries> <intent> diff --git a/android/build.gradle b/android/build.gradle index 53374c7638..9aab4e47f5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,19 +26,19 @@ buildscript { ext.googleFirebaseServicesEnabled = project.hasProperty('firebase') ?: googleFirebaseServicesDefault dependencies { - classpath 'com.android.tools.build:gradle:7.4.1' + classpath 'com.android.tools.build:gradle:7.3.1' if (googleMobileServicesEnabled) { println("Building with Google Mobile Services") - classpath 'com.google.gms:google-services:4.3.15' + classpath 'com.google.gms:google-services:4.3.10' } else { println("Building without Google Services") } if (googleFirebaseServicesEnabled) { println("Building with Google Firebase Services") - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.4' - classpath 'com.google.firebase:firebase-appdistribution-gradle:3.2.0' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' + classpath 'com.google.firebase:firebase-appdistribution-gradle:2.2.0' } else { println("Building without Google Firebase Services") } @@ -75,34 +75,36 @@ dependencies { // Google Firebase Services if (googleFirebaseServicesEnabled) { // Import the BoM for the Firebase platform - implementation platform('com.google.firebase:firebase-bom:31.2.1') + implementation platform('com.google.firebase:firebase-bom:30.5.0') // Add the dependencies for the Crashlytics and Analytics libraries // When using the BoM, you don't specify versions in Firebase library dependencies implementation 'com.google.firebase:firebase-crashlytics' implementation 'com.google.firebase:firebase-crashlytics-ndk' } - // This line is added as a workaround for duplicate classes error caused by some outdated dependency: - // > A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable - // We don't use Kotlin, but some dependencies are actively using it. - implementation(platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')) - implementation 'androidx.annotation:annotation:1.6.0-dev01' - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.annotation:annotation:1.5.0' + implementation 'androidx.appcompat:appcompat:1.7.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.fragment:fragment:1.5.5' + implementation 'androidx.fragment:fragment:1.5.4' + // Lifecycle is added as a workaround for duplicate classes error caused by some outdated dependency: + // > A failure occurred while executing com.android.build.gradle.internal.tasks.CheckDuplicatesRunnable + // > Duplicate class androidx.lifecycle.ViewModelLazy found in modules jetified-lifecycle-viewmodel-ktx-2.3.1-runtime (androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1) and lifecycle-viewmodel-2.5.1-runtime (androidx.lifecycle:lifecycle-viewmodel:2.5.1) + // Duplicate class androidx.lifecycle.ViewTreeViewModelKt found in modules jetified-lifecycle-viewmodel-ktx-2.3.1-runtime (androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1) and lifecycle-viewmodel-2.5.1-runtime (androidx.lifecycle:lifecycle-viewmodel:2.5.1) + implementation 'androidx.lifecycle:lifecycle-viewmodel:2.5.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' - implementation 'androidx.work:work-runtime:2.8.0' - implementation 'com.google.android.material:material:1.8.0' - implementation 'com.google.code.gson:gson:2.10.1' + implementation 'androidx.work:work-runtime:2.7.1' + implementation 'com.google.android.material:material:1.8.0-alpha02' + implementation 'com.google.code.gson:gson:2.10' implementation 'com.timehop.stickyheadersrecyclerview:library:0.4.3@aar' implementation 'com.github.devnullorthrow:MPAndroidChart:3.2.0-alpha' implementation 'net.jcip:jcip-annotations:1.0' // Test Dependencies testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:5.1.1' - testImplementation 'org.mockito:mockito-inline:5.1.1' + testImplementation 'org.mockito:mockito-core:4.8.1' + testImplementation 'org.mockito:mockito-inline:4.8.1' } def run(cmd) { @@ -145,7 +147,7 @@ android { compileSdkVersion propCompileSdkVersion.toInteger() buildToolsVersion propBuildToolsVersion - ndkVersion '25.2.9519653' + ndkVersion '25.1.8937393' defaultConfig { // Default package name is taken from the manifest and should be app.organicmaps @@ -452,6 +454,7 @@ task prepareGoogleReleaseListing { final sourceFiles = fileTree(dir: sourceDir, include: "**/*.txt", exclude: "**/*-${targetFlavor}.txt") sourceFiles.each { File sourceFile -> + final path = sourceFile.getPath() final locale = sourceFile.parentFile.getName() final targetLocaleDir = new File(targetDir, locale) if (!targetLocaleDir.isDirectory()) diff --git a/android/gradle.properties b/android/gradle.properties index 5f249e8b1e..c9cf2e5f8f 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,7 +1,7 @@ propMinSdkVersion=21 propTargetSdkVersion=33 propCompileSdkVersion=33 -propBuildToolsVersion=33.0.2 +propBuildToolsVersion=33.0.0 org.gradle.caching=true org.gradle.jvmargs=-Xmx1024m -Xms256m diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar index 943f0cbfa7..249e5832f0 100644 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index f398c33c4b..ae04661ee7 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip -networkTimeout=10000 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew index 65dcd68d65..a69d9cb6c2 100755 --- a/android/gradlew +++ b/android/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,11 +80,11 @@ do esac done -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,16 +143,12 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac diff --git a/android/gradlew.bat b/android/gradlew.bat index 93e3f59f13..f127cfd49d 100644 --- a/android/gradlew.bat +++ b/android/gradlew.bat @@ -26,7 +26,6 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% diff --git a/android/jni/app/organicmaps/Framework.cpp b/android/jni/app/organicmaps/Framework.cpp index ad17613965..209e0b1608 100644 --- a/android/jni/app/organicmaps/Framework.cpp +++ b/android/jni/app/organicmaps/Framework.cpp @@ -1650,6 +1650,16 @@ Java_app_organicmaps_Framework_nativeDeleteBookmarkFromMapObject(JNIEnv * env, j return usermark_helper::CreateMapObject(env, g_framework->GetPlacePageInfo()); } +JNIEXPORT jstring JNICALL +Java_app_organicmaps_Framework_nativeGetPoiContactUrl(JNIEnv *env, jclass, jint id) +{ + auto const metaID = static_cast<osm::MapObject::MetadataID>(id); + string_view const value = g_framework->GetPlacePageInfo().GetMetadata(metaID); + if (osm::isSocialContactTag(metaID)) + return jni::ToJavaString(env, osm::socialContactToURL(metaID, value)); + return jni::ToJavaString(env, value); +} + JNIEXPORT void JNICALL Java_app_organicmaps_Framework_nativeTurnOnChoosePositionMode(JNIEnv *, jclass, jboolean isBusiness, jboolean applyPosition) { diff --git a/android/jni/app/organicmaps/editor/Editor.cpp b/android/jni/app/organicmaps/editor/Editor.cpp index cbe58bb013..c4b295822f 100644 --- a/android/jni/app/organicmaps/editor/Editor.cpp +++ b/android/jni/app/organicmaps/editor/Editor.cpp @@ -100,6 +100,14 @@ Java_app_organicmaps_editor_Editor_nativeGetMetadata(JNIEnv * env, jclass, jint { auto const metaID = static_cast<osm::MapObject::MetadataID>(id); ASSERT_LESS(metaID, osm::MapObject::MetadataID::FMD_COUNT, ()); + if (osm::isSocialContactTag(metaID)) + { + auto const value = g_editableMapObject.GetMetadata(metaID); + if (value.find('/') == std::string::npos) // `value` contains pagename. + return jni::ToJavaString(env, value); + // `value` contains URL. + return jni::ToJavaString(env, osm::socialContactToURL(metaID, value)); + } return jni::ToJavaString(env, g_editableMapObject.GetMetadata(metaID)); } diff --git a/android/src/app/organicmaps/Framework.java b/android/src/app/organicmaps/Framework.java index 05fbae2eae..9b39a3704c 100644 --- a/android/src/app/organicmaps/Framework.java +++ b/android/src/app/organicmaps/Framework.java @@ -335,13 +335,12 @@ public class Framework public static native boolean nativeIsIsolinesLayerEnabled(); - public static native void nativeSetGuidesLayerEnabled(boolean enabled); - - public static native boolean nativeIsGuidesLayerEnabled(); - @NonNull public static native MapObject nativeDeleteBookmarkFromMapObject(); + @NonNull + public static native String nativeGetPoiContactUrl(int metadataType); + public static native void nativeZoomToPoint(double lat, double lon, int zoom, boolean animate); /** diff --git a/android/src/app/organicmaps/editor/Editor.java b/android/src/app/organicmaps/editor/Editor.java index 19a303d9c6..66cb1e7691 100644 --- a/android/src/app/organicmaps/editor/Editor.java +++ b/android/src/app/organicmaps/editor/Editor.java @@ -72,7 +72,6 @@ public final class Editor public static native String nativeGetMetadata(int id); public static native boolean nativeIsMetadataValid(int id, String value); public static native void nativeSetMetadata(int id, String value); - public static native String nativeGetOpeningHours(); public static native void nativeSetOpeningHours(String openingHours); public static String nativeGetPhone() diff --git a/android/src/app/organicmaps/widget/placepage/PlacePageLinksFragment.java b/android/src/app/organicmaps/widget/placepage/PlacePageLinksFragment.java index bc70897337..d43f7dc08b 100644 --- a/android/src/app/organicmaps/widget/placepage/PlacePageLinksFragment.java +++ b/android/src/app/organicmaps/widget/placepage/PlacePageLinksFragment.java @@ -12,6 +12,7 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; +import app.organicmaps.Framework; import app.organicmaps.R; import app.organicmaps.bookmarks.data.MapObject; import app.organicmaps.bookmarks.data.Metadata; @@ -145,7 +146,7 @@ public class PlacePageLinksFragment extends Fragment implements Observer<MapObje private void openUrl(Metadata.MetadataType type) { final String url = getLink(type); - if (type != Metadata.MetadataType.FMD_CONTACT_LINE || !isSocialUsername(type)) + if (!TextUtils.isEmpty(url)) Utils.openUrl(requireContext(), url); } @@ -153,35 +154,21 @@ public class PlacePageLinksFragment extends Fragment implements Observer<MapObje { final String metadata = mMapObject.getMetadata(type); if (TextUtils.isEmpty(metadata)) - { return ""; - } - String path = ""; - String domain = ""; + switch (type) { case FMD_WEBSITE: - path = getWebsiteUrl(mMapObject); - break; + return getWebsiteUrl(mMapObject); case FMD_CONTACT_FACEBOOK: - domain = "https://m.facebook.com/"; - break; case FMD_CONTACT_INSTAGRAM: - domain = "https://instagram.com/"; - break; case FMD_CONTACT_TWITTER: - domain = "https://mobile.twitter.com/"; - break; case FMD_CONTACT_VK: - domain = "https://vk.com/"; - break; case FMD_CONTACT_LINE: - path = getLineUrl(); - break; + return Framework.nativeGetPoiContactUrl(type.toInt()); + default: + return metadata; } - if (TextUtils.isEmpty(path)) - path = metadata; - return domain + path; } private boolean copyUrl(View view, Metadata.MetadataType type) @@ -202,24 +189,10 @@ public class PlacePageLinksFragment extends Fragment implements Observer<MapObje return true; } - private void refreshSocialPageLink(View view, TextView tvSocialPage, String socialPage, String webDomain) + private void refreshSocialPageLink(@NonNull MapObject mapObject, View view, TextView tvSocialPage, Metadata.MetadataType metaType) { - if (TextUtils.isEmpty(socialPage)) - view.setVisibility(GONE); - else if (socialPage.indexOf('/') >= 0) - refreshMetadataOrHide("https://" + webDomain + "/" + socialPage, view, tvSocialPage); - else - refreshMetadataOrHide("@" + socialPage, view, tvSocialPage); - } - - private void refreshSocialPageLink(View view, TextView tvSocialPage, String socialPage) - { - if (TextUtils.isEmpty(socialPage)) - view.setVisibility(GONE); - else if (socialPage.indexOf('/') >= 0) - refreshMetadataOrHide("https://" + socialPage, view, tvSocialPage); - else - refreshMetadataOrHide("@" + socialPage, view, tvSocialPage); + final String socialPage = mapObject.getMetadata(metaType); + refreshMetadataOrHide(socialPage, view, tvSocialPage); } private static String getWebsiteUrl(MapObject mapObject) @@ -229,15 +202,6 @@ public class PlacePageLinksFragment extends Fragment implements Observer<MapObje return TextUtils.isEmpty(website) ? url : website; } - private String getLineUrl() - { - final String metadata = mMapObject.getMetadata(Metadata.MetadataType.FMD_CONTACT_LINE); - if (isSocialUsername(Metadata.MetadataType.FMD_CONTACT_LINE)) - return "https://line.me/R/ti/p/@" + metadata; - else - return "https://" + metadata; - } - private void refreshLinks() { refreshMetadataOrHide(getWebsiteUrl(mMapObject), mWebsite, mTvWebsite); @@ -246,18 +210,11 @@ public class PlacePageLinksFragment extends Fragment implements Observer<MapObje refreshMetadataOrHide(wikimedia_commons_text, mWikimedia, mTvWikimedia); refreshMetadataOrHide(mMapObject.getMetadata(Metadata.MetadataType.FMD_EMAIL), mEmail, mTvEmail); - final String facebookPageLink = mMapObject.getMetadata(Metadata.MetadataType.FMD_CONTACT_FACEBOOK); - refreshSocialPageLink(mFacebookPage, mTvFacebookPage, facebookPageLink, "facebook.com"); - final String instagramPageLink = mMapObject.getMetadata(Metadata.MetadataType.FMD_CONTACT_INSTAGRAM); - refreshSocialPageLink(mInstagramPage, mTvInstagramPage, instagramPageLink, "instagram.com"); - final String twitterPageLink = mMapObject.getMetadata(Metadata.MetadataType.FMD_CONTACT_TWITTER); - refreshSocialPageLink(mTwitterPage, mTvTwitterPage, twitterPageLink, "twitter.com"); - final String vkPageLink = mMapObject.getMetadata(Metadata.MetadataType.FMD_CONTACT_VK); - refreshSocialPageLink(mVkPage, mTvVkPage, vkPageLink, "vk.com"); - final String linePageLink = mMapObject.getMetadata(Metadata.MetadataType.FMD_CONTACT_LINE); - // Tag `contact:line` could contain urls from domains: line.me, liff.line.me, page.line.me, etc. - // And `socialPage` should not be prepended with domain, but only with "https://" protocol. - refreshSocialPageLink(mLinePage, mTvLinePage, linePageLink); + refreshSocialPageLink(mMapObject, mFacebookPage, mTvFacebookPage, Metadata.MetadataType.FMD_CONTACT_FACEBOOK); + refreshSocialPageLink(mMapObject, mInstagramPage, mTvInstagramPage, Metadata.MetadataType.FMD_CONTACT_INSTAGRAM); + refreshSocialPageLink(mMapObject, mTwitterPage, mTvTwitterPage, Metadata.MetadataType.FMD_CONTACT_TWITTER); + refreshSocialPageLink(mMapObject, mVkPage, mTvVkPage, Metadata.MetadataType.FMD_CONTACT_VK); + refreshSocialPageLink(mMapObject, mLinePage, mTvLinePage, Metadata.MetadataType.FMD_CONTACT_LINE); } @Override diff --git a/base/base_tests/string_utils_test.cpp b/base/base_tests/string_utils_test.cpp index 438afb66b4..05991a6281 100644 --- a/base/base_tests/string_utils_test.cpp +++ b/base/base_tests/string_utils_test.cpp @@ -923,6 +923,18 @@ UNIT_TEST(EndsWith) TEST(!EndsWith(s, MakeUniString("aюя")), ()); TEST(!EndsWith(s, MakeUniString("1zюя")), ()); } + { + std::string const s("abcd"); + TEST(EndsWith(s, std::string_view{""}), ()); + TEST(EndsWith(s, std::string_view{"d"}), ()); + TEST(EndsWith(s, std::string_view{"bcd"}), ()); + TEST(EndsWith(s, std::string_view{"abcd"}), ()); + TEST(!EndsWith(s, std::string_view{"dd"}), ()); + TEST(!EndsWith(s, std::string_view{"c\""}), ()); + TEST(!EndsWith(s, std::string_view{"cde"}), ()); + TEST(!EndsWith(s, std::string_view{"abcde"}), ()); + TEST(!EndsWith(s, std::string_view{"0abcd"}), ()); + } } UNIT_TEST(EatPrefix_EatSuffix) diff --git a/base/string_utils.cpp b/base/string_utils.cpp index 0d0a2025b3..db7fd9f975 100644 --- a/base/string_utils.cpp +++ b/base/string_utils.cpp @@ -329,9 +329,14 @@ bool EndsWith(UniString const & s1, UniString const & s2) } bool EndsWith(std::string const & s1, char const * s2) +{ + return EndsWith(s1, std::string_view(s2)); +} + +bool EndsWith(std::string const & s1, std::string_view s2) { size_t const n = s1.size(); - size_t const m = strlen(s2); + size_t const m = s2.size(); if (n < m) return false; return (s1.compare(n - m, m, s2) == 0); diff --git a/base/string_utils.hpp b/base/string_utils.hpp index 5d892104a0..1ae52e015b 100644 --- a/base/string_utils.hpp +++ b/base/string_utils.hpp @@ -597,6 +597,7 @@ bool StartsWith(std::string const & s1, std::string const & s2); bool EndsWith(UniString const & s1, UniString const & s2); bool EndsWith(std::string const & s1, char const * s2); +bool EndsWith(std::string const & s1, std::string_view s2); bool EndsWith(std::string const & s, std::string::value_type c); bool EndsWith(std::string const & s1, std::string const & s2); diff --git a/data/editor.config b/data/editor.config index a6e2d2d7c6..9b3d774316 100644 --- a/data/editor.config +++ b/data/editor.config @@ -152,6 +152,7 @@ <field_ref name="contact_instagram" /> <field_ref name="contact_twitter" /> <field_ref name="contact_vk" /> + <field_ref name="contact_line" /> </field_group> <field_group name="poi_internet"> <field_ref name="name" /> @@ -167,6 +168,7 @@ <field_ref name="contact_instagram" /> <field_ref name="contact_twitter" /> <field_ref name="contact_vk" /> + <field_ref name="contact_line" /> </field_group> </fields> <!-- Types should be sorted by their priority. --> diff --git a/data/strings/types_strings.txt b/data/strings/types_strings.txt index a60e234715..067f54e27b 100644 --- a/data/strings/types_strings.txt +++ b/data/strings/types_strings.txt @@ -5080,7 +5080,7 @@ pt = Fronteira de subúrbio pt-BR = Fronteira de subúrbio ru = Граница пригорода - tr = Mahalle Sınırı + tr = Banliyö Sınırı uk = Приміська межа zh-Hans = 市郊行政区域界线 @@ -17467,7 +17467,7 @@ sv = Förort sw = Pambizo th = ชานเมือง - tr = Mahalle + tr = Banliyö uk = Район vi = Đảo zh-Hans = 市郊 diff --git a/docs/INSTALL.md b/docs/INSTALL.md index b395156317..a53d9bb0c6 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -43,19 +43,12 @@ Configure the repository for an opensource build: bash ./configure.sh ``` -For _Windows 10_: You should be able to build the project by following either of these setups: - -**Setup 1: Using WSL** -1. Install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) on your machine. -2. Install g++ by running the following command in WSL: `sudo apt install g++` -3. Run `./configure.sh` in WSL. - -**Setup 2: Using Visual Studio Developer Command Prompt** -1. Install the [Visual Studio Developer Command Prompt](https://docs.microsoft.com/en-us/visualstudio/ide/reference/command-prompt-powershell?view=vs-2022) (make sure to choose the latest MSVC x64/x86 build tool and Windows 10/11 SDK as individual components while installing Visual Studio). -2. Run the following command and follow instructions: +For _Windows 10_: Use WSL to run `./configure.sh`, or, alternatively, run the following command from the +[Visual Studio Developer Command Prompt](https://docs.microsoft.com/en-us/visualstudio/ide/reference/command-prompt-powershell?view=vs-2022) +and follow instructions: ```bash -"C:\Program Files\Git\bin\bash.exe" configure.sh # execute the script by using Developer Command Prompt +bash ./configure.sh # execute the script by using Ubuntu WSL VM ``` ### Special cases options diff --git a/editor/editor_tests/editor_config_test.cpp b/editor/editor_tests/editor_config_test.cpp index d2932e5d81..e609043f06 100644 --- a/editor/editor_tests/editor_config_test.cpp +++ b/editor/editor_tests/editor_config_test.cpp @@ -21,6 +21,7 @@ UNIT_TEST(EditorConfig_TypeDescription) EType::FMD_CONTACT_INSTAGRAM, EType::FMD_CONTACT_TWITTER, EType::FMD_CONTACT_VK, + EType::FMD_CONTACT_LINE, }; pugi::xml_document doc; diff --git a/editor/editor_tests/xml_feature_test.cpp b/editor/editor_tests/xml_feature_test.cpp index 5764d7d208..dac94b0812 100644 --- a/editor/editor_tests/xml_feature_test.cpp +++ b/editor/editor_tests/xml_feature_test.cpp @@ -480,3 +480,89 @@ UNIT_TEST(XMLFeature_Diet) ft.SetCuisine(""); TEST_EQUAL(ft.GetCuisine(), "", ()); } + +UNIT_TEST(XMLFeature_SocialContactsProcessing) +{ + { + std::string const nightclubStr = R"(<?xml version="1.0"?> + <node lat="50.4082862" lon="30.5130017" timestamp="2022-02-24T05:07:00Z"> + <tag k="amenity" v="nightclub" /> + <tag k="name" v="Stereo Plaza" /> + <tag k="contact:facebook" v="http://www.facebook.com/pages/Stereo-Plaza/118100041593935" /> + <tag k="contact:instagram" v="https://www.instagram.com/p/CSy87IhMhfm/" /> + <tag k="contact:line" v="liff.line.me/1645278921-kWRPP32q/?accountId=673watcr" /> + </node> + )"; + + editor::XMLFeature xmlFeature(nightclubStr); + + osm::EditableMapObject emo; + editor::FromXML(xmlFeature, emo); + + // Read and write "contact:facebook" to apply normalization. + std::string contactFacebook(emo.GetMetadata(feature::Metadata::FMD_CONTACT_FACEBOOK)); + emo.SetMetadata(osm::MapObject::MetadataID::FMD_CONTACT_FACEBOOK, contactFacebook); + + // Read and write "contact:instagram" to apply normalization. + std::string contactInstagram(emo.GetMetadata(feature::Metadata::FMD_CONTACT_INSTAGRAM)); + emo.SetMetadata(osm::MapObject::MetadataID::FMD_CONTACT_INSTAGRAM, contactInstagram); + + // Read and write "contact:line" to apply normalization. + std::string contactLine(emo.GetMetadata(feature::Metadata::FMD_CONTACT_LINE)); + emo.SetMetadata(osm::MapObject::MetadataID::FMD_CONTACT_LINE, contactLine); + + auto convertedFt = editor::ToXML(emo, true); + + TEST(convertedFt.HasTag("contact:facebook"), ()); + TEST_EQUAL(convertedFt.GetTagValue("contact:facebook"), "https://facebook.com/pages/Stereo-Plaza/118100041593935", ()); + + TEST(convertedFt.HasTag("contact:instagram"), ()); + TEST_EQUAL(convertedFt.GetTagValue("contact:instagram"), "https://instagram.com/p/CSy87IhMhfm", ()); + + TEST(convertedFt.HasTag("contact:line"), ()); + TEST_EQUAL(convertedFt.GetTagValue("contact:line"), "https://liff.line.me/1645278921-kWRPP32q/?accountId=673watcr", ()); + } +} + +UNIT_TEST(XMLFeature_SocialContactsProcessing_clean) +{ + { + std::string const nightclubStr = R"(<?xml version="1.0"?> + <node lat="40.82862" lon="20.30017" timestamp="2022-02-24T05:07:00Z"> + <tag k="amenity" v="bar" /> + <tag k="name" v="Irish Pub" /> + <tag k="contact:facebook" v="https://www.facebook.com/PierreCardinPeru.oficial/" /> + <tag k="contact:instagram" v="https://www.instagram.com/fraback.genusswelt/" /> + <tag k="contact:line" v="https://line.me/R/ti/p/%40015qevdv" /> + </node> + )"; + + editor::XMLFeature xmlFeature(nightclubStr); + + osm::EditableMapObject emo; + editor::FromXML(xmlFeature, emo); + + // Read and write "contact:facebook" to apply normalization. + std::string contactFacebook(emo.GetMetadata(feature::Metadata::FMD_CONTACT_FACEBOOK)); + emo.SetMetadata(osm::MapObject::MetadataID::FMD_CONTACT_FACEBOOK, contactFacebook); + + // Read and write "contact:instagram" to apply normalization. + std::string contactInstagram(emo.GetMetadata(feature::Metadata::FMD_CONTACT_INSTAGRAM)); + emo.SetMetadata(osm::MapObject::MetadataID::FMD_CONTACT_INSTAGRAM, contactInstagram); + + // Read and write "contact:line" to apply normalization. + std::string contactLine(emo.GetMetadata(feature::Metadata::FMD_CONTACT_LINE)); + emo.SetMetadata(osm::MapObject::MetadataID::FMD_CONTACT_LINE, contactLine); + + auto convertedFt = editor::ToXML(emo, true); + + TEST(convertedFt.HasTag("contact:facebook"), ()); + TEST_EQUAL(convertedFt.GetTagValue("contact:facebook"), "PierreCardinPeru.oficial", ()); + + TEST(convertedFt.HasTag("contact:instagram"), ()); + TEST_EQUAL(convertedFt.GetTagValue("contact:instagram"), "fraback.genusswelt", ()); + + TEST(convertedFt.HasTag("contact:line"), ()); + TEST_EQUAL(convertedFt.GetTagValue("contact:line"), "015qevdv", ()); + } +} diff --git a/editor/xml_feature.cpp b/editor/xml_feature.cpp index 5ac1b1ba85..51021c8376 100644 --- a/editor/xml_feature.cpp +++ b/editor/xml_feature.cpp @@ -3,6 +3,7 @@ #include "indexer/classificator.hpp" #include "indexer/editable_map_object.hpp" #include "indexer/ftypes_matcher.hpp" +#include "indexer/validate_and_format_contacts.hpp" #include "coding/string_utf8_multilang.hpp" @@ -606,7 +607,10 @@ XMLFeature ToXML(osm::EditableMapObject const & object, bool serializeType) object.ForEachMetadataItem([&toFeature](string_view tag, string_view value) { - toFeature.SetTagValue(tag, value); + if (osm::isSocialContactTag(tag) && value.find('/') != std::string::npos) + toFeature.SetTagValue(tag, osm::socialContactToURL(tag, value)); + else + toFeature.SetTagValue(tag, value); }); return toFeature; diff --git a/indexer/indexer_tests/validate_and_format_contacts_test.cpp b/indexer/indexer_tests/validate_and_format_contacts_test.cpp index ea8212f9bf..536876effb 100644 --- a/indexer/indexer_tests/validate_and_format_contacts_test.cpp +++ b/indexer/indexer_tests/validate_and_format_contacts_test.cpp @@ -9,11 +9,15 @@ UNIT_TEST(EditableMapObject_ValidateAndFormat_facebook) TEST_EQUAL(osm::ValidateAndFormat_facebook(""), "", ()); TEST_EQUAL(osm::ValidateAndFormat_facebook("facebook.com/OpenStreetMap"), "OpenStreetMap", ()); TEST_EQUAL(osm::ValidateAndFormat_facebook("www.facebook.com/OpenStreetMap"), "OpenStreetMap", ()); + TEST_EQUAL(osm::ValidateAndFormat_facebook("www.facebook.fr/OpenStreetMap"), "OpenStreetMap", ()); TEST_EQUAL(osm::ValidateAndFormat_facebook("http://facebook.com/OpenStreetMap"), "OpenStreetMap", ()); TEST_EQUAL(osm::ValidateAndFormat_facebook("https://facebook.com/OpenStreetMap"), "OpenStreetMap", ()); TEST_EQUAL(osm::ValidateAndFormat_facebook("http://www.facebook.com/OpenStreetMap"), "OpenStreetMap", ()); TEST_EQUAL(osm::ValidateAndFormat_facebook("https://www.facebook.com/OpenStreetMap"), "OpenStreetMap", ()); + TEST_EQUAL(osm::ValidateAndFormat_facebook("https://de-de.facebook.de/Open_Street_Map"), "Open_Street_Map", ()); TEST_EQUAL(osm::ValidateAndFormat_facebook("https://en-us.facebook.com/OpenStreetMap"), "OpenStreetMap", ()); + TEST_EQUAL(osm::ValidateAndFormat_facebook("https://de-de.facebook.com/profile.php?id=100085707580841"), "100085707580841", ()); + TEST_EQUAL(osm::ValidateAndFormat_facebook("http://facebook.com/profile.php?share=app&id=100086487430889#m"), "100086487430889", ()); TEST_EQUAL(osm::ValidateAndFormat_facebook("some.good.page"), "some.good.page", ()); TEST_EQUAL(osm::ValidateAndFormat_facebook("@tree-house-interiors"), "tree-house-interiors", ()); @@ -61,14 +65,16 @@ UNIT_TEST(EditableMapObject_ValidateAndFormat_twitter) UNIT_TEST(EditableMapObject_ValidateAndFormat_vk) { TEST_EQUAL(osm::ValidateAndFormat_vk("vk.com/id404"), "id404", ()); - TEST_EQUAL(osm::ValidateAndFormat_vk("vkontakte.ru/id404"), "id404", ()); + TEST_EQUAL(osm::ValidateAndFormat_vk("vkontakte.ru/id4321"), "id4321", ()); + TEST_EQUAL(osm::ValidateAndFormat_vk("www.vkontakte.ru/id43210"), "id43210", ()); TEST_EQUAL(osm::ValidateAndFormat_vk("www.vk.com/id404"), "id404", ()); - TEST_EQUAL(osm::ValidateAndFormat_vk("http://vk.com/id404"), "id404", ()); - TEST_EQUAL(osm::ValidateAndFormat_vk("https://vk.com/id404"), "id404", ()); - TEST_EQUAL(osm::ValidateAndFormat_vk("https://vkontakte.ru/id404"), "id404", ()); - TEST_EQUAL(osm::ValidateAndFormat_vk("http://www.vk.com/id404"), "id404", ()); - TEST_EQUAL(osm::ValidateAndFormat_vk("https://www.vk.com/id404"), "id404", ()); - TEST_EQUAL(osm::ValidateAndFormat_vk("https://www.vk.com/id405/"), "id405", ()); + TEST_EQUAL(osm::ValidateAndFormat_vk("http://vk.com/ozon"), "ozon", ()); + TEST_EQUAL(osm::ValidateAndFormat_vk("https://vk.com/sklad169"), "sklad169", ()); + TEST_EQUAL(osm::ValidateAndFormat_vk("https://vkontakte.ru/id4321"), "id4321", ()); + TEST_EQUAL(osm::ValidateAndFormat_vk("https://www.vkontakte.ru/id4321"), "id4321", ()); + TEST_EQUAL(osm::ValidateAndFormat_vk("http://www.vk.com/ugona.net.expert"), "ugona.net.expert", ()); + TEST_EQUAL(osm::ValidateAndFormat_vk("https://www.vk.com/7cvetov18"), "7cvetov18", ()); + TEST_EQUAL(osm::ValidateAndFormat_vk("https://www.vk.com/7cvetov18/"), "7cvetov18", ()); TEST_EQUAL(osm::ValidateAndFormat_vk("@22ab.cdef"), "22ab.cdef", ()); TEST_EQUAL(osm::ValidateAndFormat_vk("instagram.com/hello_world"), "", ()); @@ -81,6 +87,7 @@ UNIT_TEST(EditableMapObject_ValidateAndFormat_contactLine) TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://line.me/ti/p/@dgxs9r6wad"), "dgxs9r6wad", ()); TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://line.me/ti/p/%40vne5uwke17"), "vne5uwke17", ()); TEST_EQUAL(osm::ValidateAndFormat_contactLine("http://line.me/R/ti/p/bfsg1a8x9u"), "bfsg1a8x9u", ()); + TEST_EQUAL(osm::ValidateAndFormat_contactLine("line.me/R/ti/p/bfsg1a8x9u"), "bfsg1a8x9u", ()); TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://line.me/R/ti/p/gdltt7s380"), "gdltt7s380", ()); TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://line.me/R/ti/p/@sdb2pb3lsg"), "sdb2pb3lsg", ()); TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://line.me/R/ti/p/%40b30h5mdj11"), "b30h5mdj11", ()); @@ -91,6 +98,9 @@ UNIT_TEST(EditableMapObject_ValidateAndFormat_contactLine) TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://line.me/R/home/public/profile?id=r90ck7n1rq"), "r90ck7n1rq", ()); TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://page.line.me/fom5198h"), "fom5198h", ()); TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://page.line.me/qn58n8g?web=mobile"), "qn58n8g", ()); + TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://page.line.me/?accountId=673watcr"), "673watcr", ()); + TEST_EQUAL(osm::ValidateAndFormat_contactLine("page.line.me/?accountId=673watcr"), "673watcr", ()); + TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://liff.line.me/1645278921-kWRPP32q/?accountId=673watcr"), "liff.line.me/1645278921-kWRPP32q/?accountId=673watcr", ()); TEST_EQUAL(osm::ValidateAndFormat_contactLine("https://abc.line.me/en/some/page?id=xaladqv"), "abc.line.me/en/some/page?id=xaladqv", ()); TEST_EQUAL(osm::ValidateAndFormat_contactLine("@abcd"), "abcd", ()); TEST_EQUAL(osm::ValidateAndFormat_contactLine("@-hyphen-test-"), "-hyphen-test-", ()); @@ -103,11 +113,14 @@ UNIT_TEST(EditableMapObject_ValidateFacebookPage) TEST(osm::ValidateFacebookPage(""), ()); TEST(osm::ValidateFacebookPage("facebook.com/OpenStreetMap"), ()); TEST(osm::ValidateFacebookPage("www.facebook.com/OpenStreetMap"), ()); + TEST(osm::ValidateFacebookPage("www.facebook.fr/OpenStreetMap"), ()); TEST(osm::ValidateFacebookPage("http://facebook.com/OpenStreetMap"), ()); TEST(osm::ValidateFacebookPage("https://facebook.com/OpenStreetMap"), ()); TEST(osm::ValidateFacebookPage("http://www.facebook.com/OpenStreetMap"), ()); TEST(osm::ValidateFacebookPage("https://www.facebook.com/OpenStreetMap"), ()); + TEST(osm::ValidateFacebookPage("https://de-de.facebook.de/OpenStreetMap"), ()); TEST(osm::ValidateFacebookPage("https://en-us.facebook.com/OpenStreetMap"), ()); + TEST(osm::ValidateFacebookPage("https://www.facebook.com/profile.php?id=100085707580841"), ()); TEST(osm::ValidateFacebookPage("OpenStreetMap"), ()); TEST(osm::ValidateFacebookPage("some.good.page"), ()); TEST(osm::ValidateFacebookPage("Quaama-Volunteer-Bushfire-Brigade-526790054021506"), ()); @@ -248,3 +261,18 @@ UNIT_TEST(EditableMapObject_ValidateLinePage) TEST(!osm::ValidateLinePage("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), ()); TEST(!osm::ValidateLinePage("https://line.com/ti/p/invalid-domain"), ()); } + +UNIT_TEST(EditableMapObject_socialContactToURL) +{ + TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_INSTAGRAM, "some_page_name"), "https://instagram.com/some_page_name", ()); + TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_INSTAGRAM, "p/BvkgKZNDbqN"), "https://instagram.com/p/BvkgKZNDbqN", ()); + TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_FACEBOOK, "100086487430889"), "https://facebook.com/100086487430889", ()); + TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_FACEBOOK, "nova.poshta.official"), "https://facebook.com/nova.poshta.official", ()); + TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_FACEBOOK, "pg/ESQ-336537783591903/about"), "https://facebook.com/pg/ESQ-336537783591903/about", ()); + TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_TWITTER, "carmelopizza"), "https://twitter.com/carmelopizza", ()); + TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_TWITTER, "demhamburguesa/status/688001869269078016"), "https://twitter.com/demhamburguesa/status/688001869269078016", ()); + TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_VK, "beerhousebar"), "https://vk.com/beerhousebar", ()); + TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_VK, "wall-41524_29351"), "https://vk.com/wall-41524_29351", ()); + TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_LINE, "a26235875"), "https://line.me/R/ti/p/@a26235875", ()); + TEST_EQUAL(osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_LINE, "liff.line.me/1645278921-kWRPP32q/?accountId=673watcr"), "https://liff.line.me/1645278921-kWRPP32q/?accountId=673watcr", ()); +} \ No newline at end of file diff --git a/indexer/validate_and_format_contacts.cpp b/indexer/validate_and_format_contacts.cpp index 00c4ee462d..f544a452bb 100644 --- a/indexer/validate_and_format_contacts.cpp +++ b/indexer/validate_and_format_contacts.cpp @@ -17,22 +17,50 @@ 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}$)"); -char const * const kWebsiteProtocols[] = {"http://", "https://"}; -size_t const kWebsiteProtocolDefaultIndex = 0; +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 kProfilePhp{"profile.php"}; + +// Domains constants. +constexpr string_view kFbDot{"fb."}; +constexpr string_view kFacebookDot{"facebook."}; +constexpr string_view kInstagramCom{"instagram.com"}; +constexpr string_view kDotInstagramCom{".instagram.com"}; +constexpr string_view kTwitterCom{"twitter.com"}; +constexpr string_view kDotTwitterCom{".twitter.com"}; +constexpr string_view kVkCom{"vk.com"}; +constexpr string_view kVkontakteRu{"vkontakte.ru"}; +constexpr string_view kDotVkCom{".vk.com"}; +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"}; + +// URLs constants +constexpr string_view kUrlFacebook{"https://facebook.com/"}; +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 kHttp{"http://"}; +constexpr string_view kHttps{"https://"}; size_t GetProtocolNameLength(string const & website) { - for (auto const & protocol : kWebsiteProtocols) - { - if (strings::StartsWith(website, protocol)) - return strlen(protocol); - } + if (strings::StartsWith(website, kHttp)) + return kHttp.size(); + if (strings::StartsWith(website, kHttps)) + return kHttps.size(); return 0; } bool IsProtocolSpecified(string const & website) { - return GetProtocolNameLength(website) > 0; + return strings::StartsWith(website, kHttp) || strings::StartsWith(website, kHttps); } // TODO: Current implementation looks only for restricted symbols from ASCII block ignoring @@ -60,7 +88,7 @@ bool containsInvalidFBSymbol(string const & facebookPage, size_t startIndex = 0) std::string ValidateAndFormat_website(std::string const & v) { if (!v.empty() && !IsProtocolSpecified(v)) - return kWebsiteProtocols[kWebsiteProtocolDefaultIndex] + v; + return string{kHttp}.append(v); return v; } @@ -90,12 +118,18 @@ string ValidateAndFormat_facebook(string const & facebookPage) url::Url const url = url::Url::FromString(facebookPage); string const domain = strings::MakeLowerCase(url.GetHost()); // Check Facebook domain name. - if (strings::EndsWith(domain, "facebook.com") || strings::EndsWith(domain, "fb.com") - || strings::EndsWith(domain, "fb.me") || strings::EndsWith(domain, "facebook.de") - || strings::EndsWith(domain, "facebook.fr")) + if (strings::StartsWith(domain, kFacebookDot) || strings::StartsWith(domain, kFbDot) || + domain.find(".facebook.") != string::npos || domain.find(".fb.") != string::npos) { auto webPath = url.GetPath(); - // Strip last '/' symbol + // In case of https://www.facebook.com/profile.php?id=100085707580841 extract only ID. + if (strings::StartsWith(webPath, kProfilePhp)) + { + std::string const * id = url.GetParamValue("id"); + return (id ? *id : std::string()); + } + + // Strip last '/' symbol. webPath.erase(webPath.find_last_not_of('/') + 1); return webPath; } @@ -120,8 +154,8 @@ string ValidateAndFormat_instagram(string const & instagramPage) url::Url const url = url::Url::FromString(instagramPage); string const domain = strings::MakeLowerCase(url.GetHost()); - // Check Instagram domain name. - if (domain == "instagram.com" || strings::EndsWith(domain, ".instagram.com")) + // Check Instagram domain name: "instagram.com" or "*.instagram.com". + if (domain == kInstagramCom || strings::EndsWith(domain, kDotInstagramCom)) { auto webPath = url.GetPath(); // Strip last '/' symbol. @@ -149,12 +183,12 @@ string ValidateAndFormat_twitter(string const & twitterPage) url::Url const url = url::Url::FromString(twitterPage); string const domain = strings::MakeLowerCase(url.GetHost()); - // Check Twitter domain name. - if (domain == "twitter.com" || strings::EndsWith(domain, ".twitter.com")) + // Check Twitter domain name: "twitter.com" or "*.twitter.com". + if (domain == kTwitterCom || strings::EndsWith(domain, kDotTwitterCom)) { auto webPath = url.GetPath(); - // Strip last '/' symbol and first '@' symbol + // Strip last '/' symbol and first '@' symbol. webPath.erase(webPath.find_last_not_of('/') + 1); webPath.erase(0, webPath.find_first_not_of('@')); @@ -190,9 +224,9 @@ string ValidateAndFormat_vk(string const & vkPage) url::Url const url = url::Url::FromString(vkPage); string const domain = strings::MakeLowerCase(url.GetHost()); - // Check VK domain name. - if (domain == "vk.com" || strings::EndsWith(domain, ".vk.com") || - domain == "vkontakte.ru" || strings::EndsWith(domain, ".vkontakte.ru")) + // Check VK domain name: "vk.com" or "vkontakte.ru" or "*.vk.com" or "*.vkontakte.ru". + if (domain == kVkCom || strings::EndsWith(domain, kDotVkCom) || + domain == kVkontakteRu || strings::EndsWith(domain, kDotVkontakteRu)) { auto webPath = url.GetPath(); // Strip last '/' symbol. @@ -239,13 +273,18 @@ string ValidateAndFormat_contactLine(string const & linePage) url::Url const url = url::Url::FromString(linePage); string const domain = strings::MakeLowerCase(url.GetHost()); // Check Line domain name. - if (domain == "page.line.me") + if (domain == kPageLineMe) { + // Parse https://page.line.me/?accountId={LINE ID} + std::string const * id = url.GetParamValue("accountId"); + if (id != nullptr) + return *id; + // Parse https://page.line.me/{LINE ID} string lineId = url.GetPath(); return stripAtSymbol(lineId); } - else if (domain == "line.me" || strings::EndsWith(domain, ".line.me")) + else if (domain == kLineMe || strings::EndsWith(domain, kDotLineMe)) { auto webPath = url.GetPath(); if (strings::StartsWith(webPath, "R/ti/p/")) @@ -265,14 +304,15 @@ string ValidateAndFormat_contactLine(string const & linePage) // Parse https://line.me/R/home/public/main?id={LINE ID without @} // and https://line.me/R/home/public/profile?id={LINE ID without @} std::string const * id = url.GetParamValue("id"); - return (id? *id : std::string()); + return (id ? *id : std::string()); } else { - if (strings::StartsWith(linePage, "http://")) + if (strings::StartsWith(linePage, kHttp)) return linePage.substr(7); - if (strings::StartsWith(linePage, "https://")) + if (strings::StartsWith(linePage, kHttps)) return linePage.substr(8); + return linePage; } } @@ -289,7 +329,7 @@ bool ValidateWebsite(string const & site) if (startPos >= site.size()) return false; - // Site should contain at least one dot but not at the begining/end. + // Site should contain at least one dot but not at the beginning/end. if ('.' == site[startPos] || '.' == site.back()) return false; @@ -320,7 +360,8 @@ bool ValidateFacebookPage(string const & page) return false; string const domain = strings::MakeLowerCase(url::Url::FromString(page).GetHost()); - return (strings::StartsWith(domain, "facebook.") || strings::StartsWith(domain, "fb.") || + // Validate domain name: "facebook.*" or "fb.*" or "*.facebook.*" or "*.fb.*". + return (strings::StartsWith(domain, kFacebookDot) || strings::StartsWith(domain, kFbDot) || domain.find(".facebook.") != string::npos || domain.find(".fb.") != string::npos); } @@ -337,7 +378,7 @@ bool ValidateInstagramPage(string const & page) return false; string const domain = strings::MakeLowerCase(url::Url::FromString(page).GetHost()); - return domain == "instagram.com" || strings::EndsWith(domain, ".instagram.com"); + return domain == kInstagramCom || strings::EndsWith(domain, kDotInstagramCom); } bool ValidateTwitterPage(string const & page) @@ -349,7 +390,7 @@ bool ValidateTwitterPage(string const & page) return regex_match(page, s_twitterRegex); // Rules are defined here: https://stackoverflow.com/q/11361044 string const domain = strings::MakeLowerCase(url::Url::FromString(page).GetHost()); - return domain == "twitter.com" || strings::EndsWith(domain, ".twitter.com"); + return domain == kTwitterCom || strings::EndsWith(domain, kDotTwitterCom); } bool ValidateVkPage(string const & page) @@ -382,8 +423,8 @@ bool ValidateVkPage(string const & page) return false; string const domain = strings::MakeLowerCase(url::Url::FromString(page).GetHost()); - return domain == "vk.com" || strings::EndsWith(domain, ".vk.com") - || domain == "vkontakte.ru" || strings::EndsWith(domain, ".vkontakte.ru"); + return domain == kVkCom || strings::EndsWith(domain, kDotVkCom) + || domain == kVkontakteRu || strings::EndsWith(domain, kDotVkontakteRu); } bool ValidateLinePage(string const & page) @@ -406,7 +447,70 @@ bool ValidateLinePage(string const & page) string const domain = strings::MakeLowerCase(url::Url::FromString(page).GetHost()); // Check Line domain name. - return (domain == "line.me" || strings::EndsWith(domain, ".line.me")); + return (domain == kLineMe || strings::EndsWith(domain, kDotLineMe)); +} + +bool isSocialContactTag(string_view tag) +{ + return tag == kInstagram || tag == kFacebook || tag == kTwitter || tag == kVk || tag == kLine; +} + +bool isSocialContactTag(MapObject::MetadataID const metaID) +{ + return metaID == MapObject::MetadataID::FMD_CONTACT_INSTAGRAM || + metaID == MapObject::MetadataID::FMD_CONTACT_FACEBOOK || + metaID == MapObject::MetadataID::FMD_CONTACT_TWITTER || + metaID == MapObject::MetadataID::FMD_CONTACT_VK || + metaID == MapObject::MetadataID::FMD_CONTACT_LINE; +} + +// Functions ValidateAndFormat_{facebook,instagram,twitter,vk}(...) by default strip domain name +// from OSM data and user input. This function prepends domain name to generate full URL. +string socialContactToURL(string_view tag, string_view value) +{ + ASSERT(!value.empty(), ()); + + if (tag == kInstagram) + return string{kUrlInstagram}.append(value); + if (tag == kFacebook) + return string{kUrlFacebook}.append(value); + if (tag == kTwitter) + return string{kUrlTwitter}.append(value); + if (tag == kVk) + return string{kUrlVk}.append(value); + if (tag == kLine) + { + if (value.find('/') == string::npos) // 'value' is a username. + return string{kUrlLine}.append(value); + else // 'value' is an URL. + return string{kHttps}.append(value); + } + + return string{value}; +} + +string socialContactToURL(MapObject::MetadataID metaID, string_view value) +{ + ASSERT(!value.empty(), ()); + + switch (metaID) + { + case MapObject::MetadataID::FMD_CONTACT_INSTAGRAM: + return string{kUrlInstagram}.append(value); + case MapObject::MetadataID::FMD_CONTACT_FACEBOOK: + return string{kUrlFacebook}.append(value); + case MapObject::MetadataID::FMD_CONTACT_TWITTER: + return string{kUrlTwitter}.append(value); + case MapObject::MetadataID::FMD_CONTACT_VK: + return string{kUrlVk}.append(value); + case MapObject::MetadataID::FMD_CONTACT_LINE: + if (value.find('/') == string::npos) // 'value' is a username. + return string{kUrlLine}.append(value); + else // 'value' is an URL. + return string{kHttps}.append(value); + default: + return string{value}; + } } } // namespace osm diff --git a/indexer/validate_and_format_contacts.hpp b/indexer/validate_and_format_contacts.hpp index 2d2612b15b..7e430c1656 100644 --- a/indexer/validate_and_format_contacts.hpp +++ b/indexer/validate_and_format_contacts.hpp @@ -2,6 +2,8 @@ #include <string> +#include "map_object.hpp" + namespace osm { std::string ValidateAndFormat_website(std::string const & v); @@ -17,4 +19,9 @@ 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 isSocialContactTag(std::string_view tag); +bool isSocialContactTag(osm::MapObject::MetadataID const metaID); +std::string socialContactToURL(std::string_view tag, std::string_view value); +std::string socialContactToURL(osm::MapObject::MetadataID metaID, std::string_view value); +} // namespace osm diff --git a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.h b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.h index 9533f55927..a5da3f0f29 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.h +++ b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.h @@ -17,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, readonly, nullable) NSString *instagram; @property(nonatomic, readonly, nullable) NSString *twitter; @property(nonatomic, readonly, nullable) NSString *vk; +@property(nonatomic, readonly, nullable) NSString *line; @property(nonatomic, readonly, nullable) NSString *email; @property(nonatomic, readonly, nullable) NSURL *emailUrl; @property(nonatomic, readonly, nullable) NSString *cuisine; diff --git a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.mm b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.mm index 8e265d8ac1..22690d7f60 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.mm +++ b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.mm @@ -4,6 +4,8 @@ #import <CoreApi/StringUtils.h> +#include "indexer/validate_and_format_contacts.hpp" + #include "map/place_page_info.hpp" using namespace place_page; @@ -56,6 +58,7 @@ using namespace osm; case MetadataID::FMD_CONTACT_INSTAGRAM: _instagram = ToNSString(value); break; case MetadataID::FMD_CONTACT_TWITTER: _twitter = ToNSString(value); break; case MetadataID::FMD_CONTACT_VK: _vk = ToNSString(value); break; + case MetadataID::FMD_CONTACT_LINE: _line = ToNSString(value); break; case MetadataID::FMD_OPERATOR: _ppOperator = ToNSString(value); break; case MetadataID::FMD_INTERNET: _wifiAvailable = (rawData.GetInternet() == osm::Internet::No) diff --git a/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/Contents.json b/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/Contents.json new file mode 100644 index 0000000000..62e2d1321d --- /dev/null +++ b/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "ic_placepage_line.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_placepage_line@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_placepage_line@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/ic_placepage_line.png b/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/ic_placepage_line.png new file mode 100644 index 0000000000..f7d4ffcd78 Binary files /dev/null and b/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/ic_placepage_line.png differ diff --git a/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/ic_placepage_line@2x.png b/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/ic_placepage_line@2x.png new file mode 100644 index 0000000000..310d02bde9 Binary files /dev/null and b/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/ic_placepage_line@2x.png differ diff --git a/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/ic_placepage_line@3x.png b/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/ic_placepage_line@3x.png new file mode 100644 index 0000000000..d2cb054132 Binary files /dev/null and b/iphone/Maps/Images.xcassets/Place Page/ic_placepage_line.imageset/ic_placepage_line@3x.png differ diff --git a/iphone/Maps/UI/Editor/MWMEditorViewController.mm b/iphone/Maps/UI/Editor/MWMEditorViewController.mm index 60307afb8a..55a20bd0f0 100644 --- a/iphone/Maps/UI/Editor/MWMEditorViewController.mm +++ b/iphone/Maps/UI/Editor/MWMEditorViewController.mm @@ -26,6 +26,7 @@ #import <CoreApi/StringUtils.h> #include "platform/localization.hpp" +#include "indexer/validate_and_format_contacts.hpp" namespace { @@ -416,10 +417,15 @@ void registerCellsForTableView(std::vector<MWMEditorCellID> const & cells, UITab icon:(NSString * _Nonnull)icon placeholder:(NSString * _Nonnull)name { + MetadataID metaId = static_cast<MetadataID>(cellID); + NSString* value = ToNSString(m_mapObject.GetMetadata(metaId)); + if (osm::isSocialContactTag(metaId) && [value containsString:@"/"]) + value = ToNSString(osm::socialContactToURL(metaId, [value UTF8String])); + MWMEditorTextTableViewCell * tCell = static_cast<MWMEditorTextTableViewCell *>(cell); [tCell configWithDelegate:self icon:[UIImage imageNamed:icon] - text:ToNSString(m_mapObject.GetMetadata(static_cast<MetadataID>(cellID))) + text:value placeholder:name keyboardType:UIKeyboardTypeDefault capitalization:UITextAutocapitalizationTypeSentences]; @@ -662,6 +668,14 @@ void registerCellsForTableView(std::vector<MWMEditorCellID> const & cells, UITab placeholder:L(@"vk")]; break; } + case MetadataID::FMD_CONTACT_LINE: + { + [self configTextViewCell:cell + cellID:cellID + icon:@"ic_placepage_line" + placeholder:L(@"line")]; + break; + } case MWMEditorCellTypeNote: { MWMNoteCell * tCell = static_cast<MWMNoteCell *>(cell); diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift b/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift index 8f2428e6d7..a94cddb255 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift @@ -58,6 +58,7 @@ protocol PlacePageInfoViewControllerDelegate: AnyObject { func didPressInstagram() func didPressTwitter() func didPressVk() + func didPressLine() func didPressEmail() } @@ -84,6 +85,7 @@ class PlacePageInfoViewController: UIViewController { private var instagramView: InfoItemViewController? private var twitterView: InfoItemViewController? private var vkView: InfoItemViewController? + private var lineView: InfoItemViewController? private var cuisineView: InfoItemViewController? private var operatorView: InfoItemViewController? private var wifiView: InfoItemViewController? @@ -166,28 +168,34 @@ class PlacePageInfoViewController: UIViewController { } if let facebook = placePageInfoData.facebook { - facebookView = createInfoItem("@" + facebook, icon: UIImage(named: "ic_placepage_facebook"), style: .link) { [weak self] in + facebookView = createInfoItem(facebook, icon: UIImage(named: "ic_placepage_facebook"), style: .link) { [weak self] in self?.delegate?.didPressFacebook() } } if let instagram = placePageInfoData.instagram { - instagramView = createInfoItem("@" + instagram, icon: UIImage(named: "ic_placepage_instagram"), style: .link) { [weak self] in + instagramView = createInfoItem(instagram, icon: UIImage(named: "ic_placepage_instagram"), style: .link) { [weak self] in self?.delegate?.didPressInstagram() } } if let twitter = placePageInfoData.twitter { - twitterView = createInfoItem("@" + twitter, icon: UIImage(named: "ic_placepage_twitter"), style: .link) { [weak self] in + twitterView = createInfoItem(twitter, icon: UIImage(named: "ic_placepage_twitter"), style: .link) { [weak self] in self?.delegate?.didPressTwitter() } } if let vk = placePageInfoData.vk { - vkView = createInfoItem("@" + vk, icon: UIImage(named: "ic_placepage_vk"), style: .link) { [weak self] in + vkView = createInfoItem(vk, icon: UIImage(named: "ic_placepage_vk"), style: .link) { [weak self] in self?.delegate?.didPressVk() } } + + if let line = placePageInfoData.line { + lineView = createInfoItem(line, icon: UIImage(named: "ic_placepage_line"), style: .link) { [weak self] in + self?.delegate?.didPressLine() + } + } if let address = placePageInfoData.address { addressView = createInfoItem(address, icon: UIImage(named: "ic_placepage_adress")) diff --git a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift index 5eec841fcb..95cd6a50fd 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageInteractor.swift @@ -57,6 +57,10 @@ extension PlacePageInteractor: PlacePageInfoViewControllerDelegate { MWMPlacePageManagerHelper.openVk(placePageData) } + func didPressLine() { + MWMPlacePageManagerHelper.openLine(placePageData) + } + func didPressEmail() { MWMPlacePageManagerHelper.openEmail(placePageData) } diff --git a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm index d4d7b34603..a3f09197a7 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm +++ b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManager.mm @@ -10,9 +10,12 @@ #import "location_util.h" #import <CoreApi/CoreApi.h> +#import <CoreApi/StringUtils.h> #include "platform/downloader_defines.hpp" +#include "indexer/validate_and_format_contacts.hpp" + using namespace storage; @interface MWMPlacePageManager () @@ -238,19 +241,28 @@ using namespace storage; } - (void)openFacebook:(PlacePageData *)data { - [self.ownerViewController openUrl:[NSString stringWithFormat:@"https://m.facebook.com/%@", data.infoData.facebook]]; + std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_FACEBOOK, [data.infoData.facebook UTF8String]); + [self.ownerViewController openUrl:ToNSString(fullUrl)]; } - (void)openInstagram:(PlacePageData *)data { - [self.ownerViewController openUrl:[NSString stringWithFormat:@"https://instagram.com/%@", data.infoData.instagram]]; + std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_INSTAGRAM, [data.infoData.instagram UTF8String]); + [self.ownerViewController openUrl:ToNSString(fullUrl)]; } - (void)openTwitter:(PlacePageData *)data { - [self.ownerViewController openUrl:[NSString stringWithFormat:@"https://mobile.twitter.com/%@", data.infoData.twitter]]; + std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_TWITTER, [data.infoData.twitter UTF8String]); + [self.ownerViewController openUrl:ToNSString(fullUrl)]; } - (void)openVk:(PlacePageData *)data { - [self.ownerViewController openUrl:[NSString stringWithFormat:@"https://vk.com/%@", data.infoData.vk]]; + std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_VK, [data.infoData.vk UTF8String]); + [self.ownerViewController openUrl:ToNSString(fullUrl)]; +} + +- (void)openLine:(PlacePageData *)data { + std::string const fullUrl = osm::socialContactToURL(osm::MapObject::MetadataID::FMD_CONTACT_LINE, [data.infoData.line UTF8String]); + [self.ownerViewController openUrl:ToNSString(fullUrl)]; } - (void)openEmail:(PlacePageData *)data { diff --git a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.h b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.h index 669eea8ca2..13fffc6b67 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.h +++ b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.h @@ -15,6 +15,7 @@ + (void)openInstagram:(PlacePageData *)data; + (void)openTwitter:(PlacePageData *)data; + (void)openVk:(PlacePageData *)data; ++ (void)openLine:(PlacePageData *)data; + (void)call:(PlacePageData *)data; + (void)showAllFacilities:(PlacePageData *)data; + (void)showPlaceDescription:(NSString *)htmlString; diff --git a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.mm b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.mm index 099d1272eb..6fe4622cda 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.mm +++ b/iphone/Maps/UI/PlacePage/PlacePageManager/MWMPlacePageManagerHelper.mm @@ -22,6 +22,7 @@ - (void)openInstagram:(PlacePageData *)data; - (void)openTwitter:(PlacePageData *)data; - (void)openVk:(PlacePageData *)data; +- (void)openLine:(PlacePageData *)data; - (void)call:(PlacePageData *)data; - (void)showAllFacilities:(PlacePageData *)data; - (void)showPlaceDescription:(NSString *)htmlString; @@ -98,6 +99,10 @@ [[MWMMapViewControlsManager manager].placePageManager openVk:data]; } ++ (void)openLine:(PlacePageData *)data { + [[MWMMapViewControlsManager manager].placePageManager openLine:data]; +} + + (void)call:(PlacePageData *)data { [[MWMMapViewControlsManager manager].placePageManager call:data]; }