commit
67f047ecfc
@ -0,0 +1,179 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/config/app_config.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;
|
||||||
|
|
||||||
|
const ChatSearchFilesTab({
|
||||||
|
required this.room,
|
||||||
|
required this.startSearch,
|
||||||
|
required this.searchStream,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StreamBuilder(
|
||||||
|
stream: searchStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (searchStream == null) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: 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 events = snapshot.data?.$1
|
||||||
|
.where((event) => event.messageType == MessageTypes.File)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
if (events.isEmpty) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: 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(
|
||||||
|
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.of(context).colorScheme.secondaryContainer,
|
||||||
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
onPressed: () => startSearch(
|
||||||
|
prevBatch: nextBatch,
|
||||||
|
previousSearchResult: events,
|
||||||
|
),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_downward_outlined,
|
||||||
|
),
|
||||||
|
label: const Text('Search more...'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (!sameEnvironment) ...[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
event.originServerTs.localizedTime(context),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
Material(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(AppConfig.borderRadius),
|
||||||
|
color: Theme.of(context).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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,167 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/pages/chat/events/image_bubble.dart';
|
||||||
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
|
|
||||||
|
class ChatSearchImagesTab extends StatelessWidget {
|
||||||
|
final Room room;
|
||||||
|
final Stream<(List<Event>, String?)>? searchStream;
|
||||||
|
final void Function({
|
||||||
|
String? prevBatch,
|
||||||
|
List<Event>? previousSearchResult,
|
||||||
|
}) startSearch;
|
||||||
|
|
||||||
|
const ChatSearchImagesTab({
|
||||||
|
required this.room,
|
||||||
|
required this.startSearch,
|
||||||
|
required this.searchStream,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StreamBuilder(
|
||||||
|
stream: searchStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (searchStream == null) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: 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 events = snapshot.data?.$1
|
||||||
|
.where((event) => event.messageType == MessageTypes.Image)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
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) {
|
||||||
|
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.of(context).colorScheme.secondaryContainer,
|
||||||
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
onPressed: () => startSearch(
|
||||||
|
prevBatch: nextBatch,
|
||||||
|
previousSearchResult: events,
|
||||||
|
),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_downward_outlined,
|
||||||
|
),
|
||||||
|
label: const Text('Search more...'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final monthEvents = eventsByMonthList[i].value;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Text(
|
||||||
|
DateFormat.yMMMM(
|
||||||
|
Localizations.localeOf(context).languageCode,
|
||||||
|
).format(eventsByMonthList[i].key),
|
||||||
|
style: Theme.of(context).textTheme.labelSmall,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
GridView.count(
|
||||||
|
shrinkWrap: true,
|
||||||
|
mainAxisSpacing: padding,
|
||||||
|
crossAxisSpacing: padding,
|
||||||
|
padding: const EdgeInsets.all(padding),
|
||||||
|
crossAxisCount: 3,
|
||||||
|
children: monthEvents
|
||||||
|
.map(
|
||||||
|
(event) => ImageBubble(
|
||||||
|
event,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,191 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/utils/date_time_extension.dart';
|
||||||
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
|
import 'package:fluffychat/utils/url_launcher.dart';
|
||||||
|
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;
|
||||||
|
|
||||||
|
const ChatSearchMessageTab({
|
||||||
|
required this.searchQuery,
|
||||||
|
required this.room,
|
||||||
|
required this.searchStream,
|
||||||
|
required this.startSearch,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StreamBuilder(
|
||||||
|
key: ValueKey(searchQuery),
|
||||||
|
stream: searchStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (searchStream == null) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: 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 events = snapshot.data?.$1
|
||||||
|
.where(
|
||||||
|
(event) => {
|
||||||
|
MessageTypes.Text,
|
||||||
|
MessageTypes.Notice,
|
||||||
|
}.contains(event.messageType),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
return SelectionArea(
|
||||||
|
child: ListView.separated(
|
||||||
|
itemCount: events.length + 1,
|
||||||
|
separatorBuilder: (context, _) => Divider(
|
||||||
|
color: Theme.of(context).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.of(context).colorScheme.secondaryContainer,
|
||||||
|
foregroundColor:
|
||||||
|
Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
|
),
|
||||||
|
onPressed: () => startSearch(
|
||||||
|
prevBatch: nextBatch,
|
||||||
|
previousSearchResult: events,
|
||||||
|
),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_downward_outlined,
|
||||||
|
),
|
||||||
|
label: const Text('Search more...'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageSearchResultListTile extends StatelessWidget {
|
||||||
|
const _MessageSearchResultListTile({
|
||||||
|
required this.sender,
|
||||||
|
required this.displayname,
|
||||||
|
required this.event,
|
||||||
|
required this.room,
|
||||||
|
});
|
||||||
|
|
||||||
|
final User sender;
|
||||||
|
final String displayname;
|
||||||
|
final Event event;
|
||||||
|
final Room room;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Avatar(
|
||||||
|
mxContent: sender.avatarUrl,
|
||||||
|
name: displayname,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
displayname,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
' | ${event.originServerTs.localizedTimeShort(context)}',
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Linkify(
|
||||||
|
options: const LinkifyOptions(humanize: false),
|
||||||
|
linkStyle: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
decorationColor: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
onOpen: (url) => UrlLauncher(context, url.url).launchUrl(),
|
||||||
|
text: event.calcLocalizedBodyFallback(
|
||||||
|
plaintextBody: true,
|
||||||
|
removeMarkdown: true,
|
||||||
|
MatrixLocals(
|
||||||
|
L10n.of(context)!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
maxLines: 4,
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.chevron_right_outlined,
|
||||||
|
),
|
||||||
|
onPressed: () => context.go(
|
||||||
|
'/${Uri(
|
||||||
|
pathSegments: ['rooms', room.id],
|
||||||
|
queryParameters: {'event': event.eventId},
|
||||||
|
)}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:matrix/matrix.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/pages/chat_search/chat_search_view.dart';
|
||||||
|
import 'package:fluffychat/widgets/matrix.dart';
|
||||||
|
|
||||||
|
class ChatSearchPage extends StatefulWidget {
|
||||||
|
final String roomId;
|
||||||
|
const ChatSearchPage({required this.roomId, super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChatSearchController createState() => ChatSearchController();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatSearchController extends State<ChatSearchPage>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
Room? get room => Matrix.of(context).client.getRoomById(widget.roomId);
|
||||||
|
|
||||||
|
final TextEditingController searchController = TextEditingController();
|
||||||
|
late final TabController tabController;
|
||||||
|
|
||||||
|
Timeline? timeline;
|
||||||
|
|
||||||
|
Stream<(List<Event>, String?)>? searchStream;
|
||||||
|
|
||||||
|
void restartSearch() {
|
||||||
|
if (tabController.index == 0 && searchController.text.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
searchStream = null;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
searchStream = const Stream.empty();
|
||||||
|
});
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||||
|
startSearch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void startSearch({
|
||||||
|
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: tabController.index == 0 ? searchController.text : null,
|
||||||
|
searchFunc: switch (tabController.index) {
|
||||||
|
1 => (event) => event.messageType == MessageTypes.Image,
|
||||||
|
2 => (event) => event.messageType == MessageTypes.File,
|
||||||
|
int() => null,
|
||||||
|
},
|
||||||
|
prevBatch: prevBatch,
|
||||||
|
requestHistoryCount: 1000,
|
||||||
|
limit: 32,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(result) => (
|
||||||
|
[
|
||||||
|
if (previousSearchResult != null) ...previousSearchResult,
|
||||||
|
...result.$1,
|
||||||
|
],
|
||||||
|
result.$2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.asBroadcastStream();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
tabController = TabController(initialIndex: 0, length: 3, vsync: this);
|
||||||
|
tabController.addListener(restartSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
tabController.removeListener(restartSearch);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => ChatSearchView(this);
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||||
|
|
||||||
|
import 'package:fluffychat/config/themes.dart';
|
||||||
|
import 'package:fluffychat/pages/chat_search/chat_search_files_tab.dart';
|
||||||
|
import 'package:fluffychat/pages/chat_search/chat_search_images_tab.dart';
|
||||||
|
import 'package:fluffychat/pages/chat_search/chat_search_message_tab.dart';
|
||||||
|
import 'package:fluffychat/pages/chat_search/chat_search_page.dart';
|
||||||
|
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
|
||||||
|
import 'package:fluffychat/widgets/layouts/max_width_body.dart';
|
||||||
|
|
||||||
|
class ChatSearchView extends StatelessWidget {
|
||||||
|
final ChatSearchController controller;
|
||||||
|
|
||||||
|
const ChatSearchView(this.controller, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final room = controller.room;
|
||||||
|
if (room == null) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text(L10n.of(context)!.oopsSomethingWentWrong)),
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child:
|
||||||
|
Text(L10n.of(context)!.youAreNoLongerParticipatingInThisChat),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: const Center(child: BackButton()),
|
||||||
|
titleSpacing: 0,
|
||||||
|
title: Text(
|
||||||
|
L10n.of(context)!.searchIn(
|
||||||
|
room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: MaxWidthBody(
|
||||||
|
withScrolling: false,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (FluffyThemes.isThreeColumnMode(context))
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: controller.searchController,
|
||||||
|
onSubmitted: (_) => controller.restartSearch(),
|
||||||
|
autofocus: true,
|
||||||
|
enabled: controller.tabController.index == 0,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: L10n.of(context)!.searchIn(
|
||||||
|
room.getLocalizedDisplayname(
|
||||||
|
MatrixLocals(L10n.of(context)!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
suffixIcon: const Icon(Icons.search_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TabBar(
|
||||||
|
controller: controller.tabController,
|
||||||
|
tabs: [
|
||||||
|
Tab(child: Text(L10n.of(context)!.messages)),
|
||||||
|
Tab(child: Text(L10n.of(context)!.photos)),
|
||||||
|
Tab(child: Text(L10n.of(context)!.files)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: controller.tabController,
|
||||||
|
children: [
|
||||||
|
ChatSearchMessageTab(
|
||||||
|
searchQuery: controller.searchController.text,
|
||||||
|
room: room,
|
||||||
|
startSearch: controller.startSearch,
|
||||||
|
searchStream: controller.searchStream,
|
||||||
|
),
|
||||||
|
ChatSearchImagesTab(
|
||||||
|
room: room,
|
||||||
|
startSearch: controller.startSearch,
|
||||||
|
searchStream: controller.searchStream,
|
||||||
|
),
|
||||||
|
ChatSearchFilesTab(
|
||||||
|
room: room,
|
||||||
|
startSearch: controller.startSearch,
|
||||||
|
searchStream: controller.searchStream,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue