mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 05:45:18 +00:00
* docs(plans): add refactor plan; chore(config): introduce TransportConfig and use in BLEService/ChatViewModel/PrivateChatManager; feat: add PeerDisplayNameResolver and apply in BLEService; chore: make TransportPeerSnapshot Equatable/Hashable; perf: simplify read-receipt persistence (no synchronize) * project: add TransportConfig.swift and PeerDisplayNameResolver.swift to iOS/macOS targets (no xcodegen) * chore: remove unnecessary UserDefaults.synchronize() calls (extension/app/VM) * project: fix TransportConfig reference path; remove recovered reference; hook correct fileRef in iOS/macOS sources * chore(BLE): move connect/duty/announce constants to TransportConfig and reference them * chore(NostrTransport): factor recipient npub resolution into helper; reduce duplication * chore(BLE): trim raw hex dump on central decode failure to length+prefix * project: remove duplicate TransportConfig.swift entries from Sources build phases * chore: centralize more constants in TransportConfig (BLE thresholds, Nostr read-ack, UI caps) and adopt in BLEService/ChatViewModel/NostrTransport * chore: centralize location + geohash constants (filters, lookback, relay count) and adopt in LocationChannelManager/ChatViewModel * chore: centralize compression, dedup, verification QR, relay backoff, georelay fetch constants; adopt across modules * chore: centralize more BLE/Nostr delays; tighten NostrRelayManager logs to concise summaries; adopt config for location/geohash/relays * refactor(config): centralize remaining magic numbers and finalize log hygiene Add comprehensive TransportConfig constants for UI, Nostr, and BLE; adopt across ChatViewModel, ContentView, BLEService, ShareViewController, and BitchatApp to remove scattered literals. Standardize Nostr lookbacks/limits, UI delays/animations, and BLE announce/duty-cycle/candidate caps. Preserve behavior while making tuning explicit and safe. Highlights:\n- UI: animations, scroll throttle, long-message thresholds, batch stagger, color hue tuning, rate-limit buckets, read-receipt debounce, startup delays, share accept/dismiss windows, migration cutoff.\n- Nostr: short display length (8), conv-key prefix length (16), DM lookback (24h), geohash sample lookback/limits; consistent use throughout ChatViewModel.\n- BLE: dynamic RSSI defaults, announce intervals/base+jitter, duty cycles (dense/sparse), fragment/ingress lifetimes, expected write timings/spacing, recent packet window (30s/100), peer inactivity timeout; unified candidate caps (100).\n- Share: use constant dismiss delay; App: use constant share accept window.\n\nRisk/impact: behavior-equivalent with centralized knobs; easier to tune without code edits. * project: add TransportConfig.swift to Share Extension target to fix build --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
167 lines
6.6 KiB
Swift
167 lines
6.6 KiB
Swift
//
|
|
// ShareViewController.swift
|
|
// bitchatShareExtension
|
|
//
|
|
// This is free and unencumbered software released into the public domain.
|
|
// For more information, see <https://unlicense.org>
|
|
//
|
|
|
|
import UIKit
|
|
import UniformTypeIdentifiers
|
|
|
|
/// Modern share extension using UIKit + UTTypes.
|
|
/// Avoids deprecated Social framework and SLComposeServiceViewController.
|
|
final class ShareViewController: UIViewController {
|
|
private let statusLabel: UILabel = {
|
|
let l = UILabel()
|
|
l.translatesAutoresizingMaskIntoConstraints = false
|
|
l.font = .systemFont(ofSize: 15, weight: .semibold)
|
|
l.textAlignment = .center
|
|
l.numberOfLines = 0
|
|
l.textColor = .label
|
|
return l
|
|
}()
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
view.backgroundColor = .systemBackground
|
|
view.addSubview(statusLabel)
|
|
NSLayoutConstraint.activate([
|
|
statusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
statusLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
|
statusLabel.leadingAnchor.constraint(greaterThanOrEqualTo: view.layoutMarginsGuide.leadingAnchor),
|
|
statusLabel.trailingAnchor.constraint(lessThanOrEqualTo: view.layoutMarginsGuide.trailingAnchor)
|
|
])
|
|
|
|
processShare()
|
|
}
|
|
|
|
// MARK: - Processing
|
|
private func processShare() {
|
|
guard let ctx = self.extensionContext,
|
|
let item = ctx.inputItems.first as? NSExtensionItem else {
|
|
finishWithMessage("Nothing to share")
|
|
return
|
|
}
|
|
|
|
// Try content from attributed text first (Safari often passes URL here)
|
|
if let url = detectURL(in: item.attributedContentText?.string ?? "") {
|
|
saveAndFinish(url: url, title: item.attributedTitle?.string)
|
|
return
|
|
}
|
|
|
|
// Scan attachments for URL/text
|
|
let providers = item.attachments ?? []
|
|
if providers.isEmpty {
|
|
// Fallback: use attributed title as plain text
|
|
if let title = item.attributedTitle?.string, !title.isEmpty {
|
|
saveAndFinish(text: title)
|
|
} else {
|
|
finishWithMessage("No shareable content")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Load URL or text asynchronously
|
|
loadFirstURL(from: providers) { [weak self] url in
|
|
guard let self = self else { return }
|
|
if let url = url {
|
|
self.saveAndFinish(url: url, title: item.attributedTitle?.string)
|
|
} else {
|
|
self.loadFirstPlainText(from: providers) { text in
|
|
if let t = text, !t.isEmpty {
|
|
// Treat as URL if parseable http(s), else plain text
|
|
if let u = URL(string: t), ["http","https"].contains(u.scheme?.lowercased() ?? "") {
|
|
self.saveAndFinish(url: u, title: item.attributedTitle?.string)
|
|
} else {
|
|
self.saveAndFinish(text: t)
|
|
}
|
|
} else {
|
|
self.finishWithMessage("No shareable content")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func detectURL(in text: String) -> URL? {
|
|
guard !text.isEmpty else { return nil }
|
|
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
|
let range = NSRange(location: 0, length: (text as NSString).length)
|
|
let match = detector?.matches(in: text, options: [], range: range).first
|
|
return match?.url
|
|
}
|
|
|
|
private func loadFirstURL(from providers: [NSItemProvider], completion: @escaping (URL?) -> Void) {
|
|
let identifiers = [UTType.url.identifier, "public.url", "public.file-url"]
|
|
let grp = DispatchGroup()
|
|
var found: URL?
|
|
|
|
for p in providers where found == nil {
|
|
for id in identifiers where p.hasItemConformingToTypeIdentifier(id) {
|
|
grp.enter()
|
|
p.loadItem(forTypeIdentifier: id, options: nil) { item, _ in
|
|
defer { grp.leave() }
|
|
if let u = item as? URL { found = u; return }
|
|
if let s = item as? String, let u = URL(string: s) { found = u; return }
|
|
if let d = item as? Data, let s = String(data: d, encoding: .utf8), let u = URL(string: s) { found = u; return }
|
|
}
|
|
break
|
|
}
|
|
}
|
|
grp.notify(queue: .main) { completion(found) }
|
|
}
|
|
|
|
private func loadFirstPlainText(from providers: [NSItemProvider], completion: @escaping (String?) -> Void) {
|
|
let id = UTType.plainText.identifier
|
|
let grp = DispatchGroup()
|
|
var text: String?
|
|
for p in providers where p.hasItemConformingToTypeIdentifier(id) {
|
|
grp.enter()
|
|
p.loadItem(forTypeIdentifier: id, options: nil) { item, _ in
|
|
defer { grp.leave() }
|
|
if let s = item as? String { text = s }
|
|
else if let d = item as? Data, let s = String(data: d, encoding: .utf8) { text = s }
|
|
}
|
|
break
|
|
}
|
|
grp.notify(queue: .main) { completion(text) }
|
|
}
|
|
|
|
// MARK: - Save + Finish
|
|
private func saveAndFinish(url: URL, title: String?) {
|
|
let payload: [String: String] = [
|
|
"url": url.absoluteString,
|
|
"title": title ?? url.host ?? "Shared Link"
|
|
]
|
|
if let json = try? JSONSerialization.data(withJSONObject: payload),
|
|
let s = String(data: json, encoding: .utf8) {
|
|
saveToSharedDefaults(content: s, type: "url")
|
|
finishWithMessage("✓ Shared link to bitchat")
|
|
} else {
|
|
finishWithMessage("Failed to encode link")
|
|
}
|
|
}
|
|
|
|
private func saveAndFinish(text: String) {
|
|
saveToSharedDefaults(content: text, type: "text")
|
|
finishWithMessage("✓ Shared text to bitchat")
|
|
}
|
|
|
|
private func saveToSharedDefaults(content: String, type: String) {
|
|
guard let userDefaults = UserDefaults(suiteName: "group.chat.bitchat") else { return }
|
|
userDefaults.set(content, forKey: "sharedContent")
|
|
userDefaults.set(type, forKey: "sharedContentType")
|
|
userDefaults.set(Date(), forKey: "sharedContentDate")
|
|
// No need to force synchronize; the system persists changes
|
|
}
|
|
|
|
private func finishWithMessage(_ msg: String) {
|
|
statusLabel.text = msg
|
|
// Complete shortly after showing status
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + TransportConfig.uiShareExtensionDismissDelaySeconds) {
|
|
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
|
}
|
|
}
|
|
}
|