Skip to content

Add support for (un)resolving topics in ZT. #1235

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions docs/hotkeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
|Redraw screen|<kbd>ctrl</kbd> + <kbd>l</kbd>|
|Quit|<kbd>ctrl</kbd> + <kbd>c</kbd>|
|View user information (From Users list)|<kbd>i</kbd>|
|Show/hide topic information & modify settings|<kbd>i</kbd>|

## Navigation
|Command|Key Combination|
Expand Down
126 changes: 98 additions & 28 deletions tests/model/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pytest import param as case
from zulip import Client, ZulipError

from zulipterminal.api_types import RESOLVED_TOPIC_PREFIX
from zulipterminal.config.symbols import STREAM_TOPIC_SEPARATOR
from zulipterminal.helper import initial_index, powerset
from zulipterminal.model import (
Expand Down Expand Up @@ -1097,11 +1098,11 @@ def test_get_latest_message_in_topic(
},
{
"_": ["User not found", False],
"_owner": ["Editing messages is disabled", False],
"_admin": ["Editing messages is disabled", False],
"_moderator": ["Editing messages is disabled", False],
"_member": ["Editing messages is disabled", False],
"_guest": ["Editing messages is disabled", False],
"_owner": [" Editing messages is disabled", False],
"_admin": [" Editing messages is disabled", False],
"_moderator": [" Editing messages is disabled", False],
"_member": [" Editing messages is disabled", False],
"_guest": [" Editing messages is disabled", False],
},
id="editing_msg_disabled:PreZulip4.0",
),
Expand All @@ -1114,9 +1115,9 @@ def test_get_latest_message_in_topic(
"_": ["User not found", False],
"_owner": ["", True],
"_admin": ["", True],
"_moderator": ["Editing topic is disabled", False],
"_member": ["Editing topic is disabled", False],
"_guest": ["Editing topic is disabled", False],
"_moderator": [" Editing topic is disabled", False],
"_member": [" Editing topic is disabled", False],
"_guest": [" Editing topic is disabled", False],
},
id="editing_topic_disabled:PreZulip4.0",
),
Expand All @@ -1135,18 +1136,6 @@ def test_get_latest_message_in_topic(
},
id="editing_topic_and_msg_enabled:PreZulip4.0",
),
case(
{"allow_message_editing": False, "edit_topic_policy": 1},
{
Comment on lines -1138 to -1140
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this test case being removed?

Generally I think the changes in this revised first commit are otherwise minor enough that we can drop them? The rest appear to be:

  • reformatting (strings)
  • errors prefixing with space
  • comment removed

In any case, if we do include parts in some way, since this would be a modification of the previous function, the commit title would need adjusting.

"_": ["User not found", False],
"_owner": ["Editing messages is disabled", False],
"_admin": ["Editing messages is disabled", False],
"_moderator": ["Editing messages is disabled", False],
"_member": ["Editing messages is disabled", False],
"_guest": ["Editing messages is disabled", False],
},
id="all_but_no_editing:Zulip4.0+:ZFL75",
),
case(
{"allow_message_editing": True, "edit_topic_policy": 1},
{
Expand All @@ -1156,8 +1145,8 @@ def test_get_latest_message_in_topic(
"_moderator": ["", True],
"_member": ["", True],
"_guest": [
"Only organization administrators, moderators, full members and"
" members can edit topic",
"Only organization administrators, moderators, full members"
" and members can edit topic",
False,
],
},
Expand Down Expand Up @@ -1208,13 +1197,13 @@ def test_get_latest_message_in_topic(
"_admin": ["", True],
"_moderator": ["", True],
"_member": [
"Only organization administrators and moderators"
" can edit topic",
"Only organization administrators and moderators can edit"
" topic",
False,
],
"_guest": [
"Only organization administrators and moderators"
" can edit topic",
"Only organization administrators and moderators can edit"
" topic",
False,
],
},
Expand Down Expand Up @@ -1246,9 +1235,9 @@ def test_can_user_edit_topic(
):
model.get_user_info = mocker.Mock(return_value=user_role)
model.initial_data = initial_data
initial_data["realm_allow_message_editing"] = realm_editing_settings[
initial_data["realm_allow_message_editing"] = realm_editing_settings.get(
"allow_message_editing"
]
)
allow_community_topic_editing = realm_editing_settings.get(
"allow_community_topic_editing", None
)
Expand All @@ -1271,6 +1260,87 @@ def test_can_user_edit_topic(
else:
report_error.assert_called_once_with(expected_response[user_type][0])

@pytest.mark.parametrize(
"topic_name, msg_response, server_feature_level, topic_editing_limit_seconds,"
" expected_new_topic_name, expected_footer_error",
[
case(
"hi!",
{
"subject": "hi!",
"timestamp": 11662271397,
"id": 1,
},
Comment on lines +1269 to +1273
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dict appears identical in each case, except for the time, so let's build it in the function to keep it simpler? You could test for the None return value case, but that may be simpler in a separate test.

12,
259200,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the timestamp within this time difference? The test case doesn't make it clear that's why we expect it to resolve.

RESOLVED_TOPIC_PREFIX + "hi!",
None,
id="topic_resolved:Zulip2.1+:ZFL12",
),
case(
"hi!",
{
"subject": "hi!",
"timestamp": 0,
"id": 1,
},
None,
None,
RESOLVED_TOPIC_PREFIX + "hi!",
None,
id="no_time_limit:Zulip2.1+:None",
),
case(
RESOLVED_TOPIC_PREFIX + "hi!",
{
"subject": RESOLVED_TOPIC_PREFIX + "hi!",
"timestamp": 11662271397,
"id": 1,
},
10,
86400,
"hi!",
None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value is always None? Have we lost some test cases?

id="topic_unresolved:Zulip2.1+:ZFL10",
),
],
)
def test_toggle_topic_resolve_status(
self,
mocker,
model,
initial_data,
topic_name,
msg_response,
server_feature_level,
topic_editing_limit_seconds,
expected_new_topic_name,
expected_footer_error,
stream_id=1,
):
model.initial_data = initial_data
model.server_feature_level = server_feature_level
initial_data[
"realm_community_topic_editing_limit_seconds"
] = topic_editing_limit_seconds
# If user can't edit topic, topic (un)resolve is disabled. Therefore,
# default return_value=True
model.can_user_edit_topic = mocker.Mock(return_value=True)
model.get_latest_message_in_topic = mocker.Mock(return_value=msg_response)
model.update_stream_message = mocker.Mock(return_value={"result": "success"})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #1235 (comment)

This does not need a return value.

report_error = model.controller.report_error

model.toggle_topic_resolve_status(stream_id, topic_name)

if not expected_footer_error:
model.update_stream_message.assert_called_once_with(
message_id=msg_response["id"],
topic=expected_new_topic_name,
propagate_mode="change_all",
)
else:
report_error.assert_called_once_with(expected_footer_error)

# NOTE: This tests only getting next-unread, not a fixed anchor
def test_success_get_messages(
self,
Expand Down
19 changes: 19 additions & 0 deletions tests/ui_tools/test_popups.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
PopUpView,
StreamInfoView,
StreamMembersView,
TopicInfoView,
UserInfoView,
)
from zulipterminal.urwid_types import urwid_Size
Expand Down Expand Up @@ -1154,6 +1155,24 @@ def test_create_link_buttons(
assert link_width == expected_link_width


class TestTopicInfoView:
@pytest.fixture(autouse=True)
def mock_external_classes(
self, mocker: MockerFixture, general_stream: Dict[str, Any], topics: List[str]
) -> None:
self.controller = mocker.Mock()
mocker.patch.object(
self.controller, "maximum_popup_dimensions", return_value=(64, 64)
)
mocker.patch(LISTWALKER, return_value=[])
self.stream_id = general_stream["stream_id"]
self.topic = topics[0]

self.topic_info_view = TopicInfoView(
self.controller, self.stream_id, self.topic
)


class TestStreamInfoView:
@pytest.fixture(autouse=True)
def mock_external_classes(
Expand Down
2 changes: 1 addition & 1 deletion tools/lint-hotkeys
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ SCRIPT_NAME = PurePath(__file__).name
HELP_TEXT_STYLE = re.compile(r"^[a-zA-Z /()',&@#:_-]*$")

# Exclude keys from duplicate keys checking
KEYS_TO_EXCLUDE = ["q", "e", "m", "r"]
KEYS_TO_EXCLUDE = ["q", "e", "m", "r", "i"]


def main(fix: bool) -> None:
Expand Down
5 changes: 5 additions & 0 deletions zulipterminal/config/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@ class KeyBinding(TypedDict):
'help_text': 'View user information (From Users list)',
'key_category': 'general',
},
'TOPIC_INFO': {
'keys': ['i'],
'help_text': 'Show/hide topic information & modify settings',
'key_category': 'general',
},
'BEGINNING_OF_LINE': {
'keys': ['ctrl a'],
'help_text': 'Jump to the beginning of line',
Expand Down
1 change: 1 addition & 0 deletions zulipterminal/config/themes.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
'area:help' : 'standout',
'area:msg' : 'standout',
'area:stream' : 'standout',
'area:topic' : 'standout',
'area:error' : 'standout',
'area:user' : 'standout',
'search_error' : 'standout',
Expand Down
5 changes: 5 additions & 0 deletions zulipterminal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
PopUpConfirmationView,
StreamInfoView,
StreamMembersView,
TopicInfoView,
UserInfoView,
)
from zulipterminal.version import ZT_VERSION
Expand Down Expand Up @@ -305,6 +306,10 @@ def show_stream_info(self, stream_id: int) -> None:
show_stream_view = StreamInfoView(self, stream_id)
self.show_pop_up(show_stream_view, "area:stream")

def show_topic_info(self, stream_id: int, topic_name: str) -> None:
show_topic_view = TopicInfoView(self, stream_id, topic_name)
self.show_pop_up(show_topic_view, "area:topic")

def show_stream_members(self, stream_id: int) -> None:
stream_members_view = StreamMembersView(self, stream_id)
self.show_pop_up(stream_members_view, "area:stream")
Expand Down
39 changes: 36 additions & 3 deletions zulipterminal/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

from zulipterminal import unicode_emojis
from zulipterminal.api_types import (
RESOLVED_TOPIC_PREFIX,
Composition,
EditPropagateMode,
Event,
Expand Down Expand Up @@ -648,7 +649,7 @@ def can_user_edit_topic(self) -> bool:
user_info = self.get_user_info(self.user_id)
if user_info is not None:
if not self.initial_data.get("realm_allow_message_editing"):
self.controller.report_error("Editing messages is disabled")
self.controller.report_error(" Editing messages is disabled")
return False
role = user_info["role"]
if role <= 200:
Expand All @@ -661,7 +662,7 @@ def can_user_edit_topic(self) -> bool:
if allow_community_topic_editing is True:
return True
elif allow_community_topic_editing is False:
self.controller.report_error("Editing topic is disabled")
self.controller.report_error(" Editing topic is disabled")
return False
else:
edit_topic_policy = self.initial_data.get("realm_edit_topic_policy")
Expand Down Expand Up @@ -693,12 +694,44 @@ def can_user_edit_topic(self) -> bool:
else:
self.controller.report_error(EDIT_TOPIC_POLICY[1])
return False
else: # edit_topic_policy == 5 (or None)
else:
# All users including guests
return True
self.controller.report_error("User not found")
return False

def toggle_topic_resolve_status(self, stream_id: int, topic_name: str) -> None:
if self.can_user_edit_topic():
latest_msg = self.get_latest_message_in_topic(stream_id, topic_name)
if latest_msg:
time_since_msg_sent = time.time() - latest_msg["timestamp"]
# ZFL < 11, community_topic_editing_limit_seconds
# was hardcoded as int value in secs eg. 86400s (1 day) or None
if self.server_feature_level is None or self.server_feature_level >= 11:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #1235 (comment)

For server 2.1, this will set edit_time_limit to None.
For server 3.0 (or ZFL 11 at least), this will set edit_time_limit to the value in the server (None or the value).
For servers between those (eg. ZFL 10), this will set edit_limit to the default, ie. 86400 - this is an edge case

My impression is that the expected server behavior was

  • old 86400 <-- 2.1
  • <11 86400 <-- edge case
  • >=11 86400 or None (per server value) <-- 3.0+

This appears to generate

  • old None
  • <11 86400
  • >=11 86400 or None (per server value)

Is this going to fail for 2.1? If so, could you update the test to fail first?

edit_time_limit = self.initial_data.get(
"realm_community_topic_editing_limit_seconds", None
)
else:
edit_time_limit = 86400
# Don't allow editing topic if time-limit exceeded.
if (
edit_time_limit is not None
and time_since_msg_sent >= edit_time_limit
):
self.controller.report_error(
" Time limit for editing topic has been exceeded."
)
else:
if topic_name.startswith(RESOLVED_TOPIC_PREFIX):
topic_name = topic_name[2:]
else:
topic_name = RESOLVED_TOPIC_PREFIX + topic_name
self.update_stream_message(
message_id=latest_msg["id"],
topic=topic_name,
propagate_mode="change_all",
)

def generate_all_emoji_data(
self, custom_emoji: Dict[str, RealmEmojiData]
) -> Tuple[NamedEmojiData, List[str]]:
Expand Down
1 change: 1 addition & 0 deletions zulipterminal/themes/gruvbox_dark.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
'area:help' : (Color.DARK0_HARD, Color.BRIGHT_GREEN),
'area:msg' : (Color.DARK0_HARD, Color.NEUTRAL_PURPLE),
'area:stream' : (Color.DARK0_HARD, Color.BRIGHT_BLUE),
'area:topic' : (Color.DARK0_HARD, Color.BRIGHT_BLUE),
'area:error' : (Color.DARK0_HARD, Color.BRIGHT_RED),
'area:user' : (Color.DARK0_HARD, Color.BRIGHT_YELLOW),
'search_error' : (Color.BRIGHT_RED, Color.DARK0_HARD),
Expand Down
1 change: 1 addition & 0 deletions zulipterminal/themes/gruvbox_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
'area:help' : (Color.LIGHT0_HARD, Color.FADED_GREEN),
'area:msg' : (Color.LIGHT0_HARD, Color.NEUTRAL_PURPLE),
'area:stream' : (Color.LIGHT0_HARD, Color.FADED_BLUE),
'area:topic' : (Color.LIGHT0_HARD, Color.FADED_BLUE),
'area:error' : (Color.LIGHT0_HARD, Color.FADED_RED),
'area:user' : (Color.LIGHT0_HARD, Color.FADED_YELLOW),
'search_error' : (Color.FADED_RED, Color.LIGHT0_HARD),
Expand Down
1 change: 1 addition & 0 deletions zulipterminal/themes/zt_blue.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
'widget_disabled' : (Color.DARK_GRAY, Color.LIGHT_BLUE),
'area:help' : (Color.WHITE, Color.DARK_GREEN),
'area:stream' : (Color.WHITE, Color.DARK_CYAN),
'area:topic' : (Color.WHITE, Color.DARK_CYAN),
'area:msg' : (Color.WHITE, Color.BROWN),
'area:error' : (Color.WHITE, Color.DARK_RED),
'area:user' : (Color.WHITE, Color.DARK_BLUE),
Expand Down
1 change: 1 addition & 0 deletions zulipterminal/themes/zt_dark.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
'area:help' : (Color.WHITE, Color.DARK_GREEN),
'area:msg' : (Color.WHITE, Color.BROWN),
'area:stream' : (Color.WHITE, Color.DARK_CYAN),
'area:topic' : (Color.WHITE, Color.DARK_CYAN),
'area:error' : (Color.WHITE, Color.DARK_RED),
'area:user' : (Color.WHITE, Color.DARK_BLUE),
'search_error' : (Color.LIGHT_RED, Color.BLACK),
Expand Down
1 change: 1 addition & 0 deletions zulipterminal/themes/zt_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
'widget_disabled' : (Color.LIGHT_GRAY, Color.WHITE),
'area:help' : (Color.BLACK, Color.LIGHT_GREEN),
'area:stream' : (Color.BLACK, Color.LIGHT_BLUE),
'area:topic' : (Color.BLACK, Color.LIGHT_BLUE),
'area:msg' : (Color.BLACK, Color.YELLOW),
'area:error' : (Color.BLACK, Color.LIGHT_RED),
'area:user' : (Color.WHITE, Color.DARK_BLUE),
Expand Down
2 changes: 2 additions & 0 deletions zulipterminal/ui_tools/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
if is_command_key("TOGGLE_TOPIC", key):
# Exit topic view
self.view.left_panel.show_stream_view()
elif is_command_key("TOPIC_INFO", key):
self.model.controller.show_topic_info(self.stream_id, self.topic_name)
return super().keypress(size, key)


Expand Down
Loading