* 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
48 lines
1.3 KiB
Dart
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;
|