Real previewing chat

This commit is contained in:
2025-08-25 19:36:48 +08:00
parent 6501594100
commit 02057e663b
2 changed files with 294 additions and 65 deletions

View File

@@ -72,6 +72,207 @@ class _AppLifecycleObserver extends WidgetsBindingObserver {
} }
} }
class _PublicRoomPreview extends HookConsumerWidget {
final String id;
final SnChatRoom room;
const _PublicRoomPreview({required this.id, required this.room});
@override
Widget build(BuildContext context, WidgetRef ref) {
final messages = ref.watch(messagesNotifierProvider(id));
final messagesNotifier = ref.read(messagesNotifierProvider(id).notifier);
final scrollController = useScrollController();
final listController = useMemoized(() => ListController(), []);
var isLoading = false;
// Add scroll listener for pagination
useEffect(() {
void onScroll() {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (isLoading) return;
isLoading = true;
messagesNotifier.loadMore().then((_) => isLoading = false);
}
}
scrollController.addListener(onScroll);
return () => scrollController.removeListener(onScroll);
}, [scrollController]);
Widget chatMessageListWidget(List<LocalChatMessage> messageList) =>
SuperListView.builder(
listController: listController,
padding: EdgeInsets.symmetric(vertical: 16),
controller: scrollController,
reverse: true, // Show newest messages at the bottom
itemCount: messageList.length,
findChildIndexCallback: (key) {
final valueKey = key as ValueKey;
final messageId = valueKey.value as String;
return messageList.indexWhere((m) => m.id == messageId);
},
extentEstimation: (_, _) => 40,
itemBuilder: (context, index) {
final message = messageList[index];
final nextMessage =
index < messageList.length - 1 ? messageList[index + 1] : null;
final isLastInGroup =
nextMessage == null ||
nextMessage.senderId != message.senderId ||
nextMessage.createdAt
.difference(message.createdAt)
.inMinutes
.abs() >
3;
return MessageItem(
message: message,
isCurrentUser: false, // User is not a member, so not current user
onAction: null, // No actions allowed in preview mode
onJump: (_) {}, // No jump functionality in preview
progress: null,
showAvatar: isLastInGroup,
);
},
);
final compactHeader = isWideScreen(context);
Widget comfortHeaderWidget() => Column(
spacing: 4,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 26,
width: 26,
child:
(room.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
child: Text(
room.name![0].toUpperCase(),
style: const TextStyle(fontSize: 12),
),
),
),
Text(
(room.type == 1 && room.name == null)
? room.members!.map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(15),
],
);
Widget compactHeaderWidget() => Row(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 26,
width: 26,
child:
(room.type == 1 && room.picture?.id == null)
? SplitAvatarWidget(
filesId:
room.members!
.map((e) => e.account.profile.picture?.id)
.toList(),
)
: room.picture?.id != null
? ProfilePictureWidget(
fileId: room.picture?.id,
fallbackIcon: Symbols.chat,
)
: CircleAvatar(
child: Text(
room.name![0].toUpperCase(),
style: const TextStyle(fontSize: 12),
),
),
),
Text(
(room.type == 1 && room.name == null)
? room.members!.map((e) => e.account.nick).join(', ')
: room.name!,
).fontSize(19),
],
);
return AppScaffold(
appBar: AppBar(
leading: !compactHeader ? const Center(child: PageBackButton()) : null,
automaticallyImplyLeading: false,
toolbarHeight: compactHeader ? null : 64,
title: compactHeader ? compactHeaderWidget() : comfortHeaderWidget(),
actions: [
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
context.pushNamed('chatDetail', pathParameters: {'id': id});
},
),
const Gap(8),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: messages.when(
data:
(messageList) =>
messageList.isEmpty
? Center(child: Text('No messages yet'.tr()))
: chatMessageListWidget(messageList),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => messagesNotifier.loadInitial(),
),
),
),
// Join button at the bottom for public rooms
Container(
padding: const EdgeInsets.all(16),
child: FilledButton.tonalIcon(
onPressed: () async {
try {
showLoadingModal(context);
final apiClient = ref.read(apiClientProvider);
await apiClient.post('/sphere/chat/${room.id}/members/me');
ref.invalidate(chatroomIdentityProvider(id));
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
}
},
label: Text('chatJoin').tr(),
icon: const Icon(Icons.add),
),
),
],
),
);
}
}
@riverpod @riverpod
class MessagesNotifier extends _$MessagesNotifier { class MessagesNotifier extends _$MessagesNotifier {
late final Dio _apiClient; late final Dio _apiClient;
@@ -96,26 +297,34 @@ class MessagesNotifier extends _$MessagesNotifier {
_database = ref.watch(databaseProvider); _database = ref.watch(databaseProvider);
final room = await ref.watch(chatroomProvider(roomId).future); final room = await ref.watch(chatroomProvider(roomId).future);
final identity = await ref.watch(chatroomIdentityProvider(roomId).future); final identity = await ref.watch(chatroomIdentityProvider(roomId).future);
if (room == null || identity == null) {
throw Exception('Room or identity not found'); if (room == null) {
throw Exception('Room not found');
} }
_room = room; _room = room;
_identity = identity;
// Allow building even if identity is null for public rooms
if (identity != null) {
_identity = identity;
}
developer.log( developer.log(
'MessagesNotifier built for room $roomId', 'MessagesNotifier built for room $roomId',
name: 'MessagesNotifier', name: 'MessagesNotifier',
); );
ref.listen(appLifecycleStateProvider, (_, next) { // Only setup sync and lifecycle listeners if user is a member
if (next.hasValue && next.value == AppLifecycleState.resumed) { if (identity != null) {
developer.log( ref.listen(appLifecycleStateProvider, (_, next) {
'App resumed, syncing messages', if (next.hasValue && next.value == AppLifecycleState.resumed) {
name: 'MessagesNotifier', developer.log(
); 'App resumed, syncing messages',
syncMessages(); name: 'MessagesNotifier',
} );
}); syncMessages();
}
});
}
return await loadInitial(); return await loadInitial();
} }
@@ -737,57 +946,77 @@ class ChatRoomScreen extends HookConsumerWidget {
); );
} else if (chatIdentity.value == null) { } else if (chatIdentity.value == null) {
// Identity was not found, user was not joined // Identity was not found, user was not joined
return AppScaffold( return chatRoom.when(
appBar: AppBar(leading: const PageBackButton()), data: (room) {
body: Center( if (room!.isPublic) {
child: // Show public room preview with messages but no input
ConstrainedBox( return _PublicRoomPreview(id: id, room: room);
constraints: const BoxConstraints(maxWidth: 280), } else {
child: Column( // Show regular "not joined" screen for private rooms
crossAxisAlignment: CrossAxisAlignment.center, return AppScaffold(
mainAxisAlignment: MainAxisAlignment.center, appBar: AppBar(leading: const PageBackButton()),
children: [ body: Center(
Icon( child:
chatRoom.value?.isCommunity == true ConstrainedBox(
? Symbols.person_add constraints: const BoxConstraints(maxWidth: 280),
: Symbols.person_remove, child: Column(
size: 36, crossAxisAlignment: CrossAxisAlignment.center,
fill: 1, mainAxisAlignment: MainAxisAlignment.center,
).padding(bottom: 4), children: [
Text('chatNotJoined').tr(), Icon(
if (chatRoom.value?.isCommunity != true) room.isCommunity == true
Text( ? Symbols.person_add
'chatUnableJoin', : Symbols.person_remove,
textAlign: TextAlign.center, size: 36,
).tr().bold() fill: 1,
else ).padding(bottom: 4),
FilledButton.tonalIcon( Text('chatNotJoined').tr(),
onPressed: () async { if (room.isCommunity != true)
try { Text(
showLoadingModal(context); 'chatUnableJoin',
final apiClient = ref.read(apiClientProvider); textAlign: TextAlign.center,
if (chatRoom.value == null) { ).tr().bold()
hideLoadingModal(context); else
return; FilledButton.tonalIcon(
} onPressed: () async {
try {
await apiClient.post( showLoadingModal(context);
'/sphere/chat/${chatRoom.value!.id}/members/me', final apiClient = ref.read(apiClientProvider);
); await apiClient.post(
ref.invalidate(chatroomIdentityProvider(id)); '/sphere/chat/${room.id}/members/me',
} catch (err) { );
showErrorAlert(err); ref.invalidate(chatroomIdentityProvider(id));
} finally { } catch (err) {
if (context.mounted) hideLoadingModal(context); showErrorAlert(err);
} } finally {
}, if (context.mounted) {
label: Text('chatJoin').tr(), hideLoadingModal(context);
icon: const Icon(Icons.add), }
).padding(top: 8), }
], },
), label: Text('chatJoin').tr(),
).center(), icon: const Icon(Icons.add),
), ).padding(top: 8),
],
),
).center(),
),
);
}
},
loading:
() => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: CircularProgressIndicator().center(),
),
error:
(error, _) => AppScaffold(
appBar: AppBar(leading: const PageBackButton()),
body: ResponseErrorWidget(
error: error,
onRetry: () => ref.refresh(chatroomProvider(id)),
),
),
); );
} }

View File

@@ -95,9 +95,9 @@ class EventCalendarWidget extends HookConsumerWidget {
final textColor = final textColor =
isSameDay(selectedDay.value, day) isSameDay(selectedDay.value, day)
? Theme.of(context).colorScheme.onPrimary ? Colors.white
: isSameDay(DateTime.now(), day) : isSameDay(DateTime.now(), day)
? Theme.of(context).colorScheme.onPrimary ? Colors.white
: Theme.of(context).colorScheme.onSurface; : Theme.of(context).colorScheme.onSurface;
final shadow = final shadow =