Repository: zulip/zulip-terminal

Issue:
Support a variety of footer notification styling
We can consider styling of footer notifications, eg. to distinguish errors/warnings from simple notifications like #781, and we could even add a positive one for "message sent" type notifications.

Discussion:
Ezio-Sarthak: @zulipbot add "feedback wanted"
Rohitth007: @Ezio-Sarthak Really nice addition to ZT.
Why `area:neutral` though? `footer` should make sense as there is also a `header`, for the default color.
Also in terms of readability, I feel someone looking at `themes.py`for the very first time would instantly make sense of what a `header` is and what a `footer` is than `area:neutral`.
Just a suggestion :) 
Ezio-Sarthak: @Rohitth007 thanks for the feedback. Re `area: neutral`, it's true that it wouldn't be readable for footer styling sense, though my intent was that this could be used as a general style elsewhere in the UI in future, if that makes sense?
Rohitth007: I'm assuming the footer would have an independent styling as acts sort of like a notification bar?
Also, I think applying same setting to different components reduces independent customizability.

Patch:
diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py
index b6fdb13..2c8dafa 100644
--- a/zulipterminal/config/themes.py
+++ b/zulipterminal/config/themes.py
@@ -36,6 +36,7 @@ required_styles = {  # style-name: monochrome-bit-depth-style
     "msg_bold": "bold",
     "msg_time": "bold",
     "footer": "standout",
+    "footer_contrast": "standout",
     "starred": "bold",
     "popup_category": "bold",
     "unread_count": "bold",
@@ -51,6 +52,9 @@ required_styles = {  # style-name: monochrome-bit-depth-style
     "area:stream": "standout",
     "area:error": "standout",
     "search_error": "standout",
+    "task:success": "standout",
+    "task:error": "standout",
+    "task:warning": "standout",
 }
 
 
@@ -175,8 +179,10 @@ THEMES: Dict[str, ThemeSpec] = {
          None,             DEF['white:bold'],         DEF['black']),
         ('msg_time',       'black',                   'white',
          None,             DEF['black'],              DEF['white']),
-        ('footer',         'white',                   'dark red',
-         None,             DEF['white'],              DEF['dark_red']),
+        ('footer',         'black',                   'light gray',
+         None,             DEF['black'],              DEF['light_gray']),
+        ('footer_contrast', 'white',                  'black',
+         None,             DEF['white'],              DEF['black']),
         ('starred',        'light red, bold',         'black',
          None,             DEF['light_red:bold'],     DEF['black']),
         ('popup_category', 'light blue, bold',        'black',
@@ -213,6 +219,12 @@ THEMES: Dict[str, ThemeSpec] = {
          None,             DEF['white'],               DEF['dark_red']),
         ('search_error',   'light red',               'black',
          None,             DEF['light_red'],           DEF['black']),
+        ('task:success',   'white',                   'dark green',
+         None,             DEF['white'],              DEF['dark_green']),
+        ('task:error',     'white',                   'dark red',
+         None,             DEF['white'],              DEF['dark_red']),
+        ('task:warning',   'white',                   'brown',
+         None,             DEF['white'],              DEF['brown']),
     ],
     'gruvbox_dark': [
         # default colorscheme on 16 colors, gruvbox colorscheme
@@ -271,8 +283,10 @@ THEMES: Dict[str, ThemeSpec] = {
          None,             WHITEBOLD,         BLACK),
         ('msg_time',       'black',           'white',
          None,             BLACK,             WHITE),
-        ('footer',         'white',           'dark red',
-         None,             WHITE,             DARKRED),
+        ('footer',         'black',           'light gray',
+         None,             BLACK,             GRAY),
+        ('footer_contrast', 'white',          'black',
+         None,             WHITE,             BLACK),
         ('starred',        'light red, bold', 'black',
          None,             LIGHTREDBOLD,      BLACK),
         ('popup_category', 'light blue, bold', 'black',
@@ -309,6 +323,12 @@ THEMES: Dict[str, ThemeSpec] = {
          None,             WHITE,             DARKRED),
         ('search_error',   'light red',       'black',
          None,             LIGHTRED,          BLACK),
+        ('task:success',   'black',           'light green',
+         None,             BLACK,             LIGHTGREEN),
+        ('task:error',     'white',           'dark red',
+         None,             WHITE,             DARKRED),
+        ('task:warning',   'black',           'light red',
+         None,             BLACK,             LIGHTRED),
     ],
     'gruvbox_dark24': [
         # default colorscheme on 16 colors, gruvbox colorscheme
@@ -367,8 +387,10 @@ THEMES: Dict[str, ThemeSpec] = {
          None,             WHITEBOLD24,       BLACK24),
         ('msg_time',       'black',           'white',
          None,             BLACK24,           WHITE24),
-        ('footer',         'white',           'dark red',
-         None,             WHITE24,           DARKRED24),
+        ('footer',         'black',           'light gray',
+         None,             BLACK24,           GRAY24),
+        ('footer_contrast', 'white',          'black',
+         None,             WHITE24,           BLACK24),
         ('starred',        'light red, bold', 'black',
          None,             LIGHTREDBOLD24,    BLACK24),
         ('popup_category', 'light blue, bold', 'black',
@@ -405,6 +427,12 @@ THEMES: Dict[str, ThemeSpec] = {
          None,             WHITE24,           DARKRED24),
         ('search_error',   'light red',       'black',
          None,             LIGHTRED24,        BLACK24),
+        ('task:success',   'black',           'light green',
+         None,             BLACK24,           LIGHTGREEN24),
+        ('task:error',     'white',           'dark red',
+         None,             WHITE24,           DARKRED24),
+        ('task:warning',   'black',           'light red',
+         None,             BLACK24,           LIGHTRED24),
     ],
     'zt_light': [
         (None,             'black',           'white'),
@@ -434,7 +462,8 @@ THEMES: Dict[str, ThemeSpec] = {
         ('msg_code',       'black',           'light gray'),
         ('msg_bold',       'white, bold',     'dark gray'),
         ('msg_time',       'white',           'dark gray'),
-        ('footer',         'white',           'dark red'),
+        ('footer',         'white',           'dark gray'),
+        ('footer_contrast', 'black',          'white'),
         ('starred',        'light red, bold', 'white'),
         ('popup_category', 'dark gray, bold', 'light gray'),
         ('unread_count',   'dark blue, bold', 'white'),
@@ -453,6 +482,9 @@ THEMES: Dict[str, ThemeSpec] = {
         ('area:msg',       'black',           'yellow'),
         ('area:error',     'black',           'light red'),
         ('search_error',   'light red',       'white'),
+        ('task:success',   'black',           'dark green'),
+        ('task:error',     'white',           'dark red'),
+        ('task:warning',   'black',           'yellow'),
     ],
     'zt_blue': [
         (None,             'black',           'light blue'),
@@ -482,7 +514,8 @@ THEMES: Dict[str, ThemeSpec] = {
         ('msg_code',       'dark blue',       'white'),
         ('msg_bold',       'white, bold',     'dark blue'),
         ('msg_time',       'dark blue',       'white'),
-        ('footer',         'white',           'dark red'),
+        ('footer',         'white',           'dark gray'),
+        ('footer_contrast', 'black',          'white'),
         ('starred',        'light red, bold', 'light blue'),
         ('popup_category', 'light gray, bold', 'light blue'),
         ('unread_count',   'yellow',          'light blue'),
@@ -501,6 +534,9 @@ THEMES: Dict[str, ThemeSpec] = {
         ('area:msg',       'white',           'brown'),
         ('area:error',     'white',           'dark red'),
         ('search_error',   'light red',       'light blue'),
+        ('task:success',   'white',           'dark green'),
+        ('task:error',     'white',           'dark red'),
+        ('task:warning',   'white',           'brown'),
     ]
 }
 # fmt: on
diff --git a/zulipterminal/core.py b/zulipterminal/core.py
index 0537e72..c503aa4 100644
--- a/zulipterminal/core.py
+++ b/zulipterminal/core.py
@@ -313,10 +313,9 @@ class Controller:
         # Don't try to open web browser if running without a GUI
         # TODO: Explore and eventually support opening links in text-browsers.
         if LINUX and not os.environ.get("DISPLAY") and os.environ.get("TERM"):
-            self.view.set_footer_text(
+            self.report_error(
                 "No DISPLAY environment variable specified. This could "
-                "likely mean the ZT host is running without a GUI.",
-                3,
+                "likely mean the ZT host is running without a GUI."
             )
             return
         try:
@@ -326,13 +325,30 @@ class Controller:
             # Suppress stdout and stderr when opening browser
             with suppress_output():
                 browser_controller.open(url)
-                self.view.set_footer_text(
-                    f"The link was successfully opened using {browser_controller.name}",
-                    3,
+                self.report_success(
+                    f"The link was successfully opened using {browser_controller.name}"
                 )
         except webbrowser.Error as e:
             # Set a footer text if no runnable browser is located
-            self.view.set_footer_text(f"ERROR: {e}", 3)
+            self.report_error(f"ERROR: {e}")
+
+    def report_error(self, text: str) -> None:
+        """
+        Helper to show an error message in footer
+        """
+        self.view.set_footer_text(text, "task:error", 3)
+
+    def report_success(self, text: str) -> None:
+        """
+        Helper to show a success message in footer
+        """
+        self.view.set_footer_text(text, "task:success", 3)
+
+    def report_warning(self, text: str) -> None:
+        """
+        Helper to show a warning message in footer
+        """
+        self.view.set_footer_text(text, "task:warning", 3)
 
     def search_messages(self, text: str) -> None:
         # Search for a text in messages
diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py
index f9a973d..8271b36 100644
--- a/zulipterminal/helper.py
+++ b/zulipterminal/helper.py
@@ -661,7 +661,7 @@ def notify(title: str, html_text: str) -> str:
 
 def display_error_if_present(response: Dict[str, Any], controller: Any) -> None:
     if response["result"] == "error" and hasattr(controller, "view"):
-        controller.view.set_footer_text(response["msg"], 3)
+        controller.report_error(response["msg"])
 
 
 def check_narrow_and_notify(
@@ -674,7 +674,7 @@ def check_narrow_and_notify(
         and current_narrow != outer_narrow
         and current_narrow != inner_narrow
     ):
-        controller.view.set_footer_text("Message is sent outside of current narrow.", 3)
+        controller.report_success("Message is sent outside of current narrow.")
 
 
 def notify_if_message_sent_outside_narrow(
diff --git a/zulipterminal/model.py b/zulipterminal/model.py
index ed7d6b2..e6cde29 100644
--- a/zulipterminal/model.py
+++ b/zulipterminal/model.py
@@ -376,7 +376,7 @@ class Model:
 
     def save_draft(self, draft: Composition) -> None:
         self._draft = deepcopy(draft)
-        self.controller.view.set_footer_text("Saved message as draft", 3)
+        self.controller.report_success("Saved message as draft")
 
     @asynch
     def toggle_message_star_status(self, message: Message) -> None:
@@ -471,9 +471,8 @@ class Model:
         if response["result"] == "success":
             old_topic = self.index["messages"][message_id].get("subject", None)
             new_topic = request["topic"]
-            view = self.controller.view
             if old_topic != new_topic:
-                view.set_footer_text("You changed a message's topic.", 3)
+                self.controller.report_success("You changed a message's topic.")
 
         return response["result"] == "success"
 
@@ -1010,7 +1009,7 @@ class Model:
                 if event["op"] == "start":
                     user = self.user_dict[event["sender"]["email"]]
                     self.controller.view.set_footer_text(
-                        [" ", ("code", user["full_name"]), " is typing..."]
+                        [" ", ("footer_contrast", user["full_name"]), " is typing..."]
                     )
                 elif event["op"] == "stop":
                     self.controller.view.set_footer_text()
diff --git a/zulipterminal/ui.py b/zulipterminal/ui.py
index 7fa1132..c299aea 100644
--- a/zulipterminal/ui.py
+++ b/zulipterminal/ui.py
@@ -83,19 +83,23 @@ class View(urwid.WidgetWrap):
         random_command = random.choice(allowed_commands)
         return [
             "Help(?): ",
-            ("code", " " + ", ".join(random_command["keys"]) + " "),
+            ("footer_contrast", " " + ", ".join(random_command["keys"]) + " "),
             " " + random_command["help_text"],
         ]
 
     @asynch
     def set_footer_text(
-        self, text_list: Optional[List[Any]] = None, duration: Optional[float] = None
+        self,
+        text_list: Optional[List[Any]] = None,
+        style: str = "footer",
+        duration: Optional[float] = None,
     ) -> None:
         if text_list is None:
             text = self.get_random_help()
         else:
             text = text_list
         self._w.footer.set_text(text)
+        self._w.footer.set_attr_map({None: style})
         self.controller.update_screen()
         if duration is not None:
             assert duration > 0
@@ -110,7 +114,7 @@ class View(urwid.WidgetWrap):
             # Wrap by space.
             footer_text: List[Any] = [" " + s + " " for s in suggestions]
             if state is not None:
-                footer_text[state] = ("code", footer_text[state])
+                footer_text[state] = ("footer_contrast", footer_text[state])
             if is_truncated:
                 footer_text += [" [more] "]
             footer_text.insert(0, [" "])  # Add leading space.
@@ -265,7 +269,9 @@ class View(urwid.WidgetWrap):
                 self.body.focus_col = 1
                 self.middle_column.set_focus("footer")
             else:
-                self.set_footer_text("No draft message was saved in this session.", 3)
+                self.controller.report_error(
+                    "No draft message was saved in this session."
+                )
             return key
         elif is_command_key("ABOUT", key):
             self.controller.show_about()
diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py
index fab9cf9..371127b 100644
--- a/zulipterminal/ui_tools/boxes.py
+++ b/zulipterminal/ui_tools/boxes.py
@@ -568,8 +568,8 @@ class WriteBox(urwid.Pile):
                             content=self.msg_write_box.edit_text,
                         )
                     else:
-                        self.view.set_footer_text(
-                            "Cannot send message without specifying recipients.", 3
+                        self.view.controller.report_error(
+                            "Cannot send message without specifying recipients."
                         )
                         success = None
             if success:
@@ -624,7 +624,7 @@ class WriteBox(urwid.Pile):
                                     primary_key_for_command("AUTOCOMPLETE_REVERSE"),
                                 )
                             )
-                            self.view.set_footer_text(invalid_stream_error, 3)
+                            self.view.controller.report_error(invalid_stream_error)
                             return key
                         user_ids = self.model.get_other_subscribers_in_stream(
                             stream_name=stream_name
@@ -658,7 +658,7 @@ class WriteBox(urwid.Pile):
                         invalid_emails_error = (
                             f"Invalid recipient(s) - {', '.join(invalid_emails)}"
                         )
-                        self.view.set_footer_text(invalid_emails_error, 3)
+                        self.view.controller.report_error(invalid_emails_error)
                         return key
                     users = self.model.user_dict
                     self.recipient_user_ids = [
@@ -1441,9 +1441,10 @@ class MessageBox(urwid.Pile):
             self.model.controller.view.set_footer_text(
                 [
                     "Try pressing ",
-                    ("code", f" {selection_key} "),
+                    ("footer_contrast", f" {selection_key} "),
                     " and dragging to select text.",
-                ]
+                ],
+                "task:warning",
             )
             self.displaying_selection_hint = True
         elif event == "mouse release" and self.displaying_selection_hint:
@@ -1569,15 +1570,13 @@ class MessageBox(urwid.Pile):
             self.model.controller.view.middle_column.set_focus("footer")
         elif is_command_key("EDIT_MESSAGE", key):
             if self.message["sender_id"] != self.model.user_id:
-                self.model.controller.view.set_footer_text(
-                    " You can't edit messages sent by other users.", 3
+                self.model.controller.report_error(
+                    " You can't edit messages sent by other users."
                 )
                 return key
             # Check if editing is allowed in the realm
             elif not self.model.initial_data["realm_allow_message_editing"]:
-                self.model.controller.view.set_footer_text(
-                    " Editing sent message is disabled.", 3
-                )
+                self.model.controller.report_error(" Editing sent message is disabled.")
                 return key
             # Check if message is still editable, i.e. within
             # the time limit. A limit of 0 signifies no limit
@@ -1590,16 +1589,15 @@ class MessageBox(urwid.Pile):
                 ]
                 if time_since_msg_sent >= edit_time_limit:
                     if self.message["type"] == "private":
-                        self.model.controller.view.set_footer_text(
-                            " Time Limit for editing the message has been exceeded.", 3
+                        self.model.controller.report_error(
+                            " Time Limit for editing the message has been exceeded."
                         )
                         return key
                     elif self.message["type"] == "stream":
-                        self.model.controller.view.set_footer_text(
+                        self.model.controller.report_warning(
                             " Only topic editing allowed."
                             " Time Limit for editing the message body has"
                             " been exceeded.",
-                            3,
                         )
                         msg_body_edit_enabled = False
 
diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py
index c104706..201d387 100644
--- a/zulipterminal/ui_tools/buttons.py
+++ b/zulipterminal/ui_tools/buttons.py
@@ -543,7 +543,7 @@ class MessageLinkButton(urwid.Button):
         error = self._validate_narrow_link(parsed_link)
 
         if error:
-            self.view.set_footer_text(f" {error}", duration=3)
+            self.controller.report_error(f" {error}")
         else:
             self._switch_narrow_to(parsed_link)
 


Test patch:
diff --git a/tests/core/test_core.py b/tests/core/test_core.py
index 92d9a07..9056ce8 100644
--- a/tests/core/test_core.py
+++ b/tests/core/test_core.py
@@ -278,26 +278,26 @@ class TestController:
     def test_open_in_browser_success(self, mocker, controller, url):
         # Set DISPLAY environ to be able to run test in CI
         os.environ["DISPLAY"] = ":0"
-        controller.view.set_footer_text = mocker.Mock()
+        controller.report_success = mocker.Mock()
         mock_get = mocker.patch(CORE + ".webbrowser.get")
         mock_open = mock_get.return_value.open
 
         controller.open_in_browser(url)
 
         mock_open.assert_called_once_with(url)
-        controller.view.set_footer_text.assert_called_once_with(
-            f"The link was successfully opened using {mock_get.return_value.name}", 3
+        controller.report_success.assert_called_once_with(
+            f"The link was successfully opened using {mock_get.return_value.name}"
         )
 
     def test_open_in_browser_fail__no_browser_controller(self, mocker, controller):
         os.environ["DISPLAY"] = ":0"
         error = "No runnable browser found"
-        controller.view.set_footer_text = mocker.Mock()
+        controller.report_error = mocker.Mock()
         mocker.patch(CORE + ".webbrowser.get").side_effect = webbrowser.Error(error)
 
         controller.open_in_browser("https://chat.zulip.org/#narrow/stream/test")
 
-        controller.view.set_footer_text.assert_called_once_with(f"ERROR: {error}", 3)
+        controller.report_error.assert_called_once_with(f"ERROR: {error}")
 
     def test_main(self, mocker, controller):
         controller.view.palette = {"default": "theme_properties"}
diff --git a/tests/helper/test_helper.py b/tests/helper/test_helper.py
index ee1d8f0..fb53ab1 100644
--- a/tests/helper/test_helper.py
+++ b/tests/helper/test_helper.py
@@ -343,14 +343,14 @@ def test_notify_quotes(monkeypatch, mocker, OS, cmd_length, title, text):
 )
 def test_display_error_if_present(mocker, response, footer_updated):
     controller = mocker.Mock()
-    set_footer_text = controller.view.set_footer_text
+    report_error = controller.report_error
 
     display_error_if_present(response, controller)
 
     if footer_updated:
-        set_footer_text.assert_called_once_with(response["msg"], 3)
+        report_error.assert_called_once_with(response["msg"])
     else:
-        set_footer_text.assert_not_called()
+        report_error.assert_not_called()
 
 
 @pytest.mark.parametrize(
@@ -424,17 +424,17 @@ def test_display_error_if_present(mocker, response, footer_updated):
 )
 def test_notify_if_message_sent_outside_narrow(mocker, req, narrow, footer_updated):
     controller = mocker.Mock()
-    set_footer_text = controller.view.set_footer_text
+    report_success = controller.report_success
     controller.model.narrow = narrow
 
     notify_if_message_sent_outside_narrow(req, controller)
 
     if footer_updated:
-        set_footer_text.assert_called_once_with(
-            "Message is sent outside of current narrow.", 3
+        report_success.assert_called_once_with(
+            "Message is sent outside of current narrow."
         )
     else:
-        set_footer_text.assert_not_called()
+        report_success.assert_not_called()
 
 
 @pytest.mark.parametrize(
diff --git a/tests/model/test_model.py b/tests/model/test_model.py
index 304871b..02be6ce 100644
--- a/tests/model/test_model.py
+++ b/tests/model/test_model.py
@@ -667,11 +667,11 @@ class TestModel:
         self.client.update_message.assert_called_once_with(req)
         assert result == return_value
         self.display_error_if_present.assert_called_once_with(response, self.controller)
-        set_footer_text = model.controller.view.set_footer_text
+        report_success = model.controller.report_success
         if result and footer_updated:
-            set_footer_text.assert_called_once_with("You changed a message's topic.", 3)
+            report_success.assert_called_once_with("You changed a message's topic.")
         else:
-            set_footer_text.assert_not_called()
+            report_success.assert_not_called()
 
     # NOTE: This tests only getting next-unread, not a fixed anchor
     def test_success_get_messages(
diff --git a/tests/ui/test_ui.py b/tests/ui/test_ui.py
index 16fb1db..90b7c6b 100644
--- a/tests/ui/test_ui.py
+++ b/tests/ui/test_ui.py
@@ -80,7 +80,7 @@ class TestView:
         )
         mock_sleep = mocker.patch("time.sleep")
 
-        view.set_footer_text([custom_text], duration)
+        view.set_footer_text([custom_text], duration=duration)
 
         view._w.footer.set_text.assert_has_calls(
             [mocker.call([custom_text]), mocker.call(["some help text"])]
@@ -94,14 +94,24 @@ class TestView:
             ([], None, False, [" [No matches found]"]),
             (["some", "text"], None, False, [[" "], " some ", " text "]),
             (["some", "text"], None, True, [[" "], " some ", " text ", " [more] "]),
-            (["some", "text"], 0, False, [[" "], ("code", " some "), " text "]),
+            (
+                ["some", "text"],
+                0,
+                False,
+                [[" "], ("footer_contrast", " some "), " text "],
+            ),
             (
                 ["some", "text"],
                 0,
                 True,
-                [[" "], ("code", " some "), " text ", " [more] "],
+                [[" "], ("footer_contrast", " some "), " text ", " [more] "],
+            ),
+            (
+                ["some", "text"],
+                -1,
+                False,
+                [[" "], " some ", ("footer_contrast", " text ")],
             ),
-            (["some", "text"], -1, False, [[" "], " some ", ("code", " text ")]),
         ],
         ids=[
             "no_matches",
@@ -346,7 +356,7 @@ class TestView:
     def test_keypress_OPEN_DRAFT(self, view, mocker, draft, key, widget_size):
         view.body = mocker.Mock()
         view.middle_column = mocker.Mock()
-        view.set_footer_text = mocker.Mock()
+        view.controller.report_error = mocker.Mock()
         view.controller.is_in_editor_mode = lambda: False
         view.model.stream_id_from_name.return_value = 10
         view.model.session_draft_message.return_value = draft
@@ -373,8 +383,8 @@ class TestView:
             assert view.write_box.msg_write_box.edit_pos == len(draft["content"])
             view.middle_column.set_focus.assert_called_once_with("footer")
         else:
-            view.set_footer_text.assert_called_once_with(
-                "No draft message was saved in this session.", 3
+            view.controller.report_error.assert_called_once_with(
+                "No draft message was saved in this session."
             )
 
     @pytest.mark.parametrize("key", keys_for_command("SEARCH_PEOPLE"))
diff --git a/tests/ui_tools/test_buttons.py b/tests/ui_tools/test_buttons.py
index a79493d..0c9df78 100644
--- a/tests/ui_tools/test_buttons.py
+++ b/tests/ui_tools/test_buttons.py
@@ -853,7 +853,7 @@ class TestMessageLinkButton:
         assert mocked_button.controller.narrow_to_topic.called == narrow_to_topic_called
 
     @pytest.mark.parametrize(
-        "error, set_footer_text_called, _switch_narrow_to_called, exit_popup_called",
+        "error, report_error_called, _switch_narrow_to_called, exit_popup_called",
         [
             ("Some Validation Error", True, False, False),
             ("", False, True, True),
@@ -867,7 +867,7 @@ class TestMessageLinkButton:
         self,
         mocker,
         error,
-        set_footer_text_called,
+        report_error_called,
         _switch_narrow_to_called,
         exit_popup_called,
     ):
@@ -883,6 +883,6 @@ class TestMessageLinkButton:
 
         assert mocked_button._parse_narrow_link.called
         assert mocked_button._validate_narrow_link.called
-        assert mocked_button.view.set_footer_text.called == set_footer_text_called
+        assert mocked_button.controller.report_error.called == report_error_called
         assert mocked_button._switch_narrow_to.called == _switch_narrow_to_called
         assert mocked_button.controller.exit_popup.called == exit_popup_called