Files
JeffGandGitHub bb33016aff feat(user-search): search by name with improved UX and metadata handling (#234)
* feat(user-search): search by name with improved UX and metadata handling

- Redesign user list tile to match profile switcher pattern (medium avatar,
  middle-ellipsis npub, consistent padding)
- Sort follows: users with metadata alphabetically first, then without
- Periodically refresh follows to pick up background metadata updates
- Batch name search stream updates (300ms) to reduce list jerkiness
- Add loading spinner in search field during active name search
- Sanitize malformed UTF-16 in metadata (surrogate pairs + UTF-8 round-trip)
- Pass metadata to start chat screen for instant display
- Truncate long about text to 10 lines in user profile card

* docs: update changelog with PR #234

* fix: screenutil height unit, improve test specificity and coverage

- Fix SizedBox height in search loading indicator to use .h instead of .w
- Target specific about Text widget in truncation test via content match
- Add tests for stream onDone, onError, and whitespace query edge cases
- Coverage improved from 99.31% to 99.41%

* refactor: remove unreachable else branch in useUserSearch

The isNameQuery check always evaluates true at that point since
_isNameSearch returns true for any non-empty query that isn't a hex
pubkey or partial npub — exactly the cases remaining after the
preceding branches.

* fix: log errors in useUserSearch onError instead of swallowing them
2026-02-12 15:51:51 +01:00

48 lines
1.3 KiB
Dart

import 'dart:convert';
import 'package:whitenoise/src/rust/api/metadata.dart';
String? presentName(FlutterMetadata? metadata) {
if (metadata == null) return null;
if (metadata.displayName?.isNotEmpty == true) {
return sanitizeForDisplay(metadata.displayName!);
}
if (metadata.name?.isNotEmpty == true) {
return sanitizeForDisplay(metadata.name!);
}
return null;
}
String sanitizeForDisplay(String value) {
final buffer = StringBuffer();
for (var i = 0; i < value.length; i++) {
final code = value.codeUnitAt(i);
if (_isHighSurrogate(code)) {
if (i + 1 < value.length && _isLowSurrogate(value.codeUnitAt(i + 1))) {
buffer.writeCharCode(code);
buffer.writeCharCode(value.codeUnitAt(i + 1));
i++;
} else {
buffer.write('\u{FFFD}');
}
} else if (_isLowSurrogate(code)) {
buffer.write('\u{FFFD}');
} else {
buffer.writeCharCode(code);
}
}
final sanitized = buffer.toString();
return _roundTripUtf8(sanitized);
}
String _roundTripUtf8(String value) {
try {
return utf8.decode(utf8.encode(value));
} catch (_) {
return value.replaceAll(RegExp(r'[^\x20-\x7E]'), '\u{FFFD}');
}
}
bool _isHighSurrogate(int code) => code >= 0xD800 && code <= 0xDBFF;
bool _isLowSurrogate(int code) => code >= 0xDC00 && code <= 0xDFFF;