diff --git a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.h b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.h index 06fd51117..2e835b0e3 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.h +++ b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.h @@ -8,6 +8,8 @@ NS_ASSUME_NONNULL_BEGIN @interface PlacePageInfoData : NSObject @property(nonatomic, readonly, nullable) NSString *openingHoursString; +@property(nonatomic, readonly, nullable) NSDate *checkDate; +@property(nonatomic, readonly, nullable) NSDate *checkDateOpeningHours; @property(nonatomic, readonly, nullable) OpeningHours *openingHours; @property(nonatomic, readonly) NSArray *phones; @property(nonatomic, readonly, nullable) NSString *website; @@ -38,6 +40,8 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, readonly, nullable) NSString *outdoorSeating; @property(nonatomic, readonly, nullable) NSString *network; +- (NSDate * _Nullable)getMostRecentCheckDate; + @end NS_ASSUME_NONNULL_END diff --git a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.mm b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.mm index 12d0c9025..520402756 100644 --- a/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.mm +++ b/iphone/CoreApi/CoreApi/PlacePageData/Common/PlacePageInfoData.mm @@ -20,8 +20,41 @@ NSString * GetLocalizedMetadataValueString(MapObject::MetadataID metaID, std::st return ToNSString(platform::GetLocalizedTypeName(feature::ToString(metaID) + "." + value)); } +/// Parse date string in YYYY-MM-DD format to NSDate +NSDate * _Nullable ParseDateString(NSString * _Nullable dateString) { + if (!dateString || dateString.length == 0) { + return nil; + } + + static NSDateFormatter *dateFormatter = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateFormat = @"yyyy-MM-dd"; + dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; + dateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + }); + + return [dateFormatter dateFromString:dateString]; +} + @implementation PlacePageInfoData +- (NSDate * _Nullable)getMostRecentCheckDate { + // CheckDate can utilize checkDateOpeningHours if that is available + // As surveying opening hours would confirm presence + if (_checkDate && _checkDateOpeningHours) { + // Both available - return the more recent date + return [_checkDate compare:_checkDateOpeningHours] == NSOrderedDescending ? _checkDate : _checkDateOpeningHours; + } else if (_checkDate) { + return _checkDate; + } else if (_checkDateOpeningHours) { + return _checkDateOpeningHours; + } else { + return nil; + } +} + @end @implementation PlacePageInfoData (Core) @@ -45,6 +78,12 @@ NSString * GetLocalizedMetadataValueString(MapObject::MetadataID metaID, std::st _openingHours = [[OpeningHours alloc] initWithRawString:_openingHoursString localization:localization]; break; + case MetadataID::FMD_CHECK_DATE: + _checkDate = ParseDateString(ToNSString(value)); + break; + case MetadataID::FMD_CHECK_DATE_OPEN_HOURS: + _checkDateOpeningHours = ParseDateString(ToNSString(value)); + break; case MetadataID::FMD_PHONE_NUMBER: { NSArray *phones = [ToNSString(value) componentsSeparatedByString:@";"]; diff --git a/iphone/Maps/Categories/Date+TimeAgo.swift b/iphone/Maps/Categories/Date+TimeAgo.swift new file mode 100644 index 000000000..be1e60ad5 --- /dev/null +++ b/iphone/Maps/Categories/Date+TimeAgo.swift @@ -0,0 +1,13 @@ +import Foundation + +extension Date { + func formatTimeAgo() -> String { + return Self.relativeFormatter.localizedString(for: self, relativeTo: Date()) + } + + private static let relativeFormatter: RelativeDateTimeFormatter = { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + return formatter + }() +} \ No newline at end of file diff --git a/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings b/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings index 2887d8397..aac15f835 100644 --- a/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings +++ b/iphone/Maps/LocalizedStrings/en.lproj/Localizable.strings @@ -552,6 +552,12 @@ "editor_report_problem_send_button" = "Send"; "autodownload" = "Auto-download maps"; + + +/* Place page confirmation messages and time ago formatting */ +"existence_confirmed_time_ago" = "Existence confirmed %@"; +"hours_confirmed_time_ago" = "Confirmed %@"; + /* Place Page opening hours text */ "closed_now" = "Closed now"; diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index 0fbc548b7..8cf5747b1 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -321,6 +321,7 @@ 676507601C10559800830BB3 /* patterns.txt in Resources */ = {isa = PBXBuildFile; fileRef = 451950391B7A3E070085DA05 /* patterns.txt */; }; 676507611C10559B00830BB3 /* colors.txt in Resources */ = {isa = PBXBuildFile; fileRef = 452FCA3A1B6A3DF7007019AB /* colors.txt */; }; 6B9978361C89A316003B8AA0 /* editor.config in Resources */ = {isa = PBXBuildFile; fileRef = 6B9978341C89A316003B8AA0 /* editor.config */; }; + 8325C4E12E45519600457516 /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8325C4E02E45519600457516 /* Date+TimeAgo.swift */; }; 8C4FB9C72BEFEFF400D44877 /* CarPlayWindowScaleAdjuster.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C4FB9C62BEFEFF400D44877 /* CarPlayWindowScaleAdjuster.swift */; }; 8CB13C3B2BF1276A004288F2 /* CarplayPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB13C3A2BF1276A004288F2 /* CarplayPlaceholderView.swift */; }; 99012847243F0D6900C72B10 /* UIViewController+alternative.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99012846243F0D6900C72B10 /* UIViewController+alternative.swift */; }; @@ -1208,6 +1209,7 @@ 5605022E1B6211E100169CAD /* sound-strings */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "sound-strings"; path = "../../data/sound-strings"; sourceTree = ""; }; 6741AA5D1BF340DE002C974C /* CoMaps (Debug).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "CoMaps (Debug).app"; sourceTree = BUILT_PRODUCTS_DIR; }; 6B9978341C89A316003B8AA0 /* editor.config */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = editor.config; path = ../../data/editor.config; sourceTree = ""; }; + 8325C4E02E45519600457516 /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; 8C4FB9C62BEFEFF400D44877 /* CarPlayWindowScaleAdjuster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayWindowScaleAdjuster.swift; sourceTree = ""; }; 8CB13C3A2BF1276A004288F2 /* CarplayPlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarplayPlaceholderView.swift; sourceTree = ""; }; 8D1107310486CEB800E47090 /* CoMaps.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = CoMaps.plist; plistStructureDefinitionIdentifier = "com.apple.xcode.plist.structure-definition.iphone.info-plist"; sourceTree = ""; }; @@ -2285,6 +2287,7 @@ 3454D7981E07F045004AF2AD /* Categories */ = { isa = PBXGroup; children = ( + 8325C4E02E45519600457516 /* Date+TimeAgo.swift */, ED83880E2D54DEA4002A0536 /* UIImage+FilledWithColor.swift */, 99A614E223CDD1D900D8D8D0 /* UIButton+RuntimeAttributes.h */, 99A614E323CDD1D900D8D8D0 /* UIButton+RuntimeAttributes.m */, @@ -4637,6 +4640,7 @@ 998927382449E60200260CE2 /* BottomMenuPresenter.swift in Sources */, 27697F832E254AA100FBD913 /* EmbeddedSafariView.swift in Sources */, F6E2FE821E097BA00083EBEC /* MWMPlacePageOpeningHoursDayView.m in Sources */, + 8325C4E12E45519600457516 /* Date+TimeAgo.swift in Sources */, F6E2FD6B1E097BA00083EBEC /* MWMMapDownloaderSubplaceTableViewCell.m in Sources */, CDCA27842245090900167D87 /* ListenerContainer.swift in Sources */, 27AF18582E1DB63A00CD41E2 /* Appearance.swift in Sources */, diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderBuilder.swift b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderBuilder.swift index 73d52a8e5..389a2da89 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderBuilder.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderBuilder.swift @@ -5,8 +5,7 @@ class PlacePageHeaderBuilder { let storyboard = UIStoryboard.instance(.placePage) let viewController = storyboard.instantiateViewController(ofType: PlacePageHeaderViewController.self); let presenter = PlacePageHeaderPresenter(view: viewController, - placePagePreviewData: data.previewData, - objectType: data.objectType, + placePageData: data, delegate: delegate, headerType: headerType) diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift index daf2b7f32..169dfd25f 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderPresenter.swift @@ -23,26 +23,28 @@ class PlacePageHeaderPresenter { private weak var view: PlacePageHeaderViewProtocol? private let placePagePreviewData: PlacePagePreviewData + private let placePageData: PlacePageData let objectType: PlacePageObjectType private weak var delegate: PlacePageHeaderViewControllerDelegate? private let headerType: HeaderType init(view: PlacePageHeaderViewProtocol, - placePagePreviewData: PlacePagePreviewData, - objectType: PlacePageObjectType, + placePageData: PlacePageData, delegate: PlacePageHeaderViewControllerDelegate?, headerType: HeaderType) { self.view = view self.delegate = delegate - self.placePagePreviewData = placePagePreviewData - self.objectType = objectType + self.placePageData = placePageData + self.placePagePreviewData = placePageData.previewData + self.objectType = placePageData.objectType self.headerType = headerType } } extension PlacePageHeaderPresenter: PlacePageHeaderPresenterProtocol { func configure() { - view?.setTitle(placePagePreviewData.title, secondaryTitle: placePagePreviewData.secondaryTitle) + let existenceConfirmation = getExistenceConfirmationText() + view?.setTitle(placePagePreviewData.title, secondaryTitle: placePagePreviewData.secondaryTitle, existenceConfirmation: existenceConfirmation) switch headerType { case .flexible: view?.isExpandViewHidden = false @@ -68,4 +70,10 @@ extension PlacePageHeaderPresenter: PlacePageHeaderPresenterProtocol { func onExportTrackButtonPress(_ type: KmlFileType, from sourceView: UIView) { delegate?.previewDidPressExportTrack(type, from: sourceView) } + + private func getExistenceConfirmationText() -> String? { + guard let mostRecentDate = placePageData.infoData?.getMostRecentCheckDate() else { return nil } + let timeAgoText = mostRecentDate.formatTimeAgo() + return String(format: L("existence_confirmed_time_ago"), timeAgoText) + } } diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift index b77bfda01..f04804a03 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePageHeader/PlacePageHeaderViewController.swift @@ -3,7 +3,7 @@ protocol PlacePageHeaderViewProtocol: AnyObject { var isExpandViewHidden: Bool { get set } var isShadowViewHidden: Bool { get set } - func setTitle(_ title: String?, secondaryTitle: String?) + func setTitle(_ title: String?, secondaryTitle: String?, existenceConfirmation: String?) func showShareTrackMenu() } @@ -77,7 +77,7 @@ extension PlacePageHeaderViewController: PlacePageHeaderViewProtocol { } } - func setTitle(_ title: String?, secondaryTitle: String?) { + func setTitle(_ title: String?, secondaryTitle: String?, existenceConfirmation: String? = nil) { titleText = title secondaryText = secondaryTitle // XCode 13 is not smart enough to detect that title is used below, and requires explicit unwrapped variable. @@ -93,6 +93,18 @@ extension PlacePageHeaderViewController: PlacePageHeaderViewProtocol { let attributedText = NSMutableAttributedString(string: unwrappedTitle, attributes: titleAttributes) + // Add existence confirmation if available + if let existenceText = existenceConfirmation { + let existenceParagraphStyle = NSMutableParagraphStyle() + existenceParagraphStyle.paragraphSpacingBefore = 1 + let existenceAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: 11), + .foregroundColor: UIColor.blackSecondaryText(), + .paragraphStyle: existenceParagraphStyle + ] + attributedText.append(NSAttributedString(string: "\n" + existenceText, attributes: existenceAttributes)) + } + guard let unwrappedSecondaryTitle = secondaryTitle else { titleLabel?.attributedText = attributedText return diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePagePreviewViewController.swift b/iphone/Maps/UI/PlacePage/Components/PlacePagePreviewViewController.swift index 589efd375..638d6b533 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePagePreviewViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePagePreviewViewController.swift @@ -40,6 +40,8 @@ final class PlacePagePreviewViewController: UIViewController { } } + var placePageData: PlacePageData? + private var distance: String? = nil private var speedAndAltitude: String? = nil private var heading: CGFloat? = nil @@ -84,6 +86,9 @@ final class PlacePagePreviewViewController: UIViewController { subtitleString.append(NSAttributedString(string: !subtitleString.string.isEmpty ? " • " + subtitle : subtitle, attributes: [.foregroundColor : UIColor.blackSecondaryText(), .font : UIFont.regular14()])) + } + + if !subtitleString.string.isEmpty { subtitleLabel.attributedText = subtitleString subtitleContainerView.isHidden = false } else { @@ -254,6 +259,16 @@ final class PlacePagePreviewViewController: UIViewController { NSAttributedString.Key.foregroundColor: UIColor.blackSecondaryText()]) attributedString.append(detailsString) } + + if let openingHoursDate = placePageData?.infoData?.checkDateOpeningHours { + let timeAgoText = openingHoursDate.formatTimeAgo() + let openingHoursDateString = NSAttributedString(string: " • " + String(format: L("hours_confirmed_time_ago"), timeAgoText), + attributes: [NSAttributedString.Key.font: UIFont.regular12(), + NSAttributedString.Key.foregroundColor: UIColor.blackSecondaryText()]) + attributedString.append(openingHoursDateString) + } + scheduleLabel.attributedText = attributedString } + } diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift index 151ba5054..e72bf933a 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageCommonLayout.swift @@ -33,6 +33,7 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout { private lazy var previewViewController: PlacePagePreviewViewController = { let vc = storyboard.instantiateViewController(ofType: PlacePagePreviewViewController.self) vc.placePagePreviewData = placePageData.previewData + vc.placePageData = placePageData return vc }() @@ -110,6 +111,7 @@ class PlacePageCommonLayout: NSObject, IPlacePageLayout { guard let self = self else { return } self.actionBarViewController.updateBookmarkButtonState(isSelected: self.placePageData.bookmarkData != nil) self.previewViewController.placePagePreviewData = self.placePageData.previewData + self.previewViewController.placePageData = self.placePageData self.updateBookmarkRelatedSections() } diff --git a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageTrackLayout.swift b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageTrackLayout.swift index 1126dc04c..3759de22e 100644 --- a/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageTrackLayout.swift +++ b/iphone/Maps/UI/PlacePage/PlacePageLayout/Layouts/PlacePageTrackLayout.swift @@ -28,6 +28,7 @@ class PlacePageTrackLayout: IPlacePageLayout { private lazy var previewViewController: PlacePagePreviewViewController = { let vc = storyboard.instantiateViewController(ofType: PlacePagePreviewViewController.self) vc.placePagePreviewData = placePageData.previewData + vc.placePageData = placePageData return vc }() @@ -80,6 +81,7 @@ class PlacePageTrackLayout: IPlacePageLayout { placePageData.onBookmarkStatusUpdate = { [weak self] in guard let self = self else { return } self.previewViewController.placePagePreviewData = self.placePageData.previewData + self.previewViewController.placePageData = self.placePageData self.updateTrackRelatedSections() } @@ -113,6 +115,7 @@ private extension PlacePageTrackLayout { } if let previewViewController = headerViewControllers.compactMap({ $0 as? PlacePagePreviewViewController }).first { previewViewController.placePagePreviewData = previewData + previewViewController.placePageData = self.placePageData previewViewController.updateViews() } presenter?.layoutIfNeeded()