Files
iykyk_msn/mobile/lib/features/search/search_page.dart
2026-04-07 16:17:03 +09:00

339 lines
12 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/msn_api.dart';
import '../../core/session_controller.dart';
import '../../models/context_member_model.dart';
import '../../models/room_model.dart';
import '../../theme/toss_theme.dart';
import '../profile/user_profile_sheet.dart';
class SearchPage extends ConsumerStatefulWidget {
const SearchPage({super.key, required this.contextId});
final String contextId;
@override
ConsumerState<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends ConsumerState<SearchPage> {
final _q = TextEditingController();
List<Map<String, dynamic>> _messageResults = [];
List<RoomModel> _rooms = [];
List<ContextMember> _friends = [];
bool _busy = false;
bool _loadedLists = false;
int _segment = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _ensureLists());
}
@override
void dispose() {
_q.dispose();
super.dispose();
}
Future<void> _ensureLists() async {
if (_loadedLists || widget.contextId.isEmpty) return;
setState(() => _busy = true);
try {
final api = ref.read(msnApiProvider);
final rooms = await api.listRooms(widget.contextId);
final members = await api.listContextMembers(widget.contextId);
if (!mounted) return;
setState(() {
_rooms = rooms;
_friends = members;
_loadedLists = true;
});
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _runMessageSearch() async {
if (widget.contextId.isEmpty) return;
setState(() => _busy = true);
try {
final r = await ref.read(msnApiProvider).searchMessages(widget.contextId, _q.text);
if (!mounted) return;
setState(() => _messageResults = r);
} finally {
if (mounted) setState(() => _busy = false);
}
}
String get _needle => _q.text.trim().toLowerCase();
List<RoomModel> get _filteredRooms {
if (_needle.isEmpty) return _rooms;
return _rooms.where((r) {
final name = (r.name ?? (r.isGroup ? '그룹' : '1:1')).toLowerCase();
final last = (r.lastBody ?? '').toLowerCase();
return name.contains(_needle) || last.contains(_needle);
}).toList();
}
List<ContextMember> get _filteredFriends {
if (_needle.isEmpty) return _friends;
return _friends.where((m) {
final name = m.displayName.toLowerCase();
final st = (m.statusMessage ?? '').toLowerCase();
return name.contains(_needle) ||
m.userId.toLowerCase().contains(_needle) ||
st.contains(_needle);
}).toList();
}
@override
Widget build(BuildContext context) {
final tt = Theme.of(context).textTheme;
final myId = ref.watch(sessionProvider).value?.userId;
return Scaffold(
backgroundColor: TossColors.bg,
appBar: AppBar(title: const Text('검색')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: SegmentedButton<int>(
segments: const [
ButtonSegment(value: 0, label: Text('메시지'), icon: Icon(Icons.search, size: 18)),
ButtonSegment(value: 1, label: Text(''), icon: Icon(Icons.chat_bubble_outline, size: 18)),
ButtonSegment(value: 2, label: Text('친구'), icon: Icon(Icons.people_outline, size: 18)),
],
selected: {_segment},
onSelectionChanged: (s) {
if (s.isEmpty) return;
setState(() => _segment = s.first);
},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _q,
decoration: InputDecoration(
hintText: _segment == 0
? '이 맥락에서 메시지 검색'
: _segment == 1
? '방 이름 또는 미리보기로 필터'
: '이름·상태로 친구 필터',
prefixIcon: const Icon(Icons.search_rounded, color: TossColors.textSecondary),
),
onChanged: (_) {
if (_segment != 0) setState(() {});
},
onSubmitted: (_) {
if (_segment == 0) unawaited(_runMessageSearch());
},
),
),
const SizedBox(width: 8),
if (_segment == 0)
Material(
color: TossColors.blue,
borderRadius: BorderRadius.circular(12),
child: IconButton(
onPressed: _busy ? null : () => unawaited(_runMessageSearch()),
icon: _busy
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
)
: const Icon(Icons.arrow_forward_rounded, color: Colors.white),
),
),
],
),
),
Expanded(
child: _busy && !_loadedLists && _segment != 0
? const Center(child: CircularProgressIndicator(color: TossColors.blue))
: _segment == 0
? _buildMessageList(tt)
: _segment == 1
? _buildRoomList(tt)
: _buildFriendList(tt, myId),
),
],
),
);
}
Widget _buildMessageList(TextTheme tt) {
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
itemCount: _messageResults.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, i) {
final r = _messageResults[i];
return Material(
color: TossColors.surface,
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: () {
final roomId = r['roomId'] as String?;
if (roomId == null) return;
context.push('/chat?roomId=$roomId&contextId=${widget.contextId}');
},
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
border: Border.all(color: TossColors.line),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(r['body'] as String? ?? '', style: tt.bodyLarge),
const SizedBox(height: 6),
Text('${r['roomId']}', style: tt.bodySmall),
],
),
),
),
),
);
},
);
}
Widget _buildRoomList(TextTheme tt) {
final list = _filteredRooms;
if (list.isEmpty) {
return Center(child: Text(_emptyHint('')));
}
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
itemCount: list.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, i) {
final r = list[i];
final title = r.name ?? (r.isGroup ? '그룹 채팅' : '1:1 채팅');
final sub = r.lastBody ?? '메시지 없음';
return Material(
color: TossColors.surface,
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: () => context.push('/chat?roomId=${r.id}&contextId=${widget.contextId}'),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
border: Border.all(color: TossColors.line),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: tt.titleMedium),
const SizedBox(height: 4),
Text(sub, style: tt.bodySmall, maxLines: 2, overflow: TextOverflow.ellipsis),
],
),
),
),
),
);
},
);
}
Widget _buildFriendList(TextTheme tt, String? myId) {
final list = _filteredFriends;
if (list.isEmpty) {
return Center(child: Text(_emptyHint('친구')));
}
return ListView.separated(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
itemCount: list.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, i) {
final m = list[i];
final isSelf = myId != null && m.userId == myId;
return Material(
color: TossColors.surface,
borderRadius: BorderRadius.circular(14),
child: InkWell(
borderRadius: BorderRadius.circular(14),
onTap: () async {
if (isSelf) {
await showUserProfileSheet(
context,
ref,
contextId: widget.contextId,
userId: m.userId,
);
} else {
final roomId =
await ref.read(msnApiProvider).openDirectRoom(widget.contextId, m.userId);
if (!context.mounted) return;
await context.push('/chat?roomId=$roomId&contextId=${widget.contextId}');
}
},
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(14),
border: Border.all(color: TossColors.line),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
m.displayName.isNotEmpty ? m.displayName : m.userId,
style: tt.titleMedium,
),
if (m.statusMessage != null && m.statusMessage!.isNotEmpty)
Text(m.statusMessage!, style: tt.bodySmall, maxLines: 1),
],
),
),
IconButton(
icon: const Icon(Icons.person_outline_rounded),
onPressed: () async {
await showUserProfileSheet(
context,
ref,
contextId: widget.contextId,
userId: m.userId,
);
},
),
],
),
),
),
),
);
},
);
}
String _emptyHint(String kind) {
if (_needle.isEmpty) return '목록을 불러오는 중이거나 $kind가 없습니다';
return '검색어와 일치하는 $kind가 없습니다';
}
}