오대리ㅣㅣㅣㅣ
This commit is contained in:
178
mobile/lib/features/profile/user_profile_sheet.dart
Normal file
178
mobile/lib/features/profile/user_profile_sheet.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
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/profile_model.dart';
|
||||
import '../../theme/toss_theme.dart';
|
||||
|
||||
/// Kakao-style profile sheet: large avatar, name, status, optional 1:1 chat.
|
||||
Future<void> showUserProfileSheet(
|
||||
BuildContext parentContext,
|
||||
WidgetRef ref, {
|
||||
required String contextId,
|
||||
required String userId,
|
||||
}) async {
|
||||
await showModalBottomSheet<void>(
|
||||
context: parentContext,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
backgroundColor: TossColors.surface,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (sheetContext) => _UserProfileSheetBody(
|
||||
parentContext: parentContext,
|
||||
contextId: contextId,
|
||||
userId: userId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _UserProfileSheetBody extends ConsumerStatefulWidget {
|
||||
const _UserProfileSheetBody({
|
||||
required this.parentContext,
|
||||
required this.contextId,
|
||||
required this.userId,
|
||||
});
|
||||
|
||||
final BuildContext parentContext;
|
||||
final String contextId;
|
||||
final String userId;
|
||||
|
||||
@override
|
||||
ConsumerState<_UserProfileSheetBody> createState() => _UserProfileSheetBodyState();
|
||||
}
|
||||
|
||||
class _UserProfileSheetBodyState extends ConsumerState<_UserProfileSheetBody> {
|
||||
late Future<ProfileModel> _future;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final myId = ref.read(sessionProvider).value?.userId;
|
||||
final isSelf = myId != null && widget.userId == myId;
|
||||
_future = isSelf
|
||||
? ref.read(msnApiProvider).getMyProfile(widget.contextId)
|
||||
: ref.read(msnApiProvider).getUserProfile(widget.contextId, widget.userId);
|
||||
}
|
||||
|
||||
Future<void> _openDirect() async {
|
||||
final roomId =
|
||||
await ref.read(msnApiProvider).openDirectRoom(widget.contextId, widget.userId);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
if (widget.parentContext.mounted) {
|
||||
await widget.parentContext.push('/chat?roomId=$roomId&contextId=${widget.contextId}');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final myId = ref.watch(sessionProvider).value?.userId;
|
||||
final isSelf = myId != null && widget.userId == myId;
|
||||
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 24,
|
||||
right: 24,
|
||||
top: 8,
|
||||
bottom: 24 + MediaQuery.paddingOf(context).bottom,
|
||||
),
|
||||
child: FutureBuilder<ProfileModel>(
|
||||
future: _future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SizedBox(
|
||||
height: 220,
|
||||
child: Center(child: CircularProgressIndicator(color: TossColors.blue)),
|
||||
);
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return SizedBox(
|
||||
height: 120,
|
||||
child: Center(child: Text('${snapshot.error}')),
|
||||
);
|
||||
}
|
||||
final p = snapshot.data!;
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: _LargeAvatar(
|
||||
displayName: p.displayName,
|
||||
avatarUrl: p.avatarUrl,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
p.displayName,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (p.statusMessage != null && p.statusMessage!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
p.statusMessage!,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'이 맥락에서만 보이는 프로필이에요.',
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (!isSelf) ...[
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.icon(
|
||||
onPressed: _openDirect,
|
||||
icon: const Icon(Icons.chat_bubble_outline),
|
||||
label: const Text('1:1 채팅'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LargeAvatar extends StatelessWidget {
|
||||
const _LargeAvatar({
|
||||
required this.displayName,
|
||||
this.avatarUrl,
|
||||
});
|
||||
|
||||
final String displayName;
|
||||
final String? avatarUrl;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasUrl = avatarUrl != null && avatarUrl!.isNotEmpty;
|
||||
return CircleAvatar(
|
||||
radius: 56,
|
||||
backgroundColor: TossColors.blue.withValues(alpha: 0.12),
|
||||
backgroundImage: hasUrl ? NetworkImage(avatarUrl!) : null,
|
||||
child: hasUrl
|
||||
? null
|
||||
: Text(
|
||||
displayName.isNotEmpty ? displayName[0].toUpperCase() : '?',
|
||||
style: const TextStyle(
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: TossColors.blue,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user