input: convert pasted npub/nprofile to mention with async profile fetch

When pasting an npub or nprofile into the post composer, automatically
convert it to a human-readable mention link. If the profile isn't
cached locally, fetch it from relays and update the mention display
name when it arrives.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changelog-Added: Added automatic conversion of pasted npub/nprofile to human-readable mentions in post composer
Closes: https://github.com/damus-io/damus/issues/2289
Closes: https://github.com/damus-io/damus/pull/3473
Co-Authored-By: Claude Opus 4.5
Tested-by: William Casarin <jb55@jb55.com>
Signed-off-by: alltheseas
Reviewed-by: William Casarin <jb55@jb55.com>
This commit is contained in:
alltheseas
2025-12-26 17:16:07 -06:00
committed by Daniel D’Aquino
parent a4ad4960c4
commit 4f401c6ce9
4 changed files with 410 additions and 5 deletions

View File

@@ -6,6 +6,7 @@
//
import XCTest
import UIKit
class damusUITests: XCTestCase {
var app = XCUIApplication()
@@ -293,6 +294,131 @@ class damusUITests: XCTestCase {
app.buttons[AID.post_composer_cancel_button.rawValue].tap()
}
/// Tests that pasting an npub into the post composer converts it to a mention
/// and resolves to a human-readable profile name via async fetch.
/// This guards against regressions in https://github.com/damus-io/damus/issues/2289
func testPastedNpubResolvesToProfileName() throws {
try self.loginIfNotAlready()
// Set up interruption handler for iOS paste permission alerts
// iOS 16+ may show "Allow Paste" system alerts when pasting from other apps
addUIInterruptionMonitor(withDescription: "Paste Permission Alert") { alert in
// Handle both English and common localizations of the "Allow Paste" button
let allowButtons = ["Allow Paste", "Paste", "Allow", "Erlauben", "Autoriser", "許可"]
for buttonLabel in allowButtons {
let button = alert.buttons[buttonLabel]
if button.exists {
button.tap()
return true
}
}
// Try first button as fallback (typically the "allow" action)
if alert.buttons.count > 0 {
alert.buttons.element(boundBy: 0).tap()
return true
}
return false
}
// Open post composer
guard app.buttons[AID.post_button.rawValue].waitForExistence(timeout: 10) else {
throw DamusUITestError.timeout_waiting_for_element
}
app.buttons[AID.post_button.rawValue].tap()
guard app.textViews[AID.post_composer_text_view.rawValue].waitForExistence(timeout: 5) else {
throw DamusUITestError.timeout_waiting_for_element
}
let textView = app.textViews[AID.post_composer_text_view.rawValue]
textView.tap()
// Use a well-known npub (jack dorsey) that should resolve to a profile name
let testNpub = "npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m"
// Put npub in pasteboard
UIPasteboard.general.string = testNpub
// Long press to bring up paste menu
textView.press(forDuration: 1.0)
// Find paste menu item - handle localized variants
// iOS uses "Paste" in English but varies by locale
let pasteLabels = ["Paste", "Einfügen", "Coller", "Pegar", "Incolla", "ペースト", "貼り付け", "붙여넣기"]
var pasteButton: XCUIElement?
for label in pasteLabels {
let button = app.menuItems[label]
if button.waitForExistence(timeout: 0.5) {
pasteButton = button
break
}
}
guard let pasteButton = pasteButton else {
// Fallback: try first menu item if no known paste label found
let firstMenuItem = app.menuItems.firstMatch
if firstMenuItem.waitForExistence(timeout: 1) {
firstMenuItem.tap()
} else {
app.buttons[AID.post_composer_cancel_button.rawValue].tap()
throw XCTSkip("Paste menu not available in this environment")
}
// Trigger interruption monitors by interacting with app
app.tap()
// Check if paste worked despite not finding the button
let checkText = textView.value as? String ?? ""
if !checkText.contains("@") && !checkText.contains("npub") {
app.buttons[AID.post_composer_cancel_button.rawValue].tap()
throw XCTSkip("Could not trigger paste action")
}
// Paste worked via fallback - clean up and exit
app.buttons[AID.post_composer_cancel_button.rawValue].tap()
return
}
pasteButton.tap()
// Trigger interruption monitors in case paste permission alert appeared
app.tap()
// Wait for initial mention to appear (should contain @ symbol)
let mentionAppearedPredicate = NSPredicate(format: "value CONTAINS[c] '@'")
let mentionAppeared = expectation(for: mentionAppearedPredicate, evaluatedWith: textView)
wait(for: [mentionAppeared], timeout: 5)
// Verify initial paste created a mention (may still show @npub... initially)
let initialText = textView.value as? String ?? ""
XCTAssertTrue(initialText.contains("@"),
"Pasted npub should create a mention but text was '\(initialText)'")
// Wait for async profile fetch to resolve the name (should NOT contain "npub1" after resolution)
// Give it up to 10 seconds for relay fetch
let profileResolvedPredicate = NSPredicate(format: "NOT (value CONTAINS[c] 'npub1')")
let profileResolved = expectation(for: profileResolvedPredicate, evaluatedWith: textView)
let result = XCTWaiter.wait(for: [profileResolved], timeout: 10)
let finalText = textView.value as? String ?? ""
if result == .timedOut {
// Profile didn't resolve - this could happen if offline or relay issues
// Still verify the npub was at least converted to a mention link
XCTAssertTrue(finalText.contains("@"),
"Text should contain a mention but was '\(finalText)'")
print("Note: Profile did not resolve within timeout. Text: '\(finalText)'")
} else {
// Profile resolved - verify it's a human-readable name
XCTAssertTrue(finalText.contains("@"),
"Text should contain a mention but was '\(finalText)'")
XCTAssertFalse(finalText.contains("npub1"),
"Mention should resolve to profile name, not show npub. Text: '\(finalText)'")
}
// Cancel to clean up
app.buttons[AID.post_composer_cancel_button.rawValue].tap()
}
enum DamusUITestError: Error {
case timeout_waiting_for_element
}