diff --git a/src/App/Base.lproj/MainStoryboard.storyboard b/src/App/Base.lproj/MainStoryboard.storyboard
index 39d636453..8840c3705 100644
--- a/src/App/Base.lproj/MainStoryboard.storyboard
+++ b/src/App/Base.lproj/MainStoryboard.storyboard
@@ -321,7 +321,7 @@
-
+
diff --git a/src/App/Localizable.xcstrings b/src/App/Localizable.xcstrings
index 830f99811..f58c3a7ad 100644
--- a/src/App/Localizable.xcstrings
+++ b/src/App/Localizable.xcstrings
@@ -26547,6 +26547,9 @@
}
}
},
+ "Changes saved in changeset %@" : {
+ "comment" : "A message that appears in a banner at the top of the screen after a set of changes has been uploaded to the OpenStreetMap server. The argument is the changeset ID."
+ },
"Changeset" : {
"comment" : "OSM changeset identifier",
"localizations" : {
@@ -64023,148 +64026,6 @@
}
}
},
- "Upload complete!" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "اكتمل رفع البيانات!"
- }
- },
- "ars" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "اكتمل رفع البيانات!"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nahrávání dokončeno"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Hochladen abgeschlossen!"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Se completó la carga."
- }
- },
- "et" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Üleslaadimine lõpetatud!"
- }
- },
- "eu" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Igorria!"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Téléversement effectué !"
- }
- },
- "hr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Prijenos gotov!"
- }
- },
- "id" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pengunggahan selesai!"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "アップロード完了!"
- }
- },
- "nb-NO" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Opplasting fullført!"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Uploaden beëindigd!"
- }
- },
- "pt" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Envio terminado!"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Envio completo!"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Загрузка завершена!"
- }
- },
- "ta" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "பதிவேற்றம் முடிந்தது!"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Yükleme tamamlandı!"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Завантаження завершено!"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Đã tải lên xong!"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "上传完成!"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "上傳完畢!"
- }
- },
- "zh-Hant-HK" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "上傳完畢!"
- }
- }
- }
- },
"Upload succeeded but the server returned mismatched data.\n\nYou should use Clear Cache > OSM Data before continuing to map." : {
"comment" : "'Clear Cache' and 'OSM Data' should match what your language uses for the same terms in Display settings",
"localizations" : {
diff --git a/src/App/MainViewController.swift b/src/App/MainViewController.swift
index 55d20ca65..a0e3abe98 100644
--- a/src/App/MainViewController.swift
+++ b/src/App/MainViewController.swift
@@ -338,6 +338,7 @@ final class MainViewController: UIViewController, DPadDelegate,
// tell our error display manager where to display messages
MessageDisplay.shared.topViewController = self
MessageDisplay.shared.flashLabel = flashLabel
+ MessageDisplay.shared.flashLinkConstraint = flashLabel.bottomAnchor.constraint(equalTo: rulerView.topAnchor, constant: -12)
// Install gesture recognizers
diff --git a/src/App/MessageDisplay.swift b/src/App/MessageDisplay.swift
index 19304e776..82ab1745e 100644
--- a/src/App/MessageDisplay.swift
+++ b/src/App/MessageDisplay.swift
@@ -6,9 +6,10 @@
// Copyright © 2026 Bryce Cogswell. All rights reserved.
//
+import SafariServices
import UIKit
-class MessageDisplay {
+class MessageDisplay: NSObject {
static let shared = MessageDisplay()
weak var flashLabel: HUDLabel! {
@@ -20,14 +21,22 @@ class MessageDisplay {
flashLabel.backgroundColor = .black.withAlphaComponent(0.7)
flashLabel.textColor = .white
flashLabel.contentInsets = .init(top: 4, left: 12, bottom: 4, right: 12)
+ flashLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(flashTapped)))
+ flashLabel.layer.cornerCurve = .continuous
+ flashBlur.frame = flashLabel.bounds
+ flashBlur.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ flashLabel.insertSubview(flashBlur, at: 0)
}
}
weak var topViewController: UIViewController?
+ var flashLinkConstraint: NSLayoutConstraint?
var lastErrorDate: Date? // to prevent spamming of error dialogs
var ignoreNetworkErrorsUntilDate: Date?
private var flashGeneration = 0
+ private var flashLink: URL?
+ private lazy var flashBlur = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial))
func showAlert(_ alert: UIAlertController) {
topViewController?.present(alert, animated: true)
@@ -105,8 +114,35 @@ class MessageDisplay {
}
}
- func flashMessage(title: String?, message: String, duration: TimeInterval = 1.0) {
- flashLabel.text = title.map { $0 + "\n\n" + message } ?? message
+ @objc private func flashTapped() {
+ flashLink.map { topViewController?.present(SFSafariViewController(url: $0), animated: true) }
+ }
+
+ func flashMessage(title: String?, message: String, duration: TimeInterval = 1.0, link: URL? = nil) {
+ flashLink = link
+ let text = title.map { $0 + "\n\n" + message } ?? message
+ let font = UIFont.preferredFont(forTextStyle: link == nil ? .title3 : .headline)
+ let attributed = NSMutableAttributedString(string: link == nil ? text : " \(text) ", attributes: [
+ .font: font,
+ .foregroundColor: link == nil ? UIColor.white : UIColor.label
+ ])
+ if link != nil {
+ func symbol(_ name: String, _ color: UIColor) -> NSAttributedString {
+ let image = UIImage(systemName: name, withConfiguration: UIImage.SymbolConfiguration(font: font))
+ return NSAttributedString(attachment: NSTextAttachment(image: image!.withTintColor(color, renderingMode: .alwaysOriginal)))
+ }
+ attributed.insert(symbol("checkmark.circle.fill", .systemGreen), at: 0)
+ attributed.append(symbol("chevron.right", .tertiaryLabel))
+ }
+ flashBlur.isHidden = link == nil
+ flashLinkConstraint?.isActive = link != nil
+ flashLabel.numberOfLines = link == nil ? 0 : 1
+ flashLabel.adjustsFontSizeToFitWidth = link != nil
+ flashLabel.backgroundColor = link == nil ? .black.withAlphaComponent(0.7) : .clear
+ flashLabel.layer.cornerRadius = link == nil ? 5 : 16
+ flashLabel.contentInsets = link == nil ? .init(top: 4, left: 12, bottom: 4, right: 12)
+ : .init(top: 12, left: 16, bottom: 12, right: 16)
+ flashLabel.attributedText = attributed
flashLabel.superview?.bringSubviewToFront(flashLabel)
if flashLabel.isHidden {
diff --git a/src/Shared/CustomViews/HUDLabel.swift b/src/Shared/CustomViews/HUDLabel.swift
index a31e66b08..e97971317 100644
--- a/src/Shared/CustomViews/HUDLabel.swift
+++ b/src/Shared/CustomViews/HUDLabel.swift
@@ -15,6 +15,7 @@ public final class HUDLabel: UIView {
let l = UILabel()
l.translatesAutoresizingMaskIntoConstraints = false
l.numberOfLines = 0
+ l.minimumScaleFactor = 0.5
return l
}()
@@ -58,6 +59,11 @@ public final class HUDLabel: UIView {
set { label.numberOfLines = newValue }
}
+ public var adjustsFontSizeToFitWidth: Bool {
+ get { label.adjustsFontSizeToFitWidth }
+ set { label.adjustsFontSizeToFitWidth = newValue }
+ }
+
public var adjustsFontForContentSizeCategory: Bool {
get { label.adjustsFontForContentSizeCategory }
set { label.adjustsFontForContentSizeCategory = newValue }
diff --git a/src/Shared/OSMModels/OsmMapData.swift b/src/Shared/OSMModels/OsmMapData.swift
index 44dd90828..8c18d3f4b 100644
--- a/src/Shared/OSMModels/OsmMapData.swift
+++ b/src/Shared/OSMModels/OsmMapData.swift
@@ -1013,7 +1013,7 @@ final class OsmMapData: NSObject, NSSecureCoding {
imagery: String,
generator: String,
locale: String,
- groupIds: Set) async throws
+ groupIds: Set) async throws -> Int64
{
consistencyCheck()
@@ -1064,6 +1064,7 @@ final class OsmMapData: NSObject, NSSecureCoding {
// The upload is complete and our state is consistent, but tell the user to refresh.
throw Error.serverSkippedDelete
}
+ return changesetID
}
/// Upload xml generated by mapData.
@@ -1085,11 +1086,11 @@ final class OsmMapData: NSObject, NSSecureCoding {
source: String,
imagery: String,
generator: String,
- locale: String) async throws
+ locale: String) async throws -> Int64
{
let objects = groups.reduce(into: Set()) { $0.formUnion($1.objects) }
let groupIds = groups.reduce(into: Set()) { $0.formUnion($1.undoGroups) }
- try await uploadChangeset(.objects(objects),
+ return try await uploadChangeset(.objects(objects),
comment: comment,
source: source,
imagery: imagery,
diff --git a/src/Upload/UploadViewController.swift b/src/Upload/UploadViewController.swift
index b6c6c750b..bff36e107 100644
--- a/src/Upload/UploadViewController.swift
+++ b/src/Upload/UploadViewController.swift
@@ -805,7 +805,7 @@ class UploadViewController: UIViewController {
let imagery = currentImagery()
do {
- try await mapData.uploadChangeset(for: selectedGroups,
+ let changesetID = try await mapData.uploadChangeset(for: selectedGroups,
comment: comment,
source: source,
imagery: imagery,
@@ -814,9 +814,12 @@ class UploadViewController: UIViewController {
dismiss(animated: true)
MainActor.runAfter(nanoseconds: 300_000000) {
appDelegate.mapView.setNeedsLayout()
- MessageDisplay.shared.flashMessage(title: nil,
- message: NSLocalizedString("Upload complete!", comment: ""),
- duration: 1.5)
+ MessageDisplay.shared.flashMessage(
+ title: nil,
+ message: String.localizedStringWithFormat(NSLocalizedString("Changes saved in changeset %@", comment: ""),
+ String(changesetID)),
+ duration: 4.0,
+ link: OSM_SERVER.serverURL.appendingPathComponent("changeset/\(changesetID)"))
var editCount = UserPrefs.shared.uploadCountPerVersion.value ?? 0
editCount += 1
UserPrefs.shared.uploadCountPerVersion.value = editCount
diff --git a/src/Upload/XmlEditorViewController.swift b/src/Upload/XmlEditorViewController.swift
index 69b13dde6..803cdcadd 100644
--- a/src/Upload/XmlEditorViewController.swift
+++ b/src/Upload/XmlEditorViewController.swift
@@ -82,7 +82,7 @@ final class XmlEditorViewController: UIViewController {
do {
let xmlDoc = try DDXMLDocument(xmlString: textView.text ?? "", options: 0)
- try await mapData.uploadChangeset(.xml(xmlDoc),
+ let changesetID = try await mapData.uploadChangeset(.xml(xmlDoc),
comment: comment,
source: source,
imagery: imagery,
@@ -95,8 +95,10 @@ final class XmlEditorViewController: UIViewController {
appDelegate.mapView.setNeedsLayout()
MessageDisplay.shared.flashMessage(
title: nil,
- message: NSLocalizedString("Upload complete!", comment: ""),
- duration: 1.5)
+ message: String.localizedStringWithFormat(NSLocalizedString("Changes saved in changeset %@", comment: ""),
+ String(changesetID)),
+ duration: 4.0,
+ link: OSM_SERVER.serverURL.appendingPathComponent("changeset/\(changesetID)"))
var editCount = UserPrefs.shared.uploadCountPerVersion.value ?? 0
editCount += 1
UserPrefs.shared.uploadCountPerVersion.value = editCount