From 6ec1d80f49621a8d78715496842f7ee0396ea3ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= <pubgit@mikescher.com>
Date: Sun, 13 Apr 2025 17:43:18 +0200
Subject: [PATCH] finish sender_list && plain-text-search

---
 flutter/lib/api/api_client.dart               |   3 +
 .../layout/app_bar_filter_dialog.dart         |   7 +-
 .../modals/filter_modal_searchplain.dart      |  60 +++++++++
 .../modals/filter_modal_sendername.dart       |   6 +-
 .../filtered_message_view.dart                | 120 ++++++++++++++++++
 .../message_list/message_filter_chiplet.dart  |   3 +
 .../lib/pages/message_list/message_list.dart  |  20 ++-
 .../lib/pages/message_view/message_view.dart  |   5 +-
 .../pages/sender_list/sender_list_item.dart   |   7 +-
 9 files changed, 219 insertions(+), 12 deletions(-)
 create mode 100644 flutter/lib/components/modals/filter_modal_searchplain.dart
 create mode 100644 flutter/lib/pages/filtered_message_view/filtered_message_view.dart

diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart
index cc474a9..ac57d21 100644
--- a/flutter/lib/api/api_client.dart
+++ b/flutter/lib/api/api_client.dart
@@ -32,6 +32,7 @@ enum ChannelSelector {
 class MessageFilter {
   List<String>? channelIDs;
   List<String>? searchFilter;
+  List<String>? plainSearchFilter;
   List<String>? senderNames;
   List<String>? usedKeys;
   List<int>? priority;
@@ -42,6 +43,7 @@ class MessageFilter {
   MessageFilter({
     this.channelIDs,
     this.searchFilter,
+    this.plainSearchFilter,
     this.senderNames,
     this.usedKeys,
     this.priority,
@@ -288,6 +290,7 @@ class APIClient {
         'next_page_token': [pageToken],
         if (pageSize != null) 'page_size': [pageSize.toString()],
         if (filter?.searchFilter != null) 'search': filter!.searchFilter!,
+        if (filter?.plainSearchFilter != null) 'string_search': filter!.plainSearchFilter!,
         if (filter?.channelIDs != null) 'channel_id': filter!.channelIDs!,
         if (filter?.senderNames != null) 'sender': filter!.senderNames!,
         if (filter?.hasSenderName != null) 'has_sender': [filter!.hasSenderName!.toString()],
diff --git a/flutter/lib/components/layout/app_bar_filter_dialog.dart b/flutter/lib/components/layout/app_bar_filter_dialog.dart
index 2b7489e..547825f 100644
--- a/flutter/lib/components/layout/app_bar_filter_dialog.dart
+++ b/flutter/lib/components/layout/app_bar_filter_dialog.dart
@@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 import 'package:simplecloudnotifier/components/modals/filter_modal_channel.dart';
 import 'package:simplecloudnotifier/components/modals/filter_modal_keytoken.dart';
 import 'package:simplecloudnotifier/components/modals/filter_modal_priority.dart';
+import 'package:simplecloudnotifier/components/modals/filter_modal_searchplain.dart';
 import 'package:simplecloudnotifier/components/modals/filter_modal_sendername.dart';
 import 'package:simplecloudnotifier/components/modals/filter_modal_time.dart';
 import 'package:simplecloudnotifier/state/app_bar_state.dart';
@@ -16,7 +17,9 @@ class AppBarFilterDialog extends StatefulWidget {
 class _AppBarFilterDialogState extends State<AppBarFilterDialog> {
   double _height = 0;
 
-  double _targetHeight = 4 + (48 * 6) + (16 * 5) + 4;
+  static const int _itemCount = 7;
+
+  static const double _targetHeight = 4 + (48 * _itemCount) + (16 * (_itemCount - 1)) + 4;
 
   @override
   void initState() {
@@ -117,6 +120,6 @@ class _AppBarFilterDialogState extends State<AppBarFilterDialog> {
   }
 
   void _showPlainSearchModal(BuildContext context) {
-    //TODO showDialog<void>(context: context, builder: (BuildContext context) => FilterModalSearchPlain());
+    showDialog<void>(context: context, builder: (BuildContext context) => FilterModalSearchPlain());
   }
 }
diff --git a/flutter/lib/components/modals/filter_modal_searchplain.dart b/flutter/lib/components/modals/filter_modal_searchplain.dart
new file mode 100644
index 0000000..28f60c6
--- /dev/null
+++ b/flutter/lib/components/modals/filter_modal_searchplain.dart
@@ -0,0 +1,60 @@
+import 'package:flutter/material.dart';
+import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
+import 'package:simplecloudnotifier/state/app_events.dart';
+
+class FilterModalSearchPlain extends StatefulWidget {
+  @override
+  _FilterModalSearchPlainState createState() => _FilterModalSearchPlainState();
+}
+
+class _FilterModalSearchPlainState extends State<FilterModalSearchPlain> {
+  final _controller = TextEditingController();
+
+  @override
+  void initState() {
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: const Text('Search'),
+      content: Container(
+        child: TextField(
+          autofocus: true,
+          controller: _controller,
+          decoration: InputDecoration(hintText: "Search..."),
+        ),
+      ),
+      actions: <Widget>[
+        TextButton(
+          style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
+          child: const Text('Apply'),
+          onPressed: _onOkay,
+        ),
+      ],
+    );
+  }
+
+  void _onOkay() {
+    Navigator.of(context).pop();
+
+    List<MessageFilterChiplet> chiplets = [];
+    if (_controller.text.isNotEmpty) {
+      chiplets.add(MessageFilterChiplet(
+        label: _controller.text,
+        value: _controller.text,
+        type: MessageFilterChipletType.plainSearch,
+      ));
+    }
+
+    AppEvents().notifyFilterListeners([MessageFilterChipletType.plainSearch], chiplets);
+  }
+}
diff --git a/flutter/lib/components/modals/filter_modal_sendername.dart b/flutter/lib/components/modals/filter_modal_sendername.dart
index 3cdfd7a..7a1c8c7 100644
--- a/flutter/lib/components/modals/filter_modal_sendername.dart
+++ b/flutter/lib/components/modals/filter_modal_sendername.dart
@@ -73,15 +73,13 @@ class _FilterModalSendernameState extends State<FilterModalSendername> {
         TextButton(
           style: TextButton.styleFrom(textStyle: Theme.of(context).textTheme.labelLarge),
           child: const Text('Apply'),
-          onPressed: () {
-            onOkay();
-          },
+          onPressed: _onOkay,
         ),
       ],
     );
   }
 
-  void onOkay() {
+  void _onOkay() {
     Navigator.of(context).pop();
 
     final chiplets = _selectedEntries
diff --git a/flutter/lib/pages/filtered_message_view/filtered_message_view.dart b/flutter/lib/pages/filtered_message_view/filtered_message_view.dart
new file mode 100644
index 0000000..5efeda4
--- /dev/null
+++ b/flutter/lib/pages/filtered_message_view/filtered_message_view.dart
@@ -0,0 +1,120 @@
+import 'package:flutter/material.dart';
+import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
+import 'package:simplecloudnotifier/api/api_client.dart';
+import 'package:simplecloudnotifier/components/layout/scaffold.dart';
+import 'package:simplecloudnotifier/models/channel.dart';
+import 'package:simplecloudnotifier/models/scn_message.dart';
+import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
+import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
+import 'package:simplecloudnotifier/settings/app_settings.dart';
+import 'package:simplecloudnotifier/state/app_auth.dart';
+import 'package:simplecloudnotifier/state/application_log.dart';
+import 'package:simplecloudnotifier/state/scn_data_cache.dart';
+import 'package:simplecloudnotifier/utils/navi.dart';
+import 'package:provider/provider.dart';
+
+class FilteredMessageViewPage extends StatefulWidget {
+  const FilteredMessageViewPage({
+    required this.title,
+    required this.filter,
+    super.key,
+  });
+
+  final String title;
+  final MessageFilter filter;
+
+  @override
+  State<FilteredMessageViewPage> createState() => _FilteredMessageViewPageState();
+}
+
+class _FilteredMessageViewPageState extends State<FilteredMessageViewPage> {
+  PagingController<String, SCNMessage> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
+
+  Map<String, Channel>? _channels = null;
+  bool _channelsFetched = false;
+
+  @override
+  void initState() {
+    super.initState();
+
+    _channels = SCNDataCache().getChannelMap();
+
+    _pagingController.addPageRequestListener(_fetchPage);
+
+    _pagingController.refresh();
+  }
+
+  @override
+  void dispose() {
+    _pagingController.dispose();
+    super.dispose();
+  }
+
+  Future<void> _fetchPage(String thisPageToken) async {
+    final acc = Provider.of<AppAuth>(context, listen: false);
+    final cfg = Provider.of<AppSettings>(context, listen: false);
+
+    ApplicationLog.debug('Start FilteredMessageViewPage::_pagingController::_fetchPage [ ${thisPageToken} ]');
+
+    if (!acc.isAuth()) {
+      _pagingController.error = 'Not logged in';
+      return;
+    }
+
+    try {
+      if (_channels == null || !_channelsFetched) {
+        final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
+        setState(() {
+          _channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
+          _channelsFetched = true;
+        });
+      }
+
+      final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize, filter: this.widget.filter);
+
+      SCNDataCache().addToMessageCache(newItems); // no await
+
+      if (npt == '@end') {
+        _pagingController.appendLastPage(newItems);
+      } else {
+        _pagingController.appendPage(newItems, npt);
+      }
+    } catch (exc, trace) {
+      _pagingController.error = exc.toString();
+      ApplicationLog.error('Failed to list channel-messages: ' + exc.toString(), trace: trace);
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SCNScaffold(
+      title: this.widget.title,
+      showSearch: false,
+      showShare: false,
+      child: _buildMessageList(context),
+    );
+  }
+
+  Widget _buildMessageList(BuildContext context) {
+    return Padding(
+      padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
+      child: RefreshIndicator(
+        onRefresh: () => Future.sync(
+          () => _pagingController.refresh(),
+        ),
+        child: PagedListView<String, SCNMessage>(
+          pagingController: _pagingController,
+          builderDelegate: PagedChildBuilderDelegate<SCNMessage>(
+            itemBuilder: (context, item, index) => MessageListItem(
+              message: item,
+              allChannels: _channels ?? {},
+              onPressed: () {
+                Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,)));
+              },
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/flutter/lib/pages/message_list/message_filter_chiplet.dart b/flutter/lib/pages/message_list/message_filter_chiplet.dart
index cfc8220..0d07501 100644
--- a/flutter/lib/pages/message_list/message_filter_chiplet.dart
+++ b/flutter/lib/pages/message_list/message_filter_chiplet.dart
@@ -3,6 +3,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 
 enum MessageFilterChipletType {
   search,
+  plainSearch,
   channel,
   sender,
   timeRange,
@@ -21,6 +22,8 @@ class MessageFilterChiplet {
     switch (type) {
       case MessageFilterChipletType.search:
         return FontAwesomeIcons.magnifyingGlass;
+      case MessageFilterChipletType.plainSearch:
+        return FontAwesomeIcons.magnifyingGlassPlus;
       case MessageFilterChipletType.channel:
         return FontAwesomeIcons.snake;
       case MessageFilterChipletType.sender:
diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart
index 2126611..84293ae 100644
--- a/flutter/lib/pages/message_list/message_list.dart
+++ b/flutter/lib/pages/message_list/message_list.dart
@@ -30,6 +30,7 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
   PagingController<String, SCNMessage> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
 
   Map<String, Channel>? _channels = null;
+  bool _channelsFetched = false;
 
   bool _isInitialized = false;
 
@@ -135,9 +136,12 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
     }
 
     try {
-      if (_channels == null) {
+      if (_channels == null || !_channelsFetched) {
         final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
-        _channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
+        setState(() {
+          _channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
+          _channelsFetched = true;
+        });
 
         SCNDataCache().setChannelCache(channels); // no await
       }
@@ -314,6 +318,11 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
       filter.searchFilter = chipletsSearch.map((p) => p.value as String).toList();
     }
 
+    var chipletsPlainSearch = _filterChiplets.where((p) => p.type == MessageFilterChipletType.plainSearch).toList();
+    if (chipletsPlainSearch.isNotEmpty) {
+      filter.plainSearchFilter = chipletsPlainSearch.map((p) => p.value as String).toList();
+    }
+
     var chipletsKeyTokens = _filterChiplets.where((p) => p.type == MessageFilterChipletType.sendkey).toList();
     if (chipletsKeyTokens.isNotEmpty) {
       filter.usedKeys = chipletsKeyTokens.map((p) => p.value as String).toList();
@@ -329,6 +338,13 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
       filter.senderNames = chipletSender.map((p) => p.value as String).toList();
     }
 
+    var chipletsTimeRange = _filterChiplets.where((p) => p.type == MessageFilterChipletType.timeRange).toList();
+    if (chipletsTimeRange.isNotEmpty) {
+      //TODO
+      //filter.timeAfter = chipletsTimeRange[0].value1 as DateTime;
+      //filter.timeBefore = chipletsTimeRange[0].value2 as DateTime;
+    }
+
     return filter;
   }
 }
diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart
index efe840a..f34624e 100644
--- a/flutter/lib/pages/message_view/message_view.dart
+++ b/flutter/lib/pages/message_view/message_view.dart
@@ -11,6 +11,7 @@ import 'package:simplecloudnotifier/models/keytoken.dart';
 import 'package:simplecloudnotifier/models/scn_message.dart';
 import 'package:simplecloudnotifier/models/user.dart';
 import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
+import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
 import 'package:simplecloudnotifier/state/app_auth.dart';
 import 'package:simplecloudnotifier/state/app_bar_state.dart';
 import 'package:simplecloudnotifier/utils/navi.dart';
@@ -152,7 +153,9 @@ class _MessageViewPageState extends State<MessageViewPage> {
                 icon: FontAwesomeIcons.solidSignature,
                 title: 'Sender',
                 values: [message.senderName!],
-                mainAction: () => {/*TODO*/},
+                mainAction: () => {
+                  Navi.push(context, () => FilteredMessageViewPage(title: message.senderName!, filter: MessageFilter(senderNames: [message.senderName!])))
+                },
               ),
             UI.metaCard(
               context: context,
diff --git a/flutter/lib/pages/sender_list/sender_list_item.dart b/flutter/lib/pages/sender_list/sender_list_item.dart
index 70b6c14..3e9df98 100644
--- a/flutter/lib/pages/sender_list/sender_list_item.dart
+++ b/flutter/lib/pages/sender_list/sender_list_item.dart
@@ -1,7 +1,9 @@
 import 'package:flutter/material.dart';
 import 'package:font_awesome_flutter/font_awesome_flutter.dart';
 import 'package:intl/intl.dart';
+import 'package:simplecloudnotifier/api/api_client.dart';
 import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
+import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
 import 'package:simplecloudnotifier/utils/navi.dart';
 
 enum SenderListItemMode {
@@ -27,8 +29,7 @@ class SenderListItem extends StatelessWidget {
       color: Theme.of(context).cardTheme.color,
       child: InkWell(
         onTap: () {
-          //TODO
-          Navi.popToRoot(context);
+          Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(senderNames: [item.name])));
         },
         child: Padding(
           padding: const EdgeInsets.all(8),
@@ -69,7 +70,7 @@ class SenderListItem extends StatelessWidget {
               SizedBox(width: 4),
               GestureDetector(
                 onTap: () {
-                  //TODO
+                  Navi.push(context, () => FilteredMessageViewPage(title: item.name, filter: MessageFilter(senderNames: [item.name])));
                 },
                 child: Padding(
                   padding: const EdgeInsets.all(8),