fluffychat merge
This commit is contained in:
commit
8750f116bd
10 changed files with 444 additions and 530 deletions
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
63
lib/pages/chat_search/search_footer.dart
Normal file
63
lib/pages/chat_search/search_footer.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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#
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue