implemented global user search integration for Talk chats with role-coded badges and direct chat creation logic
This commit is contained in:
@@ -1,49 +1,249 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../api/errors/error_mapper.dart';
|
||||
import '../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../api/marianumconnect/queries/user_search/user_search.dart';
|
||||
import '../../../api/marianumconnect/queries/user_search/user_search_response.dart';
|
||||
import '../../../widget/app_progress_indicator.dart';
|
||||
import 'widgets/chat_tile.dart';
|
||||
import 'widgets/user_search_tile.dart';
|
||||
|
||||
class SearchChat extends SearchDelegate<GetRoomResponseObject?> {
|
||||
List<GetRoomResponseObject> chats;
|
||||
final void Function(GetRoomResponseObject room)? onTapOverride;
|
||||
|
||||
SearchChat(this.chats, {this.onTapOverride});
|
||||
/// When set, results are extended with a clearly separated "start a new chat"
|
||||
/// section fed by the MarianumConnect user search. Tapping a person invokes
|
||||
/// this callback with their username and display name. Left null (e.g. the
|
||||
/// share picker) the search stays limited to existing chats.
|
||||
final void Function(String username, String displayName)? onStartChat;
|
||||
|
||||
SearchChat(this.chats, {this.onTapOverride, this.onStartChat});
|
||||
|
||||
@override
|
||||
List<Widget>? buildActions(BuildContext context) => [
|
||||
if (query.isNotEmpty)
|
||||
IconButton(onPressed: () => query = '', icon: const Icon(Icons.delete)),
|
||||
IconButton(onPressed: () => query = '', icon: const Icon(Icons.clear)),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget? buildLeading(BuildContext context) => null;
|
||||
|
||||
@override
|
||||
Widget buildResults(BuildContext context) {
|
||||
var items =
|
||||
chats
|
||||
.where(
|
||||
(e) =>
|
||||
e.displayName.toString().toLowerCase().contains(
|
||||
query.toLowerCase(),
|
||||
) ||
|
||||
e.name.toString().toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => b.lastActivity.compareTo(a.lastActivity));
|
||||
return ListView.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
var item = items.elementAt(index);
|
||||
return ChatTile(
|
||||
data: item,
|
||||
disableContextActions: true,
|
||||
onTapOverride: onTapOverride,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
Widget buildResults(BuildContext context) => _SearchChatResults(
|
||||
chats: chats,
|
||||
query: query,
|
||||
onTapOverride: onTapOverride,
|
||||
onStartChat: onStartChat == null
|
||||
? null
|
||||
: (username, displayName) {
|
||||
close(context, null);
|
||||
onStartChat!(username, displayName);
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
Widget buildSuggestions(BuildContext context) => buildResults(context);
|
||||
}
|
||||
|
||||
class _SearchChatResults extends StatefulWidget {
|
||||
final List<GetRoomResponseObject> chats;
|
||||
final String query;
|
||||
final void Function(GetRoomResponseObject room)? onTapOverride;
|
||||
final void Function(String username, String displayName)? onStartChat;
|
||||
|
||||
const _SearchChatResults({
|
||||
required this.chats,
|
||||
required this.query,
|
||||
required this.onTapOverride,
|
||||
required this.onStartChat,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SearchChatResults> createState() => _SearchChatResultsState();
|
||||
}
|
||||
|
||||
class _SearchChatResultsState extends State<_SearchChatResults> {
|
||||
static const _debounceDelay = Duration(milliseconds: 350);
|
||||
static const _minQueryLength = 2;
|
||||
static const _loadingIndicator = Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(child: AppProgressIndicator.medium()),
|
||||
);
|
||||
|
||||
Timer? _debounce;
|
||||
|
||||
// Monotonic id so a slow response for an earlier query can never overwrite
|
||||
// the results of a newer one.
|
||||
int _requestId = 0;
|
||||
|
||||
// null until the first response for the current query settles; previous
|
||||
// results stay put while the next query is loading, so the list does not
|
||||
// flash to a spinner on every keystroke.
|
||||
List<McUserSearchResult>? _users;
|
||||
Object? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scheduleUserSearch(widget.query);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_SearchChatResults oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.query != widget.query) _scheduleUserSearch(widget.query);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounce?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _scheduleUserSearch(String query) {
|
||||
if (widget.onStartChat == null) return;
|
||||
_debounce?.cancel();
|
||||
final trimmed = query.trim();
|
||||
if (trimmed.length < _minQueryLength) {
|
||||
// Invalidate any in-flight response and drop the section content.
|
||||
_requestId++;
|
||||
setState(() {
|
||||
_users = null;
|
||||
_error = null;
|
||||
});
|
||||
return;
|
||||
}
|
||||
_debounce = Timer(_debounceDelay, () => _runSearch(trimmed));
|
||||
}
|
||||
|
||||
Future<void> _runSearch(String query) async {
|
||||
final id = ++_requestId;
|
||||
try {
|
||||
final response = await UserSearch().run(query);
|
||||
if (!mounted || id != _requestId) return;
|
||||
setState(() {
|
||||
_users = response.result;
|
||||
_error = null;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted || id != _requestId) return;
|
||||
setState(() => _error = e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final query = widget.query.toLowerCase();
|
||||
final chatMatches =
|
||||
widget.chats
|
||||
.where(
|
||||
(e) =>
|
||||
e.displayName.toString().toLowerCase().contains(query) ||
|
||||
e.name.toString().toLowerCase().contains(query),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => b.lastActivity.compareTo(a.lastActivity));
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
...chatMatches.map(
|
||||
(item) => ChatTile(
|
||||
data: item,
|
||||
disableContextActions: true,
|
||||
onTapOverride: widget.onTapOverride,
|
||||
),
|
||||
),
|
||||
if (widget.onStartChat != null) _buildUserSection(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserSection(BuildContext context) {
|
||||
if (widget.query.trim().length < _minQueryLength) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
// Usernames that already have a direct chat — they are listed above, so
|
||||
// the "start new chat" section skips them.
|
||||
final existing = <String>{
|
||||
for (final c in widget.chats)
|
||||
if (c.type == GetRoomResponseObjectConversationType.oneToOne) c.name,
|
||||
};
|
||||
final theme = Theme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Divider(height: 32),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_add_alt_1,
|
||||
size: 20,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Neuen Chat starten',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Personen aus der Suche',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.hintColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_buildUserResults(context, existing),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserResults(BuildContext context, Set<String> existing) {
|
||||
if (_error != null) return _hint(context, errorToUserMessage(_error));
|
||||
final all = _users;
|
||||
if (all == null) return _loadingIndicator;
|
||||
final users = all.where((u) => !existing.contains(u.username)).toList();
|
||||
if (users.isEmpty) {
|
||||
return _hint(context, 'Keine weiteren Personen gefunden');
|
||||
}
|
||||
return Column(
|
||||
children: users
|
||||
.map(
|
||||
(user) => UserSearchTile(
|
||||
user: user,
|
||||
onTap: () => widget.onStartChat!(
|
||||
user.username,
|
||||
'${user.firstName} ${user.lastName}',
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _hint(BuildContext context, String text) {
|
||||
final theme = Theme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
|
||||
child: Text(
|
||||
text,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user