Merge 228a855409 into c59031c44c
This commit is contained in:
commit
306ea2d950
5 changed files with 627 additions and 27 deletions
|
|
@ -3009,6 +3009,8 @@
|
|||
"@incomingMessages": {},
|
||||
"stickers": "Stickers",
|
||||
"@stickers": {},
|
||||
"gifs": "GIFs",
|
||||
"@gifs": {},
|
||||
"discover": "Discover",
|
||||
"@discover": {},
|
||||
"commandHint_ignore": "Ignore the given matrix ID",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:matrix/matrix.dart';
|
|||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/pages/chat/gif_picker_dialog.dart';
|
||||
import 'package:fluffychat/pages/chat/sticker_picker_dialog.dart';
|
||||
import 'chat.dart';
|
||||
|
||||
|
|
@ -25,13 +26,14 @@ class ChatEmojiPicker extends StatelessWidget {
|
|||
: 0,
|
||||
child: controller.showEmojiPicker
|
||||
? DefaultTabController(
|
||||
length: 2,
|
||||
length: 3,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: [
|
||||
Tab(text: L10n.of(context).emojis),
|
||||
Tab(text: L10n.of(context).stickers),
|
||||
Tab(text: L10n.of(context).gifs),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
|
|
@ -85,6 +87,25 @@ class ChatEmojiPicker extends StatelessWidget {
|
|||
controller.hideEmojiPicker();
|
||||
},
|
||||
),
|
||||
GifPickerDialog(
|
||||
onSelected: (gif) {
|
||||
controller.room.sendEvent(
|
||||
{
|
||||
'body':
|
||||
gif.title.isNotEmpty ? gif.title : 'GIF',
|
||||
'info': {
|
||||
'mimetype': 'image/gif',
|
||||
'w': gif.width,
|
||||
'h': gif.height,
|
||||
'size': null,
|
||||
},
|
||||
'url': gif.url,
|
||||
},
|
||||
type: EventTypes.Sticker,
|
||||
);
|
||||
controller.hideEmojiPicker();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
388
lib/pages/chat/gif_picker_dialog.dart
Normal file
388
lib/pages/chat/gif_picker_dialog.dart
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:fluffychat/l10n/l10n.dart';
|
||||
import 'package:fluffychat/utils/tenor_api.dart';
|
||||
|
||||
class GifPickerDialog extends StatefulWidget {
|
||||
final void Function(TenorGif) onSelected;
|
||||
|
||||
const GifPickerDialog({
|
||||
required this.onSelected,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
GifPickerDialogState createState() => GifPickerDialogState();
|
||||
}
|
||||
|
||||
class GifPickerDialogState extends State<GifPickerDialog> {
|
||||
String? searchFilter;
|
||||
List<TenorGif> gifs = [];
|
||||
bool isLoading = false;
|
||||
bool hasError = false;
|
||||
String? errorMessage;
|
||||
Timer? _debounceTimer;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
String _nextPos = '0';
|
||||
bool _hasMoreResults = true;
|
||||
bool _isLoadingMore = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
_loadFeaturedGifs();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounceTimer?.cancel();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 300 &&
|
||||
!_isLoadingMore &&
|
||||
_hasMoreResults &&
|
||||
!isLoading) {
|
||||
_loadMoreGifs();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFeaturedGifs() async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
hasError = false;
|
||||
_nextPos = '0';
|
||||
_hasMoreResults = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await TenorApi.getFeaturedGifs();
|
||||
setState(() {
|
||||
gifs = response.results;
|
||||
isLoading = false;
|
||||
_nextPos = response.next;
|
||||
_hasMoreResults = response.next != '0' && response.results.isNotEmpty;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
hasError = true;
|
||||
errorMessage = e.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _searchGifs(String query) async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
hasError = false;
|
||||
_nextPos = '0';
|
||||
_hasMoreResults = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await TenorApi.searchGifs(query);
|
||||
setState(() {
|
||||
gifs = response.results;
|
||||
isLoading = false;
|
||||
_nextPos = response.next;
|
||||
_hasMoreResults = response.next != '0' && response.results.isNotEmpty;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
hasError = true;
|
||||
errorMessage = e.toString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMoreGifs() async {
|
||||
if (_isLoadingMore || !_hasMoreResults || isLoading || _nextPos == '0') {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoadingMore = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = searchFilter?.isNotEmpty == true
|
||||
? await TenorApi.searchGifs(searchFilter!, pos: _nextPos)
|
||||
: await TenorApi.getFeaturedGifs(pos: _nextPos);
|
||||
|
||||
setState(() {
|
||||
gifs.addAll(response.results);
|
||||
_nextPos = response.next;
|
||||
_hasMoreResults = response.next != '0' && response.results.isNotEmpty;
|
||||
_isLoadingMore = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoadingMore = false;
|
||||
// Don't disable _hasMoreResults on error, allow retry
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onSearchChanged(String query) {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 500), () {
|
||||
setState(() => searchFilter = query);
|
||||
if (query.trim().isEmpty) {
|
||||
_loadFeaturedGifs();
|
||||
} else {
|
||||
_searchGifs(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: theme.colorScheme.onInverseSurface,
|
||||
body: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: <Widget>[
|
||||
SliverAppBar(
|
||||
floating: true,
|
||||
pinned: true,
|
||||
scrolledUnderElevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
title: SizedBox(
|
||||
height: 42,
|
||||
child: TextField(
|
||||
autofocus: false,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
hintText: L10n.of(context).search,
|
||||
prefixIcon: const Icon(Icons.search_outlined),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
onChanged: _onSearchChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isLoading && gifs.isEmpty)
|
||||
const SliverFillRemaining(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
else if (hasError)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
L10n.of(context).oopsSomethingWentWrong,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
errorMessage ?? '',
|
||||
style: theme.textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (searchFilter?.isNotEmpty == true) {
|
||||
_searchGifs(searchFilter!);
|
||||
} else {
|
||||
_loadFeaturedGifs();
|
||||
}
|
||||
},
|
||||
child: Text(L10n.of(context).tryAgain),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (gifs.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.gif_box_outlined,
|
||||
size: 64,
|
||||
color:
|
||||
theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
L10n.of(context).nothingFound,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
sliver: SliverGrid(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount:
|
||||
MediaQuery.of(context).size.width > 600 ? 3 : 2,
|
||||
crossAxisSpacing: 8.0,
|
||||
mainAxisSpacing: 8.0,
|
||||
childAspectRatio: 1.0,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index >= gifs.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final gif = gifs[index];
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => widget.onSelected(gif),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline
|
||||
.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.network(
|
||||
gif.previewUrl,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder:
|
||||
(context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
value: loadingProgress
|
||||
.expectedTotalBytes !=
|
||||
null
|
||||
? loadingProgress
|
||||
.cumulativeBytesLoaded /
|
||||
loadingProgress
|
||||
.expectedTotalBytes!
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
color: theme.colorScheme
|
||||
.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.broken_image,
|
||||
color: theme
|
||||
.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// GIF indicator
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
vertical: 2.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius:
|
||||
BorderRadius.circular(4.0),
|
||||
),
|
||||
child: Text(
|
||||
'GIF',
|
||||
style: theme.textTheme.labelSmall
|
||||
?.copyWith(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (gif.title.isNotEmpty)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Colors.black
|
||||
.withValues(alpha: 0.7),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
gif.title,
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: Colors.white,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
childCount: gifs.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isLoadingMore)
|
||||
const SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
145
lib/utils/tenor_api.dart
Normal file
145
lib/utils/tenor_api.dart
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class TenorGif {
|
||||
final String id;
|
||||
final String title;
|
||||
final String url;
|
||||
final String previewUrl;
|
||||
final int width;
|
||||
final int height;
|
||||
|
||||
TenorGif({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.previewUrl,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
factory TenorGif.fromJson(Map<String, dynamic> json) {
|
||||
final mediaFormats = json['media_formats'] as Map<String, dynamic>;
|
||||
|
||||
// Find gif and tinygif formats - v2 API uses media_formats structure
|
||||
final gifFormat = mediaFormats['gif'] as Map<String, dynamic>?;
|
||||
final previewFormat =
|
||||
mediaFormats['tinygif'] as Map<String, dynamic>? ?? gifFormat;
|
||||
|
||||
if (gifFormat == null) {
|
||||
throw Exception('No GIF format found in media_formats');
|
||||
}
|
||||
|
||||
return TenorGif(
|
||||
id: json['id'],
|
||||
title: json['title'] ?? json['content_description'] ?? '',
|
||||
url: gifFormat['url'],
|
||||
previewUrl: previewFormat!['url'],
|
||||
width: gifFormat['dims'][0],
|
||||
height: gifFormat['dims'][1],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TenorApiResponse {
|
||||
final List<TenorGif> results;
|
||||
final String next;
|
||||
|
||||
TenorApiResponse({
|
||||
required this.results,
|
||||
required this.next,
|
||||
});
|
||||
|
||||
factory TenorApiResponse.fromJson(Map<String, dynamic> json) {
|
||||
final results = (json['results'] as List)
|
||||
.map((item) => TenorGif.fromJson(item))
|
||||
.toList();
|
||||
|
||||
return TenorApiResponse(
|
||||
results: results,
|
||||
next: json['next']?.toString() ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TenorApi {
|
||||
static const String _baseUrl = 'https://tenor.googleapis.com/v2';
|
||||
static const String _apiKey =
|
||||
'<tenor-api-key>'; // Test API key from documentation
|
||||
static const String _clientKey =
|
||||
'fluffychat_app'; // Client key for integration tracking
|
||||
static const int _limit = 20;
|
||||
|
||||
static Future<TenorApiResponse> searchGifs(
|
||||
String query, {
|
||||
String? pos,
|
||||
}) async {
|
||||
if (query.trim().isEmpty) {
|
||||
return getFeaturedGifs(pos: pos);
|
||||
}
|
||||
|
||||
try {
|
||||
final queryParams = {
|
||||
'key': _apiKey,
|
||||
'client_key': _clientKey,
|
||||
'q': query,
|
||||
'limit': _limit.toString(),
|
||||
'locale': 'en_US',
|
||||
'country': 'US',
|
||||
'contentfilter': 'medium',
|
||||
'media_filter': 'gif,tinygif',
|
||||
'ar_range': 'all',
|
||||
};
|
||||
|
||||
if (pos != null && pos.isNotEmpty && pos != '0') {
|
||||
queryParams['pos'] = pos;
|
||||
}
|
||||
|
||||
final uri =
|
||||
Uri.parse('$_baseUrl/search').replace(queryParameters: queryParams);
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
return TenorApiResponse.fromJson(data);
|
||||
} else {
|
||||
throw Exception('Failed to search GIFs: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error searching GIFs: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<TenorApiResponse> getFeaturedGifs({String? pos}) async {
|
||||
try {
|
||||
final queryParams = {
|
||||
'key': _apiKey,
|
||||
'client_key': _clientKey,
|
||||
'limit': _limit.toString(),
|
||||
'locale': 'en_US',
|
||||
'country': 'US',
|
||||
'contentfilter': 'medium',
|
||||
'media_filter': 'gif,tinygif',
|
||||
'ar_range': 'all',
|
||||
};
|
||||
|
||||
if (pos != null && pos.isNotEmpty && pos != '0') {
|
||||
queryParams['pos'] = pos;
|
||||
}
|
||||
|
||||
final uri =
|
||||
Uri.parse('$_baseUrl/featured').replace(queryParameters: queryParams);
|
||||
final response = await http.get(uri);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = json.decode(response.body);
|
||||
return TenorApiResponse.fromJson(data);
|
||||
} else {
|
||||
throw Exception('Failed to get featured GIFs: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Error getting featured GIFs: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:matrix/matrix.dart';
|
||||
|
||||
import 'package:fluffychat/config/themes.dart';
|
||||
|
|
@ -75,36 +76,79 @@ class _MxcImageState extends State<MxcImage> {
|
|||
final event = widget.event;
|
||||
|
||||
if (uri != null) {
|
||||
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
final width = widget.width;
|
||||
final realWidth = width == null ? null : width * devicePixelRatio;
|
||||
final height = widget.height;
|
||||
final realHeight = height == null ? null : height * devicePixelRatio;
|
||||
// Handle direct HTTP/HTTPS URLs
|
||||
if (uri.scheme == 'http' || uri.scheme == 'https') {
|
||||
try {
|
||||
final response = await http.get(uri);
|
||||
if (response.statusCode == 200) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_imageData = response.bodyBytes;
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
Logs().w('Failed to load HTTP image: $uri', e);
|
||||
}
|
||||
} else if (uri.scheme == 'mxc') {
|
||||
// Handle MXC URLs
|
||||
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
final width = widget.width;
|
||||
final realWidth = width == null ? null : width * devicePixelRatio;
|
||||
final height = widget.height;
|
||||
final realHeight = height == null ? null : height * devicePixelRatio;
|
||||
|
||||
final remoteData = await client.downloadMxcCached(
|
||||
uri,
|
||||
width: realWidth,
|
||||
height: realHeight,
|
||||
thumbnailMethod: widget.thumbnailMethod,
|
||||
isThumbnail: widget.isThumbnail,
|
||||
animated: widget.animated,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_imageData = remoteData;
|
||||
});
|
||||
final remoteData = await client.downloadMxcCached(
|
||||
uri,
|
||||
width: realWidth,
|
||||
height: realHeight,
|
||||
thumbnailMethod: widget.thumbnailMethod,
|
||||
isThumbnail: widget.isThumbnail,
|
||||
animated: widget.animated,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_imageData = remoteData;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (event != null) {
|
||||
final data = await event.downloadAndDecryptAttachment(
|
||||
getThumbnail: widget.isThumbnail,
|
||||
);
|
||||
if (data.detectFileType is MatrixImageFile || widget.isThumbnail) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_imageData = data.bytes;
|
||||
});
|
||||
return;
|
||||
// Check if the event has a direct HTTP URL
|
||||
final eventUrl = event.content.tryGet<String>('url');
|
||||
if (eventUrl != null) {
|
||||
final eventUri = Uri.tryParse(eventUrl);
|
||||
if (eventUri != null &&
|
||||
(eventUri.scheme == 'http' || eventUri.scheme == 'https')) {
|
||||
try {
|
||||
final response = await http.get(eventUri);
|
||||
if (response.statusCode == 200) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_imageData = response.bodyBytes;
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
Logs().w('Failed to load HTTP image from event: $eventUrl', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to Matrix attachment handling for MXC URLs
|
||||
try {
|
||||
final data = await event.downloadAndDecryptAttachment(
|
||||
getThumbnail: widget.isThumbnail,
|
||||
);
|
||||
if (data.detectFileType is MatrixImageFile || widget.isThumbnail) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_imageData = data.bytes;
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
Logs().w('Failed to download Matrix attachment', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue