fluffychat merge

This commit is contained in:
ggurdin 2026-02-05 16:54:16 -05:00
commit 8750f116bd
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
10 changed files with 444 additions and 530 deletions

View file

@ -3467,6 +3467,16 @@
"skipChatBackupWarning": "Are you sure? Without enabling the chat backup you may lose access to your messages if you switch your device.",
"loadingMessages": "Loading messages",
"setupChatBackup": "Set up chat backup",
"noMoreResultsFound": "No more results found",
"chatSearchedUntil": "Chat searched until {time}",
"@chatSearchedUntil": {
"type": "String",
"placeholders": {
"time": {
"type": "String"
}
}
},
"ignore": "Block",
"ignoredUsers": "Blocked users",
"writeAMessageLangCodes": "Type in {l1} or {l2}...",
@ -3527,7 +3537,6 @@
"updateLanguage": "My languages",
"whatLanguageYouWantToLearn": "What language do you want to learn?",
"whatIsYourBaseLanguage": "What is your base language?",
"saveChanges": "Save changes",
"publicProfileTitle": "Allow my profile to be found in search",
"publicProfileDesc": "By turning on, you enable other users to find your profile in the global search bar and send requests to chat. At this point, you can choose to accept or deny the request.",
"errorDisableIT": "Translation assistance is turned off.",

View file

@ -4,167 +4,106 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat_search/search_footer.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
class ChatSearchFilesTab extends StatelessWidget {
final Room room;
final Stream<(List<Event>, String?)>? searchStream;
final void Function({String? prevBatch, List<Event>? previousSearchResult})
startSearch;
final List<Event> events;
final void Function() onStartSearch;
final bool endReached, isLoading;
final DateTime? searchedUntil;
const ChatSearchFilesTab({
required this.room,
required this.startSearch,
required this.searchStream,
required this.events,
required this.onStartSearch,
required this.endReached,
required this.isLoading,
super.key,
required this.searchedUntil,
});
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: searchStream,
builder: (context, snapshot) {
final theme = Theme.of(context);
final events = snapshot.data?.$1;
if (searchStream == null || events == null) {
return Column(
mainAxisAlignment: .center,
children: [
const CircularProgressIndicator.adaptive(strokeWidth: 2),
const SizedBox(height: 8),
Text(
L10n.of(context).searchIn(
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
),
),
],
);
}
if (events.isEmpty) {
return Column(
mainAxisAlignment: .center,
children: [
const Icon(Icons.file_present_outlined, size: 64),
const SizedBox(height: 8),
Text(L10n.of(context).nothingFound),
],
);
}
return SelectionArea(
child: ListView.builder(
final theme = Theme.of(context);
return SelectionArea(
child: ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: events.length + 1,
itemBuilder: (context, i) {
if (i == events.length) {
return SearchFooter(
searchedUntil: searchedUntil,
endReached: endReached,
isLoading: isLoading,
onStartSearch: onStartSearch,
);
}
final event = events[i];
final filename =
event.content.tryGet<String>('filename') ??
event.content.tryGet<String>('body') ??
L10n.of(context).unknownEvent('File');
final filetype = (filename.contains('.')
? filename.split('.').last.toUpperCase()
: event.content
.tryGetMap<String, dynamic>('info')
?.tryGet<String>('mimetype')
?.toUpperCase() ??
'UNKNOWN');
final sizeString = event.sizeString;
final prevEvent = i > 0 ? events[i - 1] : null;
final sameEnvironment = prevEvent == null
? false
: prevEvent.originServerTs.sameEnvironment(event.originServerTs);
return Padding(
padding: const EdgeInsets.all(8.0),
itemCount: events.length + 1,
itemBuilder: (context, i) {
if (i == events.length) {
if (snapshot.connectionState != ConnectionState.done) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
),
);
}
final nextBatch = snapshot.data?.$2;
if (nextBatch == null) {
return const SizedBox.shrink();
}
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: TextButton.icon(
style: TextButton.styleFrom(
backgroundColor: theme.colorScheme.secondaryContainer,
foregroundColor: theme.colorScheme.onSecondaryContainer,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!sameEnvironment) ...[
Row(
children: [
Expanded(
child: Container(height: 1, color: theme.dividerColor),
),
onPressed: () => startSearch(
prevBatch: nextBatch,
previousSearchResult: events,
),
icon: const Icon(Icons.arrow_downward_outlined),
label: Text(L10n.of(context).searchMore),
),
),
);
}
final event = events[i];
final filename =
event.content.tryGet<String>('filename') ??
event.content.tryGet<String>('body') ??
L10n.of(context).unknownEvent('File');
final filetype = (filename.contains('.')
? filename.split('.').last.toUpperCase()
: event.content
.tryGetMap<String, dynamic>('info')
?.tryGet<String>('mimetype')
?.toUpperCase() ??
'UNKNOWN');
final sizeString = event.sizeString;
final prevEvent = i > 0 ? events[i - 1] : null;
final sameEnvironment = prevEvent == null
? false
: prevEvent.originServerTs.sameEnvironment(
event.originServerTs,
);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: .min,
children: [
if (!sameEnvironment) ...[
Row(
children: [
Expanded(
child: Container(
height: 1,
color: theme.dividerColor,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
event.originServerTs.localizedTime(context),
style: theme.textTheme.labelSmall,
textAlign: TextAlign.center,
),
),
Expanded(
child: Container(
height: 1,
color: theme.dividerColor,
),
),
],
),
const SizedBox(height: 4),
],
Material(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
color: theme.colorScheme.onInverseSurface,
clipBehavior: Clip.hardEdge,
child: ListTile(
leading: const Icon(Icons.file_present_outlined),
title: Text(
filename,
maxLines: 1,
overflow: TextOverflow.ellipsis,
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
event.originServerTs.localizedTime(context),
style: theme.textTheme.labelSmall,
textAlign: TextAlign.center,
),
subtitle: Text('$sizeString | $filetype'),
onTap: () => event.saveFile(context),
),
Expanded(
child: Container(height: 1, color: theme.dividerColor),
),
],
),
const SizedBox(height: 4),
],
Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
color: theme.colorScheme.onInverseSurface,
clipBehavior: Clip.hardEdge,
child: ListTile(
leading: const Icon(Icons.file_present_outlined),
title: Text(
filename,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
subtitle: Text('$sizeString | $filetype'),
onTap: () => event.saveFile(context),
),
),
);
},
),
);
},
],
),
);
},
),
);
}
}

View file

@ -6,169 +6,147 @@ import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat/events/video_player.dart';
import 'package:fluffychat/pages/chat_search/search_footer.dart';
import 'package:fluffychat/pages/image_viewer/image_viewer.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/mxc_image.dart';
class ChatSearchImagesTab extends StatelessWidget {
final Room room;
final Stream<(List<Event>, String?)>? searchStream;
final void Function({String? prevBatch, List<Event>? previousSearchResult})
startSearch;
final List<Event> events;
final void Function() onStartSearch;
final bool endReached, isLoading;
final DateTime? searchedUntil;
const ChatSearchImagesTab({
required this.room,
required this.startSearch,
required this.searchStream,
required this.events,
required this.onStartSearch,
required this.endReached,
required this.isLoading,
super.key,
required this.searchedUntil,
});
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(AppConfig.borderRadius / 2);
return StreamBuilder(
stream: searchStream,
builder: (context, snapshot) {
final theme = Theme.of(context);
final events = snapshot.data?.$1;
if (searchStream == null || events == null) {
return Column(
mainAxisAlignment: .center,
children: [
const CircularProgressIndicator.adaptive(strokeWidth: 2),
const SizedBox(height: 8),
Text(
L10n.of(context).searchIn(
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
),
),
],
final theme = Theme.of(context);
if (events.isEmpty) {
if (isLoading) {
return const Center(child: CircularProgressIndicator.adaptive());
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator.adaptive(strokeWidth: 2),
const SizedBox(height: 8),
Text(
L10n.of(context).searchIn(
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
),
),
],
);
}
if (events.isEmpty) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.photo_outlined, size: 64),
const SizedBox(height: 8),
Text(L10n.of(context).nothingFound),
],
);
}
final eventsByMonth = <DateTime, List<Event>>{};
for (final event in events) {
final month = DateTime(
event.originServerTs.year,
event.originServerTs.month,
);
eventsByMonth[month] ??= [];
eventsByMonth[month]!.add(event);
}
final eventsByMonthList = eventsByMonth.entries.toList();
const padding = 8.0;
return ListView.builder(
itemCount: eventsByMonth.length + 1,
itemBuilder: (context, i) {
if (i == eventsByMonth.length) {
return SearchFooter(
searchedUntil: searchedUntil,
endReached: endReached,
isLoading: isLoading,
onStartSearch: onStartSearch,
);
}
if (events.isEmpty) {
return Column(
mainAxisAlignment: .center,
children: [
const Icon(Icons.photo_outlined, size: 64),
const SizedBox(height: 8),
Text(L10n.of(context).nothingFound),
],
);
}
final eventsByMonth = <DateTime, List<Event>>{};
for (final event in events) {
final month = DateTime(
event.originServerTs.year,
event.originServerTs.month,
);
eventsByMonth[month] ??= [];
eventsByMonth[month]!.add(event);
}
final eventsByMonthList = eventsByMonth.entries.toList();
const padding = 8.0;
return ListView.builder(
itemCount: eventsByMonth.length + 1,
itemBuilder: (context, i) {
if (i == eventsByMonth.length) {
if (snapshot.connectionState != ConnectionState.done) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
),
);
}
final nextBatch = snapshot.data?.$2;
if (nextBatch == null) {
return const SizedBox.shrink();
}
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: TextButton.icon(
style: TextButton.styleFrom(
backgroundColor: theme.colorScheme.secondaryContainer,
foregroundColor: theme.colorScheme.onSecondaryContainer,
),
onPressed: () => startSearch(
prevBatch: nextBatch,
previousSearchResult: events,
),
icon: const Icon(Icons.arrow_downward_outlined),
label: Text(L10n.of(context).searchMore),
),
),
);
}
final monthEvents = eventsByMonthList[i].value;
return Column(
mainAxisSize: .min,
final monthEvents = eventsByMonthList[i].value;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4),
Row(
children: [
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: Container(height: 1, color: theme.dividerColor),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
DateFormat.yMMMM(
Localizations.localeOf(context).languageCode,
).format(eventsByMonthList[i].key),
style: theme.textTheme.labelSmall,
textAlign: TextAlign.center,
),
),
Expanded(
child: Container(height: 1, color: theme.dividerColor),
),
],
Expanded(
child: Container(height: 1, color: theme.dividerColor),
),
GridView.count(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
mainAxisSpacing: padding,
crossAxisSpacing: padding,
clipBehavior: Clip.hardEdge,
padding: const EdgeInsets.all(padding),
crossAxisCount: 3,
children: monthEvents.map((event) {
if (event.messageType == MessageTypes.Video) {
return Material(
clipBehavior: Clip.hardEdge,
borderRadius: borderRadius,
child: EventVideoPlayer(event),
);
}
return InkWell(
onTap: () => showDialog(
context: context,
builder: (_) =>
ImageViewer(event, outerContext: context),
),
borderRadius: borderRadius,
child: Material(
clipBehavior: Clip.hardEdge,
borderRadius: borderRadius,
child: MxcImage(
event: event,
width: 128,
height: 128,
fit: BoxFit.cover,
animated: true,
isThumbnail: true,
),
),
);
}).toList(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
DateFormat.yMMMM(
Localizations.localeOf(context).languageCode,
).format(eventsByMonthList[i].key),
style: theme.textTheme.labelSmall,
textAlign: TextAlign.center,
),
),
Expanded(
child: Container(height: 1, color: theme.dividerColor),
),
],
);
},
),
GridView.count(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
mainAxisSpacing: padding,
crossAxisSpacing: padding,
clipBehavior: Clip.hardEdge,
padding: const EdgeInsets.all(padding),
crossAxisCount: 3,
children: monthEvents.map((event) {
if (event.messageType == MessageTypes.Video) {
return Material(
clipBehavior: Clip.hardEdge,
borderRadius: borderRadius,
child: EventVideoPlayer(event),
);
}
return InkWell(
onTap: () => showDialog(
context: context,
builder: (_) => ImageViewer(event, outerContext: context),
),
borderRadius: borderRadius,
child: Material(
clipBehavior: Clip.hardEdge,
borderRadius: borderRadius,
child: MxcImage(
event: event,
width: 128,
height: 128,
fit: BoxFit.cover,
animated: true,
isThumbnail: true,
),
),
);
}).toList(),
),
],
);
},
);

View file

@ -4,6 +4,7 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:matrix/matrix.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/pages/chat_search/search_footer.dart';
import 'package:fluffychat/pangea/navigation/navigation_util.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
@ -13,101 +14,74 @@ import 'package:fluffychat/widgets/avatar.dart';
class ChatSearchMessageTab extends StatelessWidget {
final String searchQuery;
final Room room;
final Stream<(List<Event>, String?)>? searchStream;
final void Function({String? prevBatch, List<Event>? previousSearchResult})
startSearch;
final List<Event> events;
final void Function() onStartSearch;
final bool endReached, isLoading;
final DateTime? searchedUntil;
const ChatSearchMessageTab({
required this.searchQuery,
required this.room,
required this.searchStream,
required this.startSearch,
required this.onStartSearch,
required this.events,
required this.searchedUntil,
required this.endReached,
required this.isLoading,
super.key,
});
@override
Widget build(BuildContext context) {
return StreamBuilder(
key: ValueKey(searchQuery),
stream: searchStream,
builder: (context, snapshot) {
final theme = Theme.of(context);
if (searchStream == null) {
return Column(
mainAxisAlignment: .center,
children: [
const Icon(Icons.search_outlined, size: 64),
const SizedBox(height: 8),
Text(
L10n.of(context).searchIn(
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
),
final theme = Theme.of(context);
if (events.isEmpty) {
if (isLoading) {
return const Center(child: CircularProgressIndicator.adaptive());
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.search_outlined, size: 64),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Text(
L10n.of(context).searchIn(
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context))),
),
],
);
}
final events = snapshot.data?.$1 ?? [];
// #Pangea
events.removeWhere(
(event) =>
event.type != EventTypes.Message ||
event.messageType != MessageTypes.Text ||
event.redacted,
);
// Pangea#
return SelectionArea(
child: ListView.separated(
itemCount: events.length + 1,
separatorBuilder: (context, _) =>
Divider(color: theme.dividerColor, height: 1),
itemBuilder: (context, i) {
if (i == events.length) {
if (snapshot.connectionState != ConnectionState.done) {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator.adaptive(strokeWidth: 2),
),
);
}
final nextBatch = snapshot.data?.$2;
if (nextBatch == null) {
return const SizedBox.shrink();
}
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: TextButton.icon(
style: TextButton.styleFrom(
backgroundColor: theme.colorScheme.secondaryContainer,
foregroundColor: theme.colorScheme.onSecondaryContainer,
),
onPressed: () => startSearch(
prevBatch: nextBatch,
previousSearchResult: events,
),
icon: const Icon(Icons.arrow_downward_outlined),
label: Text(L10n.of(context).searchMore),
),
),
);
}
final event = events[i];
final sender = event.senderFromMemoryOrFallback;
final displayname = sender.calcDisplayname(
i18n: MatrixLocals(L10n.of(context)),
);
return _MessageSearchResultListTile(
sender: sender,
displayname: displayname,
event: event,
room: room,
);
},
textAlign: TextAlign.center,
),
),
);
},
],
);
}
return SelectionArea(
child: ListView.separated(
itemCount: events.length + 1,
separatorBuilder: (context, _) =>
Divider(color: theme.dividerColor, height: 1),
itemBuilder: (context, i) {
if (i == events.length) {
return SearchFooter(
searchedUntil: searchedUntil,
endReached: endReached,
isLoading: isLoading,
onStartSearch: onStartSearch,
);
}
final event = events[i];
final sender = event.senderFromMemoryOrFallback;
final displayname = sender.calcDisplayname(
i18n: MatrixLocals(L10n.of(context)),
);
return _MessageSearchResultListTile(
sender: sender,
displayname: displayname,
event: event,
room: room,
);
},
),
);
}
}

View file

@ -1,5 +1,3 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
@ -22,170 +20,112 @@ class ChatSearchController extends State<ChatSearchPage>
final TextEditingController searchController = TextEditingController();
late final TabController tabController;
Timeline? timeline;
Stream<(List<Event>, String?)>? searchStream;
Stream<(List<Event>, String?)>? galleryStream;
Stream<(List<Event>, String?)>? fileStream;
final List<Event> messages = [];
final List<Event> images = [];
final List<Event> files = [];
String? messagesNextBatch, imagesNextBatch, filesNextBatch;
bool messagesEndReached = false;
bool imagesEndReached = false;
bool filesEndReached = false;
bool isLoading = false;
DateTime? searchedUntil;
void restartSearch() {
if (searchController.text.isEmpty) {
setState(() {
searchStream = null;
});
return;
}
setState(() {
searchStream = const Stream.empty();
messages.clear();
images.clear();
files.clear();
messagesNextBatch = imagesNextBatch = filesNextBatch = searchedUntil =
null;
messagesEndReached = imagesEndReached = filesEndReached = false;
});
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
startMessageSearch();
startSearch();
});
}
void startMessageSearch({
String? prevBatch,
List<Event>? previousSearchResult,
}) async {
final timeline = this.timeline ??= await room!.getTimeline();
if (tabController.index == 0 && searchController.text.isEmpty) {
return;
}
setState(() {
searchStream = timeline
.startSearch(
searchTerm: searchController.text,
prevBatch: prevBatch,
requestHistoryCount: 1000,
limit: 32,
)
.map(
(result) => (
[
if (previousSearchResult != null) ...previousSearchResult,
...result.$1,
],
result.$2,
),
)
// Deduplication workaround for
// https://github.com/famedly/matrix-dart-sdk/issues/1831
.map(
(result) => (
<String, Event>{
for (final event in result.$1) event.eventId: event,
// #Pangea
// }.values.toList(),
}.values
.toList()
.where(
(e) => !e.hasAggregatedEvents(
timeline,
RelationshipTypes.edit,
),
)
.toList(),
// Pangea#
result.$2,
),
)
void startSearch() async {
switch (tabController.index) {
case 0:
final searchQuery = searchController.text.trim();
if (searchQuery.isEmpty) return;
setState(() {
isLoading = true;
});
final result = await room!.searchEvents(
searchTerm: searchController.text.trim(),
nextBatch: messagesNextBatch,
);
setState(() {
isLoading = false;
// #Pangea
.where((result) => result.$1.isNotEmpty)
// messages.addAll(result.events);
messages.addAll(
result.events.where(
(e) =>
room?.timeline == null ||
!e.hasAggregatedEvents(
room!.timeline!,
RelationshipTypes.edit,
),
),
);
// Pangea#
.asBroadcastStream();
});
}
void startGallerySearch({
String? prevBatch,
List<Event>? previousSearchResult,
}) async {
final timeline = this.timeline ??= await room!.getTimeline();
setState(() {
galleryStream = timeline
.startSearch(
searchFunc: (event) => {
MessageTypes.Image,
MessageTypes.Video,
}.contains(event.messageType),
prevBatch: prevBatch,
requestHistoryCount: 1000,
limit: 32,
)
.map(
(result) => (
[
if (previousSearchResult != null) ...previousSearchResult,
...result.$1,
],
result.$2,
),
)
// Deduplication workaround for
// https://github.com/famedly/matrix-dart-sdk/issues/1831
.map(
(result) => (
<String, Event>{
for (final event in result.$1) event.eventId: event,
}.values.toList(),
result.$2,
),
)
.asBroadcastStream();
});
}
void startFileSearch({
String? prevBatch,
List<Event>? previousSearchResult,
}) async {
final timeline = this.timeline ??= await room!.getTimeline();
setState(() {
fileStream = timeline
.startSearch(
searchFunc: (event) =>
event.messageType == MessageTypes.File ||
(event.messageType == MessageTypes.Audio &&
!event.content.containsKey('org.matrix.msc3245.voice')),
prevBatch: prevBatch,
requestHistoryCount: 1000,
limit: 32,
)
.map(
(result) => (
[
if (previousSearchResult != null) ...previousSearchResult,
...result.$1,
],
result.$2,
),
)
// Deduplication workaround for
// https://github.com/famedly/matrix-dart-sdk/issues/1831
.map(
(result) => (
<String, Event>{
for (final event in result.$1) event.eventId: event,
}.values.toList(),
result.$2,
),
)
.asBroadcastStream();
});
messagesNextBatch = result.nextBatch;
messagesEndReached = result.nextBatch == null;
searchedUntil = result.searchedUntil;
});
return;
case 1:
setState(() {
isLoading = true;
});
final result = await room!.searchEvents(
searchFunc: (event) => {
MessageTypes.Image,
MessageTypes.Video,
}.contains(event.messageType),
nextBatch: imagesNextBatch,
);
setState(() {
isLoading = false;
images.addAll(result.events);
imagesNextBatch = result.nextBatch;
imagesEndReached = result.nextBatch == null;
searchedUntil = result.searchedUntil;
});
return;
case 2:
setState(() {
isLoading = true;
});
final result = await room!.searchEvents(
searchFunc: (event) =>
event.messageType == MessageTypes.File ||
(event.messageType == MessageTypes.Audio &&
!event.content.containsKey('org.matrix.msc3245.voice')),
nextBatch: filesNextBatch,
);
setState(() {
isLoading = false;
files.addAll(result.events);
filesNextBatch = result.nextBatch;
filesEndReached = result.nextBatch == null;
searchedUntil = result.searchedUntil;
});
return;
default:
return;
}
}
void _onTabChanged() {
switch (tabController.index) {
case 1:
startGallerySearch();
break;
case 2:
startFileSearch();
startSearch();
break;
case 0:
default:
restartSearch();
break;

View file

@ -85,18 +85,27 @@ class ChatSearchView extends StatelessWidget {
ChatSearchMessageTab(
searchQuery: controller.searchController.text,
room: room,
startSearch: controller.startMessageSearch,
searchStream: controller.searchStream,
onStartSearch: controller.startSearch,
events: controller.messages,
endReached: controller.messagesEndReached,
isLoading: controller.isLoading,
searchedUntil: controller.searchedUntil,
),
ChatSearchImagesTab(
room: room,
startSearch: controller.startGallerySearch,
searchStream: controller.galleryStream,
onStartSearch: controller.startSearch,
events: controller.images,
endReached: controller.imagesEndReached,
isLoading: controller.isLoading,
searchedUntil: controller.searchedUntil,
),
ChatSearchFilesTab(
room: room,
startSearch: controller.startFileSearch,
searchStream: controller.fileStream,
onStartSearch: controller.startSearch,
events: controller.files,
endReached: controller.filesEndReached,
isLoading: controller.isLoading,
searchedUntil: controller.searchedUntil,
),
],
),

View file

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
class SearchFooter extends StatelessWidget {
final DateTime? searchedUntil;
final bool endReached, isLoading;
final void Function() onStartSearch;
const SearchFooter({
super.key,
required this.searchedUntil,
required this.endReached,
required this.isLoading,
required this.onStartSearch,
});
@override
Widget build(BuildContext context) {
if (endReached) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(L10n.of(context).noMoreResultsFound),
),
);
}
final theme = Theme.of(context);
final searchedUntil = this.searchedUntil;
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: .min,
children: [
if (searchedUntil != null)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
L10n.of(
context,
).chatSearchedUntil(searchedUntil.localizedTime(context)),
style: TextStyle(fontSize: 10.5),
),
),
TextButton.icon(
style: TextButton.styleFrom(
backgroundColor: theme.colorScheme.secondaryContainer,
foregroundColor: theme.colorScheme.onSecondaryContainer,
),
onPressed: isLoading ? null : onStartSearch,
icon: isLoading
? const CircularProgressIndicator.adaptive()
: const Icon(Icons.arrow_downward_outlined),
label: Text(L10n.of(context).searchMore),
),
],
),
),
);
}
}

View file

@ -1,3 +1,8 @@
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/l10n/l10n.dart';
import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart';
@ -5,10 +10,6 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart'
import 'package:fluffychat/widgets/app_lock.dart';
import 'package:fluffychat/widgets/future_loading_dialog.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:matrix/matrix.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'settings_security_view.dart';
class SettingsSecurity extends StatefulWidget {

View file

@ -1475,7 +1475,7 @@ packages:
path: "/Users/ggurdin/pangea/matrix-dart-sdk"
relative: false
source: path
version: "4.0.0"
version: "4.0.1"
meta:
dependency: transitive
description:

View file

@ -53,14 +53,15 @@ dependencies:
highlight: ^0.7.0
html: ^0.15.4
http: ^1.6.0
image: ^4.6.0
image: ^4.7.1
image_picker: ^1.2.1
intl: any
just_audio: ^0.10.5
latlong2: ^0.9.1
linkify: ^5.0.0
# #Pangea
# matrix: ^4.0.1
# matrix: #^4.0.1
# git: https://github.com/famedly/matrix-dart-sdk.git
matrix:
path: /Users/ggurdin/pangea/matrix-dart-sdk
# Pangea#