added a swipeable media gallery for chat and folder images and videos with server previews, on-demand originals and zoom, and marked document previews in chat so they no longer pass for photos

This commit is contained in:
MineTec committed 2026-10-28 22:15:57 +01:00
1 parent 952f916231
commit aa176c90e3
22 files changed
+1658 -103

No files matched your search

+2 -33
View File
@@ -1,12 +1,10 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart';
import '../../../../model/endpoint_data.dart';
import '../../../../session/session_manager.dart';
import '../../../../utils/emoji_detection.dart';
import '../../../../utils/url_opener.dart';
import '../widgets/chat_file_attachment.dart';
import '../widgets/highlighted_linkify.dart';
import '../widgets/message_markdown.dart';
@@ -78,36 +76,7 @@ class ChatMessage {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CachedNetworkImage(
errorWidget: (context, url, error) => Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.file_open_outlined, size: 35),
const SizedBox(width: 10),
Flexible(
child: Text(
file.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 10),
],
),
alignment: Alignment.center,
placeholder: (context, url) => const Padding(
padding: EdgeInsets.all(15),
child: SizedBox(width: 50, child: LinearProgressIndicator()),
),
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
errorListener: (value) {},
httpHeaders: SessionManager().requireNextcloud().authHeaders,
imageUrl:
'${EndpointData().nextcloud().origin()}/index.php/core/preview?fileId=${file.id}&x=130&y=-1&a=1',
),
ChatFileAttachment(file: file),
if (originalMessage != '{file}') ...[
SizedBox(height: 5),
contentWidget,
@@ -0,0 +1,128 @@
import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
import '../../../../api/marianumcloud/talk/get_shared_items/get_shared_items.dart';
import '../../../../extensions/date_time.dart';
import '../../../../session/session_manager.dart';
import '../../../../share_intent/remote_file_ref.dart';
import '../../../../widget/media_gallery/gallery_item.dart';
import '../../../../widget/media_gallery/gallery_source.dart';
import '../details/shared_items_view.dart';
GalleryItem? galleryItemFromTalk(GetChatResponseObject message) {
final file = message.messageParameters?['file'];
if (file == null || file.path == null) return null;
final kind = galleryMediaKindOf(file.name, mimeType: file.mimetype);
if (kind == null) return null;
return GalleryItem(
file: RemoteFileRef.fromTalk(file),
kind: kind,
messageId: message.id,
subtitle:
'${message.actorDisplayName} · ${message.sentAt.formatDateTime()}',
);
}
/// All images and videos of a chat, via Talk's shared-items `media` stream.
/// Opens on the tapped message and fills in around it in the background.
class TalkGallerySource extends GallerySource {
/// The share endpoint only pages backwards from the newest item, so reaching
/// an old tapped image means walking down to it; give up beyond this.
static const _maxCatchUpPages = 25;
final String token;
final GalleryItem initial;
final int _epoch = SessionManager().sessionEpoch;
List<GalleryItem> _items;
int? _cursor;
bool _hasOlder = false;
bool _loading = false;
bool _disposed = false;
TalkGallerySource({required this.token, required this.initial})
: _items = [initial] {
_catchUp();
}
@override
List<GalleryItem> get items => _items;
@override
bool get hasOlder => _hasOlder && !_loading;
bool get _stale => _disposed || !SessionManager().isCurrentSession(_epoch);
Future<SharedItemsPage> _fetch(int? cursor) async => buildSharedItemsPage(
await GetSharedItems(
token,
objectType: 'media',
limit: sharedItemsPageSize,
lastKnownMessageId: cursor,
).run(),
cursor,
);
Iterable<GalleryItem> _toItems(SharedItemsPage page) =>
page.items.map(galleryItemFromTalk).nonNulls;
Future<void> _catchUp() async {
final initialId = initial.messageId!;
_loading = true;
try {
final collected = <GalleryItem>[];
int? cursor;
var hasMore = true;
var reached = false;
for (var pages = 0; hasMore && pages < _maxCatchUpPages; pages++) {
final page = await _fetch(cursor);
if (_stale) return;
collected.addAll(_toItems(page));
cursor = page.lastKnownMessageId;
hasMore = page.hasMore;
if (page.items.any((item) => item.id <= initialId)) {
reached = true;
break;
}
}
if (reached || !hasMore) {
_items = mergeGalleryItems(_items, collected);
_cursor = cursor;
_hasOlder = hasMore;
} else {
// Too far down: skip the newer media and only page older ones.
_cursor = initialId;
_hasOlder = true;
}
} on Object {
if (_stale) return;
_cursor = initialId;
_hasOlder = true;
} finally {
_loading = false;
}
notifyListeners();
}
@override
Future<void> loadOlder() async {
if (!hasOlder) return;
_loading = true;
try {
final page = await _fetch(_cursor);
if (_stale) return;
_items = mergeGalleryItems(_items, _toItems(page));
_cursor = page.lastKnownMessageId;
_hasOlder = page.hasMore;
} on Object {
// Retried on the next page change.
} finally {
_loading = false;
}
if (!_stale) notifyListeners();
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
}