feat: image fullscreen viewer, public room browser

- Chat: tap any image to open fullscreen InteractiveViewer (pinch/zoom)
- Rooms: public room browser (Find rooms FAB + empty state button)
  searches Matrix room directory via queryPublicRooms, join from results
- Rooms: empty state now shows 'Find rooms' button to onboard new users

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-27 06:23:27 +10:00
parent c1b5b31d48
commit 542a5e5221
4 changed files with 306 additions and 10 deletions

View File

@@ -1,4 +1,4 @@
// Version: 1.2.0 | Created: 2026-04-01 | Updated: 2026-04-10
// Version: 1.3.0 | Created: 2026-04-01 | Updated: 2026-04-27
// Message bubble widget. Handles text, images, files, redacted, replies.
import 'package:cached_network_image/cached_network_image.dart';
@@ -172,11 +172,15 @@ class _ImageContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (message.mediaUrl != null) {
return ClipRRect(
final url = message.mediaUrl;
if (url == null) return const Icon(Icons.image_not_supported);
return GestureDetector(
onTap: () => _openFullscreen(context, url, message.body),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: message.mediaUrl!,
imageUrl: url,
width: 240,
fit: BoxFit.cover,
placeholder: (_, __) => const SizedBox(
@@ -188,9 +192,60 @@ class _ImageContent extends StatelessWidget {
child: Center(child: Icon(Icons.broken_image)),
),
),
);
}
return const Icon(Icons.image_not_supported);
),
);
}
void _openFullscreen(BuildContext context, String url, String? caption) {
Navigator.of(context).push(
MaterialPageRoute<void>(
fullscreenDialog: true,
builder: (_) => _ImageViewer(url: url, caption: caption),
),
);
}
}
class _ImageViewer extends StatelessWidget {
const _ImageViewer({required this.url, this.caption});
final String url;
final String? caption;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
title: caption != null
? Text(
caption!,
style: const TextStyle(fontSize: 14),
overflow: TextOverflow.ellipsis,
)
: null,
),
body: Center(
child: InteractiveViewer(
minScale: 0.5,
maxScale: 4,
child: CachedNetworkImage(
imageUrl: url,
fit: BoxFit.contain,
placeholder: (_, __) => const CircularProgressIndicator(
color: Colors.white,
),
errorWidget: (_, __, ___) => const Icon(
Icons.broken_image,
color: Colors.white,
size: 64,
),
),
),
),
);
}
}