From eacf62768c29839ad127e096ab3f78f775b51bb3 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 25 Mar 2023 13:56:18 -0700 Subject: [PATCH 001/276] pull_request_template: Update and improve PR template. The main changes are: - Simplifying titles contributors see while editing via ### instead of bold - Adding 'and why' to the 'What does this PR do?' top title - Updating the 'Tested?' section to be 'How did you test this?' - Generally clarifying each entry - Manual checks split into behavioral and visual entries - 'Linting and tests per commit' removed in favor of a refactor-only entry - Adding a self-review checklist (for each commit) to emphasize expectations - Adding an 'External discussion & connections' section with checkboxes - The comments re including Fixes and chat link requests are here - The 'Interactions' section examples are also moved here - Other entries are added to prompt contributors (unmerged/followup PRs) - Adds tips to the Visual changes section - Zulip general tools - using asciinema for 'videos' --- .github/pull_request_template.md | 75 +++++++++++++++++--------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c210fb441e..941cb5a6c5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,36 +1,41 @@ - - -**What does this PR do?** - - - - - -**Tested?** -- [ ] Manually -- [ ] Existing tests (adapted, if necessary) -- [ ] New tests added (for any new behavior) -- [ ] Passed linting & tests (each commit) - - - -**Commit flow** - +### What does this PR do, and why? + + + +### Outstanding aspect(s) + + +- [ ] + +### External discussion & connections + +- [ ] Discussed in **#zulip-terminal** in `topic` +- [ ] Fully fixes # +- [ ] Partially fixes issue # +- [ ] Builds upon previous unmerged work in PR # +- [ ] Is a follow-up to work in PR # +- [ ] Requires merge of PR # +- [ ] Merge will enable work on # + +### How did you test this? + +- [ ] Manually - Behavioral changes +- [ ] Manually - Visual changes +- [ ] Adapting existing automated tests +- [ ] Adding automated tests for new behavior (or missing tests) +- [ ] Existing automated tests should already cover this (*only a refactor of tested code*) + +### Self-review checklist for each commit +- [ ] It is a [minimal coherent idea](https://github.com/zulip/zulip-terminal#structuring-commits---speeding-up-reviews-merging--development) +- [ ] It has a commit summary following the [documented style](https://github.com/zulip/zulip-terminal#structuring-commits---speeding-up-reviews-merging--development) (title & body) +- [ ] It has a commit summary describing the motivation and reasoning for the change +- [ ] It individually passes linting and tests +- [ ] It contains test additions for any new behavior +- [ ] It flows clearly from a previous branch commit, and/or prepares for the next commit + +### Visual changes + + - -**Notes & Questions** - - -**Interactions** - - -**Visual changes** From a522fc2c8c8f9fd8e1b7f407da0b0a485f4c099f Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 27 Mar 2023 13:35:49 -0700 Subject: [PATCH 002/276] lint-all: Reorder linting for faster development cycle. By running more important, faster checks first, feedback is given to the developer sooner when failures occur. In addition, this addresses two specific issues: - ruff and black appear to disagree in some cases; running them first makes that faster to resolve - isort gives less specific feedback on error locations than black (or ruff) when there are syntax errors, so if isort is first and fails, causing linting as a whole to fail, then that information is not available The tools most likely to trigger failures are now run in order of the fastest tool first, ie. starting with: - ruff (check) [was 3rd] - black (check) [was 4th] - isort (check) [was 1st] - mypy [was 2nd] Spelling and code-document synchronization typically run fast, but are less important checks that can be worked on later, so remain after the above tools. --- tools/lint-all | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/lint-all b/tools/lint-all index 1b148fc6e1..b9a38462bb 100755 --- a/tools/lint-all +++ b/tools/lint-all @@ -15,10 +15,10 @@ python_sources = [ python_sources += lintable_tools_files tools = { - "Import order (isort)": "./tools/run-isort-check", - "Type consistency (mypy)": "./tools/run-mypy", "PEP8 & more (ruff)": ["ruff"] + python_sources, "Formatting (black)": ["black", "--check"] + python_sources, + "Import order (isort)": "./tools/run-isort-check", + "Type consistency (mypy)": "./tools/run-mypy", "Common spelling mistakes": "./tools/run-spellcheck", "Hotkey linting & docs sync check": ["./tools/lint-hotkeys"], "Docstring linting & docs sync check (lint-docstring)": "./tools/lint-docstring", From 91afac73dee34f89a9d3ebaf010a8fa10311e469 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 28 Mar 2023 13:43:54 -0700 Subject: [PATCH 003/276] refactor: api_types: Add resource notes for API data structure groups. --- zulipterminal/api_types.py | 43 +++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index a772c576b4..f8b2f58003 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -22,6 +22,10 @@ TYPING_STARTED_WAIT_PERIOD = 10 TYPING_STOPPED_WAIT_PERIOD = 5 +############################################################################### +# Parameter to pass in request to: +# https://zulip.com/api/send-message + class PrivateComposition(TypedDict): type: Literal["private"] @@ -36,9 +40,12 @@ class StreamComposition(TypedDict): subject: str # TODO: Migrate to using topic -# https://zulip.com/api/send-message Composition = Union[PrivateComposition, StreamComposition] +############################################################################### +# Parameter to pass in request to: +# https://zulip.com/api/update-message + class PrivateMessageUpdateRequest(TypedDict): message_id: int @@ -64,9 +71,15 @@ class StreamMessageUpdateRequest(TypedDict): # stream_id: int -# https://zulip.com/api/update-message MessageUpdateRequest = Union[PrivateMessageUpdateRequest, StreamMessageUpdateRequest] +############################################################################### +# In "messages" response from: +# https://zulip.com/api/get-messages +# In "message" response from: +# https://zulip.com/api/get-events#message +# https://zulip.com/api/get-message (unused) + class Message(TypedDict, total=False): id: int @@ -102,7 +115,14 @@ class Message(TypedDict, total=False): # sender_short_name: str -# Elements and types taken from https://zulip.com/api/get-events +############################################################################### +# In "subscriptions" response from: +# https://zulip.com/api/register-queue +# Also directly from: +# https://zulip.com/api/get-events#subscription-add +# https://zulip.com/api/get-subscriptions (unused) + + class Subscription(TypedDict): stream_id: int name: str @@ -136,6 +156,17 @@ class Subscription(TypedDict): # in_home_view: bool # Replaced by is_muted in Zulip 2.1; still present in updates +############################################################################### +# In "realm_user" response from: +# https://zulip.com/api/register-queue +# Also directly from: +# https://zulip.com/api/get-events#realm_user-add +# https://zulip.com/api/get-users (unused) +# https://zulip.com/api/get-own-user (unused) +# https://zulip.com/api/get-user (unused) +# NOTE: Responses between versions & endpoints vary + + class RealmUser(TypedDict): user_id: int full_name: str @@ -173,6 +204,12 @@ class RealmUser(TypedDict): # max_message_id: int # NOTE: DEPRECATED & only for /users/me +############################################################################### +# Events possible in "events" from: +# https://zulip.com/api/get-events +# (also helper data structures not used elsewhere) + + class MessageEvent(TypedDict): type: Literal["message"] message: Message From dee956e6f9c659f5deaa39b858706366bd4fd344 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 10 Mar 2023 16:00:12 -0800 Subject: [PATCH 004/276] api_types: Add ServerSettings & related types. This is as documented at https://zulip.com/api/get-server-settings. Also additional discussion on chat.zulip.org at topic # api documentation > /server_settings: `realm_name`, etc. --- zulipterminal/api_types.py | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index f8b2f58003..4fdc812845 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -357,3 +357,59 @@ class UpdateGlobalNotificationsEvent(TypedDict): UpdateGlobalNotificationsEvent, RealmUserEvent, ] + +############################################################################### +# In response from: +# https://zulip.com/api/get-server-settings + +AuthenticationMethod = Literal[ + "password", + "dev", + "email", + "ldap", + "remoteuser", + "github", + "azuread", + "gitlab", # New in Zulip 3.0, ZFL 1 + "apple", + "google", + "saml", + "openid_connect", +] + + +class ExternalAuthenticationMethod(TypedDict): + name: str + display_name: str + display_icon: Optional[str] + login_url: str + signup_url: str + + +# As of ZFL 121 +class ServerSettings(TypedDict): + # authentication_methods is deprecated in favor of external_authentication_methods + authentication_methods: Dict[AuthenticationMethod, bool] + # Added in Zulip 2.1.0 + external_authentication_methods: List[ExternalAuthenticationMethod] + + # TODO Refactor ZFL to default to zero + zulip_feature_level: NotRequired[int] # New in Zulip 3.0, ZFL 1 + zulip_version: str + zulip_merge_base: NotRequired[str] # New in Zulip 5.0, ZFL 88 + + push_notifications_enabled: bool + is_incompatible: bool + email_auth_enabled: bool + require_email_format_usernames: bool + + # This appears to be present for all Zulip servers, even for no organization, + # which makes it useful to determine a 'preferred' URL for the server/organization + realm_uri: str + + # These may only be present if it's an organization, not just a Zulip server + # Re realm_name discussion, See #api document > /server_settings: `realm_name`, etc. + realm_name: NotRequired[str] # Absence indicates root Zulip server but no realm + realm_icon: str + realm_description: str + realm_web_public_access_enabled: NotRequired[bool] # New in Zulip 5.0, ZFL 116 From 3c8a8945b7841df37a2462ba0de5bc2e583a87ad Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 10 Mar 2023 17:53:25 -0800 Subject: [PATCH 005/276] refactor: run: Apply new ServerSettings type. Test updated. --- tests/cli/test_run.py | 7 ++++++- zulipterminal/cli/run.py | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 9fafdab363..6cc20d3c11 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -8,6 +8,7 @@ from pytest import CaptureFixture from pytest_mock import MockerFixture +from zulipterminal.api_types import ServerSettings from zulipterminal.cli.run import ( _write_zuliprc, exit_with_error, @@ -54,7 +55,11 @@ def test_in_color(color: str, code: str, text: str = "some text") -> None: (dict(require_email_format_usernames=True, email_auth_enabled=False), "Email"), ], ) -def test_get_login_id(mocker: MockerFixture, json: Dict[str, bool], label: str) -> None: +def test_get_login_id( + mocker: MockerFixture, + json: ServerSettings, # NOTE: pytest does not ensure dict above is complete + label: str, +) -> None: mocked_styled_input = mocker.patch( MODULE + ".styled_input", return_value="input return value" ) diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index 225f37d0fe..fe3e0e5e8a 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -11,11 +11,12 @@ import traceback from enum import Enum from os import path, remove -from typing import Any, Dict, List, NamedTuple, Optional, Tuple +from typing import Dict, List, NamedTuple, Optional, Tuple import requests from urwid import display_common, set_encoding +from zulipterminal.api_types import ServerSettings from zulipterminal.config.themes import ( InvalidThemeColorCode, aliased_themes, @@ -206,7 +207,7 @@ def styled_input(label: str) -> str: return input(in_color("blue", label)) -def get_login_id(server_properties: Dict[str, Any]) -> str: +def get_login_id(server_properties: ServerSettings) -> str: require_email_format_usernames = server_properties["require_email_format_usernames"] email_auth_enabled = server_properties["email_auth_enabled"] From 3d1cd26b080b67d9fc2baceb71c9dcdc6d8c9c93 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 10 Mar 2023 16:13:13 -0800 Subject: [PATCH 006/276] refactor: run: Move username prompt from get_login_id into get_api_key. Since get_login_id now returns a string only, it is also renamed to get_login_label. Test updated. --- tests/cli/test_run.py | 14 ++++---------- zulipterminal/cli/run.py | 7 ++++--- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 6cc20d3c11..174d7bb2db 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -12,7 +12,7 @@ from zulipterminal.cli.run import ( _write_zuliprc, exit_with_error, - get_login_id, + get_login_label, in_color, main, parse_args, @@ -55,19 +55,13 @@ def test_in_color(color: str, code: str, text: str = "some text") -> None: (dict(require_email_format_usernames=True, email_auth_enabled=False), "Email"), ], ) -def test_get_login_id( +def test_get_login_label( mocker: MockerFixture, json: ServerSettings, # NOTE: pytest does not ensure dict above is complete label: str, ) -> None: - mocked_styled_input = mocker.patch( - MODULE + ".styled_input", return_value="input return value" - ) - - result = get_login_id(json) - - assert result == "input return value" - mocked_styled_input.assert_called_with(label + ": ") + result = get_login_label(json) + assert result == label + ": " @pytest.mark.parametrize("options", ["-h", "--help"]) diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index fe3e0e5e8a..64b0964171 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -207,7 +207,7 @@ def styled_input(label: str) -> str: return input(in_color("blue", label)) -def get_login_id(server_properties: ServerSettings) -> str: +def get_login_label(server_properties: ServerSettings) -> str: require_email_format_usernames = server_properties["require_email_format_usernames"] email_auth_enabled = server_properties["email_auth_enabled"] @@ -219,7 +219,7 @@ def get_login_id(server_properties: ServerSettings) -> str: # TODO: Validate Email address label = "Email: " - return styled_input(label) + return label def get_api_key(realm_url: str) -> Optional[Tuple[str, str, str]]: @@ -231,7 +231,8 @@ def get_api_key(realm_url: str) -> Optional[Tuple[str, str, str]]: # This avoids cases where there are redirects between http and https, for example preferred_realm_url = server_properties["realm_uri"] - login_id = get_login_id(server_properties) + login_id_label = get_login_label(server_properties) + login_id = styled_input(login_id_label) password = getpass(in_color("blue", "Password: ")) response = requests.post( From 5dcd87038ad5671fa4d381262c9d184bb9e53c03 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 10 Mar 2023 16:31:53 -0800 Subject: [PATCH 007/276] refactor: run: Simplify get_login_label. --- zulipterminal/cli/run.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index 64b0964171..ae82003547 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -212,14 +212,12 @@ def get_login_label(server_properties: ServerSettings) -> str: email_auth_enabled = server_properties["email_auth_enabled"] if not require_email_format_usernames and email_auth_enabled: - label = "Email or Username: " + return "Email or Username: " elif not require_email_format_usernames: - label = "Username: " + return "Username: " else: # TODO: Validate Email address - label = "Email: " - - return label + return "Email: " def get_api_key(realm_url: str) -> Optional[Tuple[str, str, str]]: From d321eeb0528d55dc770111417fb3f226604097a6 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 10 Mar 2023 16:47:53 -0800 Subject: [PATCH 008/276] refactor: run: Extract get_server_settings into standalone function. Add minimal test. --- tests/cli/test_run.py | 35 +++++++++++++++++++++++++++++++++++ zulipterminal/cli/run.py | 7 ++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 174d7bb2db..fc68b6308a 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -13,6 +13,7 @@ _write_zuliprc, exit_with_error, get_login_label, + get_server_settings, in_color, main, parse_args, @@ -64,6 +65,40 @@ def test_get_login_label( assert result == label + ": " +@pytest.fixture +def server_settings_minimal() -> ServerSettings: + return ServerSettings( + authentication_methods={}, + external_authentication_methods=[], + zulip_feature_level=0, # New in Zulip 3.0, ZFL 1 + zulip_version="2.1.0", + zulip_merge_base="", # New in Zulip 5.0, ZFL 88 + push_notifications_enabled=True, + is_incompatible=False, + require_email_format_usernames=False, + email_auth_enabled=True, + realm_uri="chat.zulip.zulip", # Present if a Zulip server; preferred URL + realm_name="A Zulip Server", # Present if Organization is active at URL + realm_icon="...", + realm_description="Very exciting server", + realm_web_public_access_enabled=True, # New in Zulip 5.0, ZFL 116 + ) + + +def test_get_server_settings( + mocker: MockerFixture, + server_settings_minimal: ServerSettings, + realm_url: str = "https://chat.zulip.org", +) -> None: + response = mocker.Mock(json=lambda: server_settings_minimal) + mocked_get = mocker.patch("requests.get", return_value=response) + + result = get_server_settings(realm_url) + + assert mocked_get.called_once_with(url=realm_url + "/api/v1/server_settings") + assert result == server_settings_minimal + + @pytest.mark.parametrize("options", ["-h", "--help"]) def test_main_help(capsys: CaptureFixture[str], options: str) -> None: with pytest.raises(SystemExit): diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index ae82003547..0cf596ffe2 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -220,10 +220,15 @@ def get_login_label(server_properties: ServerSettings) -> str: return "Email: " +def get_server_settings(realm_url: str) -> ServerSettings: + server_properties = requests.get(url=f"{realm_url}/api/v1/server_settings").json() + return server_properties + + def get_api_key(realm_url: str) -> Optional[Tuple[str, str, str]]: from getpass import getpass - server_properties = requests.get(url=f"{realm_url}/api/v1/server_settings").json() + server_properties = get_server_settings(realm_url) # Assuming we connect to and get data from the server, use the realm_url it suggests # This avoids cases where there are redirects between http and https, for example From f750cd15698382a92d73e8ec13efcb757b2f6111 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 10 Mar 2023 17:27:25 -0800 Subject: [PATCH 009/276] run: Add handling to get_server_setting if not a Zulip Organization. Test added and updated. --- tests/cli/test_run.py | 21 ++++++++++++++++++++- zulipterminal/cli/run.py | 15 ++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index fc68b6308a..9d4a7219ad 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -5,11 +5,13 @@ from typing import Callable, Dict, Generator, List, Optional, Tuple import pytest +import requests from pytest import CaptureFixture from pytest_mock import MockerFixture from zulipterminal.api_types import ServerSettings from zulipterminal.cli.run import ( + NotAZulipOrganizationError, _write_zuliprc, exit_with_error, get_login_label, @@ -90,7 +92,9 @@ def test_get_server_settings( server_settings_minimal: ServerSettings, realm_url: str = "https://chat.zulip.org", ) -> None: - response = mocker.Mock(json=lambda: server_settings_minimal) + response = mocker.Mock( + status_code=requests.codes.OK, json=lambda: server_settings_minimal + ) mocked_get = mocker.patch("requests.get", return_value=response) result = get_server_settings(realm_url) @@ -99,6 +103,21 @@ def test_get_server_settings( assert result == server_settings_minimal +def test_get_server_settings__not_a_zulip_organization( + mocker: MockerFixture, realm_url: str = "https://google.com" +) -> None: + response = mocker.Mock( + status_code=requests.codes.bad_request # FIXME: Test others? + ) + mocked_get = mocker.patch("requests.get", return_value=response) + + with pytest.raises(NotAZulipOrganizationError) as exc: + get_server_settings(realm_url) + + assert mocked_get.called_once_with(url=realm_url + "/api/v1/server_settings") + assert str(exc.value) == realm_url + + @pytest.mark.parametrize("options", ["-h", "--help"]) def test_main_help(capsys: CaptureFixture[str], options: str) -> None: with pytest.raises(SystemExit): diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index 0cf596ffe2..e6f5f22c06 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -220,15 +220,24 @@ def get_login_label(server_properties: ServerSettings) -> str: return "Email: " +class NotAZulipOrganizationError(Exception): + pass + + def get_server_settings(realm_url: str) -> ServerSettings: - server_properties = requests.get(url=f"{realm_url}/api/v1/server_settings").json() - return server_properties + response = requests.get(url=f"{realm_url}/api/v1/server_settings") + if response.status_code != requests.codes.OK: + raise NotAZulipOrganizationError(realm_url) + return response.json() def get_api_key(realm_url: str) -> Optional[Tuple[str, str, str]]: from getpass import getpass - server_properties = get_server_settings(realm_url) + try: + server_properties = get_server_settings(realm_url) + except NotAZulipOrganizationError: + exit_with_error(f"No Zulip Organization found at {realm_url}.") # Assuming we connect to and get data from the server, use the realm_url it suggests # This avoids cases where there are redirects between http and https, for example From a7eb4b1047a65932e36bd42388ca66b539f6183d Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 4 Apr 2023 13:41:12 -0700 Subject: [PATCH 010/276] unicode_emojis: Update list of unicode emojis from server using script. --- zulipterminal/unicode_emojis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zulipterminal/unicode_emojis.py b/zulipterminal/unicode_emojis.py index d4d449976b..90cd422d3d 100644 --- a/zulipterminal/unicode_emojis.py +++ b/zulipterminal/unicode_emojis.py @@ -517,7 +517,7 @@ ("flag_china", {'code': '1f1e8-1f1f3', 'aliases': []}), ("flag_christmas_island", {'code': '1f1e8-1f1fd', 'aliases': []}), ("flag_clipperton_island", {'code': '1f1e8-1f1f5', 'aliases': []}), - ("flag_cocos_(keeling)_islands", {'code': '1f1e8-1f1e8', 'aliases': []}), + ("flag_cocos_keeling_islands", {'code': '1f1e8-1f1e8', 'aliases': []}), ("flag_colombia", {'code': '1f1e8-1f1f4', 'aliases': []}), ("flag_comoros", {'code': '1f1f0-1f1f2', 'aliases': []}), ("flag_congo_brazzaville", {'code': '1f1e8-1f1ec', 'aliases': []}), @@ -529,7 +529,7 @@ ("flag_curaçao", {'code': '1f1e8-1f1fc', 'aliases': ['flag_curacao']}), ("flag_cyprus", {'code': '1f1e8-1f1fe', 'aliases': []}), ("flag_czechia", {'code': '1f1e8-1f1ff', 'aliases': []}), - ("flag_côte_d'ivoire", {'code': '1f1e8-1f1ee', 'aliases': ["flag_cote_d'ivoire"]}), + ("flag_côte_divoire", {'code': '1f1e8-1f1ee', 'aliases': ['flag_cote_divoire']}), ("flag_denmark", {'code': '1f1e9-1f1f0', 'aliases': []}), ("flag_diego_garcia", {'code': '1f1e9-1f1ec', 'aliases': []}), ("flag_djibouti", {'code': '1f1e9-1f1ef', 'aliases': []}), @@ -623,7 +623,7 @@ ("flag_montserrat", {'code': '1f1f2-1f1f8', 'aliases': []}), ("flag_morocco", {'code': '1f1f2-1f1e6', 'aliases': []}), ("flag_mozambique", {'code': '1f1f2-1f1ff', 'aliases': []}), - ("flag_myanmar_(burma)", {'code': '1f1f2-1f1f2', 'aliases': []}), + ("flag_myanmar_burma", {'code': '1f1f2-1f1f2', 'aliases': []}), ("flag_namibia", {'code': '1f1f3-1f1e6', 'aliases': []}), ("flag_nauru", {'code': '1f1f3-1f1f7', 'aliases': []}), ("flag_nepal", {'code': '1f1f3-1f1f5', 'aliases': []}), From 3fbd5ac8463aeaf3ae9bc78ae58ba83b76925a66 Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Wed, 22 Mar 2023 13:12:07 +0000 Subject: [PATCH 011/276] refactor: convert-unicode-emoji-data: Use dict for sorted input emoji. ordered_emojis was initialized as an OrderedDict to preserve its sorted insertion order when used in creating unicode_emojis.EMOJI_DATA. Since Dict also preserves insertion order from python3.7, we can safely initialize ordered_emojis as a Dict without any change in behavior. --- tools/convert-unicode-emoji-data | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/convert-unicode-emoji-data b/tools/convert-unicode-emoji-data index 015e39e326..b9da5c2efa 100755 --- a/tools/convert-unicode-emoji-data +++ b/tools/convert-unicode-emoji-data @@ -3,7 +3,6 @@ # Convert emoji file downloaded using tools/fetch-unicode-emoji-data # into what is required by ZT. -from collections import OrderedDict from pathlib import Path, PurePath @@ -32,7 +31,7 @@ for emoji_code, emoji_data in emoji_dict.items(): "aliases": emoji_data["aliases"], } -ordered_emojis = OrderedDict(sorted(emoji_map.items())) +ordered_emojis = dict(sorted(emoji_map.items())) with OUTPUT_FILE.open("w") as f: f.write( From fc7ad34e14397554cd8c615649ff058dec48f4b2 Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Thu, 23 Mar 2023 08:01:19 +0000 Subject: [PATCH 012/276] refactor: convert-unicode-emoji-data/unicode_emojis: Output using dict. EMOJI_DATA is generated by convert-unicode-emoji-data and was initialized as an OrderedDict to preserve its sorted insertion order when used in model.Model.generate_all_emoji_data to aggregate all emoji data. Since Dict also preserves insertion order from python3.7, we can safely initialize EMOJI_DATA as a Dict without any change in behavior. unicode_emojis.py regenerated using updated conversion script. --- tools/convert-unicode-emoji-data | 9 +- zulipterminal/unicode_emojis.py | 3661 +++++++++++++++--------------- 2 files changed, 1831 insertions(+), 1839 deletions(-) diff --git a/tools/convert-unicode-emoji-data b/tools/convert-unicode-emoji-data index b9da5c2efa..00a28fdc66 100755 --- a/tools/convert-unicode-emoji-data +++ b/tools/convert-unicode-emoji-data @@ -43,23 +43,20 @@ with OUTPUT_FILE.open("w") as f: "# Ignore long lines", "# ruff: noqa: E501", "", - "from collections import OrderedDict\n\n", "# fmt: off", f"# Generated automatically by tools/{SCRIPT_NAME}", "# Do not modify.\n", - "EMOJI_DATA = OrderedDict(", - " [\n", + "EMOJI_DATA = {\n", ] ) ) for emoji_name, emoji in ordered_emojis.items(): # {'smile': {'code': '263a', 'aliases': []}} - f.write(f' ("{emoji_name}", {emoji}),\n') + f.write(f' "{emoji_name}": {emoji},\n') f.write( "\n".join( [ - " ]", - ")", + "}", "# fmt: on", "", ] diff --git a/zulipterminal/unicode_emojis.py b/zulipterminal/unicode_emojis.py index 90cd422d3d..4daac0bb8c 100644 --- a/zulipterminal/unicode_emojis.py +++ b/zulipterminal/unicode_emojis.py @@ -4,1841 +4,1836 @@ # Ignore long lines # ruff: noqa: E501 -from collections import OrderedDict - - # fmt: off # Generated automatically by tools/convert-unicode-emoji-data # Do not modify. -EMOJI_DATA = OrderedDict( - [ - ("+1", {'code': '1f44d', 'aliases': ['thumbs_up', 'like']}), - ("-1", {'code': '1f44e', 'aliases': ['thumbs_down']}), - ("100", {'code': '1f4af', 'aliases': ['hundred']}), - ("1234", {'code': '1f522', 'aliases': ['numbers']}), - ("a", {'code': '1f170', 'aliases': []}), - ("ab", {'code': '1f18e', 'aliases': []}), - ("abacus", {'code': '1f9ee', 'aliases': ['calculation']}), - ("abc", {'code': '1f524', 'aliases': []}), - ("abcd", {'code': '1f521', 'aliases': ['alphabet']}), - ("accessible", {'code': '267f', 'aliases': ['wheelchair', 'disabled']}), - ("accordion", {'code': '1fa97', 'aliases': ['concertina', 'squeeze_box']}), - ("action", {'code': '1f3ac', 'aliases': []}), - ("adhesive_bandage", {'code': '1fa79', 'aliases': ['bandage']}), - ("aerial_tramway", {'code': '1f6a1', 'aliases': ['ski_lift']}), - ("airplane", {'code': '2708', 'aliases': []}), - ("alarm_clock", {'code': '23f0', 'aliases': []}), - ("alchemy", {'code': '2697', 'aliases': ['alembic']}), - ("alien", {'code': '1f47d', 'aliases': ['ufo']}), - ("ambulance", {'code': '1f691', 'aliases': []}), - ("american_football", {'code': '1f3c8', 'aliases': []}), - ("anatomical_heart", {'code': '1fac0', 'aliases': ['anatomical', 'cardiology', 'pulse']}), - ("anchor", {'code': '2693', 'aliases': []}), - ("angel", {'code': '1f47c', 'aliases': []}), - ("anger", {'code': '1f4a2', 'aliases': ['bam', 'pow']}), - ("anger_bubble", {'code': '1f5ef', 'aliases': []}), - ("angry", {'code': '1f620', 'aliases': []}), - ("angry_cat", {'code': '1f63e', 'aliases': ['pouting_cat']}), - ("anguish", {'code': '1f62b', 'aliases': []}), - ("anguished", {'code': '1f627', 'aliases': ['pained']}), - ("ant", {'code': '1f41c', 'aliases': []}), - ("apple", {'code': '1f34e', 'aliases': []}), - ("aquarius", {'code': '2652', 'aliases': []}), - ("arabian_camel", {'code': '1f42a', 'aliases': []}), - ("archive", {'code': '1f5c3', 'aliases': []}), - ("aries", {'code': '2648', 'aliases': []}), - ("art", {'code': '1f3a8', 'aliases': ['palette', 'painting']}), - ("artist", {'code': '1f9d1-200d-1f3a8', 'aliases': []}), - ("asterisk", {'code': '002a-20e3', 'aliases': []}), - ("astonished", {'code': '1f632', 'aliases': []}), - ("astronaut", {'code': '1f9d1-200d-1f680', 'aliases': []}), - ("at_work", {'code': '2692', 'aliases': ['hammer_and_pick']}), - ("athletic_shoe", {'code': '1f45f', 'aliases': ['sneaker', 'running_shoe']}), - ("atm", {'code': '1f3e7', 'aliases': []}), - ("atom", {'code': '269b', 'aliases': ['physics']}), - ("auto_rickshaw", {'code': '1f6fa', 'aliases': ['tuk_tuk']}), - ("avocado", {'code': '1f951', 'aliases': []}), - ("axe", {'code': '1fa93', 'aliases': ['hatchet', 'split']}), - ("b", {'code': '1f171', 'aliases': []}), - ("baby", {'code': '1f476', 'aliases': []}), - ("baby_bottle", {'code': '1f37c', 'aliases': []}), - ("baby_change_station", {'code': '1f6bc', 'aliases': ['nursery']}), - ("back", {'code': '1f519', 'aliases': []}), - ("backpack", {'code': '1f392', 'aliases': ['satchel']}), - ("bacon", {'code': '1f953', 'aliases': []}), - ("badger", {'code': '1f9a1', 'aliases': ['honey_badger', 'pester']}), - ("badminton", {'code': '1f3f8', 'aliases': []}), - ("bagel", {'code': '1f96f', 'aliases': ['schmear']}), - ("baggage_claim", {'code': '1f6c4', 'aliases': []}), - ("baguette", {'code': '1f956', 'aliases': []}), - ("ball", {'code': '26f9', 'aliases': ['sports']}), - ("ballet_shoes", {'code': '1fa70', 'aliases': ['ballet']}), - ("balloon", {'code': '1f388', 'aliases': ['celebration']}), - ("ballot_box", {'code': '1f5f3', 'aliases': []}), - ("bamboo", {'code': '1f38d', 'aliases': []}), - ("banana", {'code': '1f34c', 'aliases': []}), - ("bangbang", {'code': '203c', 'aliases': ['double_exclamation']}), - ("banjo", {'code': '1fa95', 'aliases': ['stringed']}), - ("bank", {'code': '1f3e6', 'aliases': []}), - ("bar_chart", {'code': '1f4ca', 'aliases': []}), - ("barber", {'code': '1f488', 'aliases': ['striped_pole']}), - ("baseball", {'code': '26be', 'aliases': []}), - ("basket", {'code': '1f9fa', 'aliases': ['farming', 'laundry', 'picnic']}), - ("basketball", {'code': '1f3c0', 'aliases': []}), - ("bat", {'code': '1f987', 'aliases': []}), - ("bath", {'code': '1f6c0', 'aliases': []}), - ("bathtub", {'code': '1f6c1', 'aliases': []}), - ("battery", {'code': '1f50b', 'aliases': ['full_battery']}), - ("beach", {'code': '1f3d6', 'aliases': []}), - ("beach_umbrella", {'code': '26f1', 'aliases': []}), - ("beans", {'code': '1fad8', 'aliases': ['kidney', 'legume']}), - ("bear", {'code': '1f43b', 'aliases': []}), - ("beaver", {'code': '1f9ab', 'aliases': ['dam']}), - ("bed", {'code': '1f6cf', 'aliases': ['bedroom']}), - ("bee", {'code': '1f41d', 'aliases': ['buzz', 'honeybee']}), - ("beer", {'code': '1f37a', 'aliases': []}), - ("beers", {'code': '1f37b', 'aliases': []}), - ("beetle", {'code': '1fab2', 'aliases': []}), - ("beginner", {'code': '1f530', 'aliases': []}), - ("bell_pepper", {'code': '1fad1', 'aliases': ['capsicum', 'pepper', 'vegetable']}), - ("bellhop_bell", {'code': '1f6ce', 'aliases': ['reception', 'services', 'ding']}), - ("bento", {'code': '1f371', 'aliases': []}), - ("beverage_box", {'code': '1f9c3', 'aliases': ['beverage', 'box', 'straw']}), - ("big_smile", {'code': '1f604', 'aliases': []}), - ("bike", {'code': '1f6b2', 'aliases': ['bicycle']}), - ("bikini", {'code': '1f459', 'aliases': []}), - ("billed_cap", {'code': '1f9e2', 'aliases': ['baseball_cap']}), - ("billiards", {'code': '1f3b1', 'aliases': ['pool', '8_ball']}), - ("biohazard", {'code': '2623', 'aliases': []}), - ("bird", {'code': '1f426', 'aliases': []}), - ("birthday", {'code': '1f382', 'aliases': []}), - ("bison", {'code': '1f9ac', 'aliases': ['buffalo', 'herd', 'wisent']}), - ("biting_lip", {'code': '1fae6', 'aliases': ['flirting', 'uncomfortable']}), - ("black_and_white_square", {'code': '1f533', 'aliases': []}), - ("black_belt", {'code': '1f94b', 'aliases': ['keikogi', 'dogi', 'martial_arts']}), - ("black_cat", {'code': '1f408-200d-2b1b', 'aliases': ['black', 'unlucky']}), - ("black_circle", {'code': '26ab', 'aliases': []}), - ("black_flag", {'code': '1f3f4', 'aliases': []}), - ("black_heart", {'code': '1f5a4', 'aliases': []}), - ("black_large_square", {'code': '2b1b', 'aliases': []}), - ("black_medium_small_square", {'code': '25fe', 'aliases': []}), - ("black_medium_square", {'code': '25fc', 'aliases': []}), - ("black_nib", {'code': '2712', 'aliases': ['nib']}), - ("black_small_square", {'code': '25aa', 'aliases': []}), - ("blossom", {'code': '1f33c', 'aliases': []}), - ("blowfish", {'code': '1f421', 'aliases': []}), - ("blue_book", {'code': '1f4d8', 'aliases': []}), - ("blue_circle", {'code': '1f535', 'aliases': []}), - ("blue_heart", {'code': '1f499', 'aliases': []}), - ("blue_square", {'code': '1f7e6', 'aliases': []}), - ("blueberries", {'code': '1fad0', 'aliases': ['berry', 'bilberry', 'blueberry']}), - ("blush", {'code': '1f60a', 'aliases': []}), - ("boar", {'code': '1f417', 'aliases': []}), - ("boat", {'code': '26f5', 'aliases': ['sailboat']}), - ("bomb", {'code': '1f4a3', 'aliases': []}), - ("bone", {'code': '1f9b4', 'aliases': []}), - ("book", {'code': '1f4d6', 'aliases': ['open_book']}), - ("bookmark", {'code': '1f516', 'aliases': []}), - ("books", {'code': '1f4da', 'aliases': []}), - ("boom", {'code': '1f4a5', 'aliases': ['explosion', 'crash', 'collision']}), - ("boomerang", {'code': '1fa83', 'aliases': ['rebound', 'repercussion']}), - ("boot", {'code': '1f462', 'aliases': []}), - ("bouquet", {'code': '1f490', 'aliases': []}), - ("bow", {'code': '1f647', 'aliases': []}), - ("bow_and_arrow", {'code': '1f3f9', 'aliases': ['archery']}), - ("bowl_with_spoon", {'code': '1f963', 'aliases': ['cereal', 'congee']}), - ("boxing_glove", {'code': '1f94a', 'aliases': []}), - ("boy", {'code': '1f466', 'aliases': []}), - ("brain", {'code': '1f9e0', 'aliases': ['intelligent']}), - ("bread", {'code': '1f35e', 'aliases': []}), - ("breast_feeding", {'code': '1f931', 'aliases': ['breast']}), - ("brick", {'code': '1f9f1', 'aliases': ['bricks', 'clay', 'mortar', 'wall']}), - ("bride", {'code': '1f470', 'aliases': []}), - ("bridge", {'code': '1f309', 'aliases': []}), - ("briefcase", {'code': '1f4bc', 'aliases': []}), - ("briefs", {'code': '1fa72', 'aliases': ['one_piece', 'swimsuit']}), - ("brightness", {'code': '1f506', 'aliases': ['high_brightness']}), - ("broccoli", {'code': '1f966', 'aliases': ['wild_cabbage']}), - ("broken_heart", {'code': '1f494', 'aliases': ['heartache']}), - ("broom", {'code': '1f9f9', 'aliases': ['sweeping']}), - ("brown_circle", {'code': '1f7e4', 'aliases': []}), - ("brown_heart", {'code': '1f90e', 'aliases': []}), - ("brown_square", {'code': '1f7eb', 'aliases': []}), - ("bubble_tea", {'code': '1f9cb', 'aliases': []}), - ("bubbles", {'code': '1fae7', 'aliases': ['burp', 'underwater']}), - ("bucket", {'code': '1faa3', 'aliases': ['cask', 'pail', 'vat']}), - ("bug", {'code': '1f41b', 'aliases': ['caterpillar']}), - ("bullet_train", {'code': '1f685', 'aliases': []}), - ("bunny", {'code': '1f430', 'aliases': []}), - ("burrito", {'code': '1f32f', 'aliases': []}), - ("bus", {'code': '1f68c', 'aliases': ['school_bus']}), - ("bus_stop", {'code': '1f68f', 'aliases': []}), - ("butter", {'code': '1f9c8', 'aliases': ['dairy']}), - ("butterfly", {'code': '1f98b', 'aliases': []}), - ("cactus", {'code': '1f335', 'aliases': []}), - ("cake", {'code': '1f370', 'aliases': []}), - ("calendar", {'code': '1f4c5', 'aliases': []}), - ("calf", {'code': '1f42e', 'aliases': []}), - ("call_me", {'code': '1f919', 'aliases': []}), - ("calling", {'code': '1f4f2', 'aliases': []}), - ("camel", {'code': '1f42b', 'aliases': []}), - ("camera", {'code': '1f4f7', 'aliases': []}), - ("campsite", {'code': '1f3d5', 'aliases': []}), - ("cancer", {'code': '264b', 'aliases': []}), - ("candle", {'code': '1f56f', 'aliases': []}), - ("candy", {'code': '1f36c', 'aliases': []}), - ("canned_food", {'code': '1f96b', 'aliases': ['can']}), - ("canoe", {'code': '1f6f6', 'aliases': []}), - ("capital_abcd", {'code': '1f520', 'aliases': ['capital_letters']}), - ("capricorn", {'code': '2651', 'aliases': []}), - ("car", {'code': '1f697', 'aliases': []}), - ("carousel", {'code': '1f3a0', 'aliases': ['merry_go_round']}), - ("carp_streamer", {'code': '1f38f', 'aliases': ['flags']}), - ("carpenter_square", {'code': '1f4d0', 'aliases': ['triangular_ruler']}), - ("carpentry_saw", {'code': '1fa9a', 'aliases': ['carpenter', 'saw']}), - ("carrot", {'code': '1f955', 'aliases': []}), - ("cartwheel", {'code': '1f938', 'aliases': ['acrobatics', 'gymnastics', 'tumbling']}), - ("castle", {'code': '1f3f0', 'aliases': []}), - ("cat", {'code': '1f408', 'aliases': ['meow']}), - ("cd", {'code': '1f4bf', 'aliases': []}), - ("cell_reception", {'code': '1f4f6', 'aliases': ['signal_strength', 'signal_bars']}), - ("chains", {'code': '26d3', 'aliases': []}), - ("chair", {'code': '1fa91', 'aliases': ['sit']}), - ("champagne", {'code': '1f37e', 'aliases': []}), - ("chart", {'code': '1f4c8', 'aliases': ['upwards_trend', 'growing', 'increasing']}), - ("check", {'code': '2705', 'aliases': ['all_good', 'approved']}), - ("check_mark", {'code': '2714', 'aliases': []}), - ("checkbox", {'code': '2611', 'aliases': []}), - ("checkered_flag", {'code': '1f3c1', 'aliases': ['race', 'go', 'start']}), - ("cheese", {'code': '1f9c0', 'aliases': []}), - ("cherries", {'code': '1f352', 'aliases': []}), - ("cherry_blossom", {'code': '1f338', 'aliases': []}), - ("chess_pawn", {'code': '265f', 'aliases': ['chess', 'dupe', 'expendable']}), - ("chestnut", {'code': '1f330', 'aliases': []}), - ("chick", {'code': '1f424', 'aliases': ['baby_chick']}), - ("chicken", {'code': '1f414', 'aliases': ['cluck']}), - ("child", {'code': '1f9d2', 'aliases': ['young']}), - ("children_crossing", {'code': '1f6b8', 'aliases': ['school_crossing', 'drive_with_care']}), - ("chipmunk", {'code': '1f43f', 'aliases': []}), - ("chocolate", {'code': '1f36b', 'aliases': []}), - ("chopsticks", {'code': '1f962', 'aliases': ['hashi']}), - ("church", {'code': '26ea', 'aliases': []}), - ("cinema", {'code': '1f3a6', 'aliases': ['movie_theater']}), - ("circle", {'code': '2b55', 'aliases': []}), - ("circus", {'code': '1f3aa', 'aliases': []}), - ("city", {'code': '1f3d9', 'aliases': ['skyline']}), - ("city_sunrise", {'code': '1f307', 'aliases': []}), - ("cl", {'code': '1f191', 'aliases': []}), - ("clap", {'code': '1f44f', 'aliases': ['applause']}), - ("classical_building", {'code': '1f3db', 'aliases': []}), - ("clink", {'code': '1f942', 'aliases': ['toast']}), - ("clipboard", {'code': '1f4cb', 'aliases': []}), - ("clockwise", {'code': '1f503', 'aliases': []}), - ("closed_mailbox", {'code': '1f4ea', 'aliases': []}), - ("closed_umbrella", {'code': '1f302', 'aliases': []}), - ("clothing", {'code': '1f45a', 'aliases': []}), - ("cloud", {'code': '2601', 'aliases': ['overcast']}), - ("cloudy", {'code': '1f325', 'aliases': []}), - ("clown", {'code': '1f921', 'aliases': []}), - ("clubs", {'code': '2663', 'aliases': []}), - ("coat", {'code': '1f9e5', 'aliases': ['jacket']}), - ("cockroach", {'code': '1fab3', 'aliases': ['roach']}), - ("cocktail", {'code': '1f378', 'aliases': []}), - ("coconut", {'code': '1f965', 'aliases': ['piña_colada', 'pina_colada']}), - ("coffee", {'code': '2615', 'aliases': []}), - ("coffin", {'code': '26b0', 'aliases': ['burial', 'grave']}), - ("coin", {'code': '1fa99', 'aliases': ['metal']}), - ("cold_face", {'code': '1f976', 'aliases': ['blue_faced', 'freezing', 'frostbite', 'icicles']}), - ("cold_sweat", {'code': '1f630', 'aliases': []}), - ("comet", {'code': '2604', 'aliases': ['meteor']}), - ("compass", {'code': '1f9ed', 'aliases': ['navigation', 'orienteering']}), - ("compression", {'code': '1f5dc', 'aliases': ['vise']}), - ("computer", {'code': '1f4bb', 'aliases': ['laptop']}), - ("computer_mouse", {'code': '1f5b1', 'aliases': []}), - ("confetti", {'code': '1f38a', 'aliases': ['party_ball']}), - ("confounded", {'code': '1f616', 'aliases': ['agony']}), - ("construction", {'code': '1f3d7', 'aliases': []}), - ("construction_worker", {'code': '1f477', 'aliases': []}), - ("control_knobs", {'code': '1f39b', 'aliases': []}), - ("convenience_store", {'code': '1f3ea', 'aliases': []}), - ("cook", {'code': '1f9d1-200d-1f373', 'aliases': []}), - ("cookie", {'code': '1f36a', 'aliases': []}), - ("cooking", {'code': '1f373', 'aliases': []}), - ("cool", {'code': '1f192', 'aliases': []}), - ("copyright", {'code': '00a9', 'aliases': ['c']}), - ("coral", {'code': '1fab8', 'aliases': ['reef']}), - ("corn", {'code': '1f33d', 'aliases': ['maize']}), - ("counterclockwise", {'code': '1f504', 'aliases': ['return']}), - ("couple_with_heart", {'code': '1f491', 'aliases': []}), - ("couple_with_heart_man_man", {'code': '1f468-200d-2764-200d-1f468', 'aliases': []}), - ("couple_with_heart_woman_man", {'code': '1f469-200d-2764-200d-1f468', 'aliases': []}), - ("couple_with_heart_woman_woman", {'code': '1f469-200d-2764-200d-1f469', 'aliases': []}), - ("cow", {'code': '1f404', 'aliases': []}), - ("cowboy", {'code': '1f920', 'aliases': []}), - ("crab", {'code': '1f980', 'aliases': []}), - ("crayon", {'code': '1f58d', 'aliases': []}), - ("credit_card", {'code': '1f4b3', 'aliases': ['debit_card']}), - ("cricket", {'code': '1f997', 'aliases': ['grasshopper']}), - ("cricket_game", {'code': '1f3cf', 'aliases': []}), - ("crocodile", {'code': '1f40a', 'aliases': []}), - ("croissant", {'code': '1f950', 'aliases': []}), - ("cross", {'code': '271d', 'aliases': ['christianity']}), - ("cross_mark", {'code': '274c', 'aliases': ['incorrect', 'wrong']}), - ("crossed_flags", {'code': '1f38c', 'aliases': ['solidarity']}), - ("crown", {'code': '1f451', 'aliases': ['queen', 'king']}), - ("crutch", {'code': '1fa7c', 'aliases': ['cane', 'disability', 'mobility_aid']}), - ("cry", {'code': '1f622', 'aliases': []}), - ("crying_cat", {'code': '1f63f', 'aliases': []}), - ("crystal_ball", {'code': '1f52e', 'aliases': ['oracle', 'future', 'fortune_telling']}), - ("cucumber", {'code': '1f952', 'aliases': []}), - ("cup_with_straw", {'code': '1f964', 'aliases': ['soda']}), - ("cupcake", {'code': '1f9c1', 'aliases': []}), - ("cupid", {'code': '1f498', 'aliases': ['smitten', 'heart_arrow']}), - ("curling_stone", {'code': '1f94c', 'aliases': []}), - ("curry", {'code': '1f35b', 'aliases': []}), - ("custard", {'code': '1f36e', 'aliases': ['flan']}), - ("customs", {'code': '1f6c3', 'aliases': []}), - ("cut_of_meat", {'code': '1f969', 'aliases': ['lambchop', 'porkchop', 'steak']}), - ("cute", {'code': '1f4a0', 'aliases': ['kawaii', 'diamond_with_a_dot']}), - ("cyclist", {'code': '1f6b4', 'aliases': []}), - ("cyclone", {'code': '1f300', 'aliases': ['hurricane', 'typhoon']}), - ("dagger", {'code': '1f5e1', 'aliases': ['rated_for_violence']}), - ("dancer", {'code': '1f483', 'aliases': []}), - ("dancers", {'code': '1f46f', 'aliases': []}), - ("dancing", {'code': '1f57a', 'aliases': ['disco']}), - ("dango", {'code': '1f361', 'aliases': []}), - ("dark_sunglasses", {'code': '1f576', 'aliases': []}), - ("dash", {'code': '1f4a8', 'aliases': []}), - ("date", {'code': '1f4c6', 'aliases': []}), - ("deaf_man", {'code': '1f9cf-200d-2642', 'aliases': []}), - ("deaf_person", {'code': '1f9cf', 'aliases': ['hear']}), - ("deaf_woman", {'code': '1f9cf-200d-2640', 'aliases': []}), - ("decorative_notebook", {'code': '1f4d4', 'aliases': []}), - ("deer", {'code': '1f98c', 'aliases': []}), - ("department_store", {'code': '1f3ec', 'aliases': []}), - ("derelict_house", {'code': '1f3da', 'aliases': ['condemned']}), - ("desert", {'code': '1f3dc', 'aliases': []}), - ("desktop_computer", {'code': '1f5a5', 'aliases': []}), - ("detective", {'code': '1f575', 'aliases': ['spy', 'sleuth', 'agent', 'sneaky']}), - ("devil", {'code': '1f47f', 'aliases': ['imp', 'angry_devil']}), - ("diamonds", {'code': '2666', 'aliases': []}), - ("dice", {'code': '1f3b2', 'aliases': ['die']}), - ("direct_hit", {'code': '1f3af', 'aliases': ['darts', 'bulls_eye']}), - ("disappointed", {'code': '1f61e', 'aliases': []}), - ("disguised_face", {'code': '1f978', 'aliases': ['disguise', 'incognito']}), - ("diving_mask", {'code': '1f93f', 'aliases': ['scuba', 'snorkeling']}), - ("division", {'code': '2797', 'aliases': ['divide']}), - ("diya_lamp", {'code': '1fa94', 'aliases': ['diya', 'lamp', 'oil']}), - ("dizzy", {'code': '1f635', 'aliases': []}), - ("dna", {'code': '1f9ec', 'aliases': ['evolution', 'gene', 'genetics', 'life']}), - ("do_not_litter", {'code': '1f6af', 'aliases': []}), - ("document", {'code': '1f4c4', 'aliases': ['paper', 'file', 'page']}), - ("dodo", {'code': '1f9a4', 'aliases': ['mauritius']}), - ("dog", {'code': '1f415', 'aliases': ['woof']}), - ("dollar_bills", {'code': '1f4b5', 'aliases': []}), - ("dollars", {'code': '1f4b2', 'aliases': []}), - ("dolls", {'code': '1f38e', 'aliases': []}), - ("dolphin", {'code': '1f42c', 'aliases': ['flipper']}), - ("doner_kebab", {'code': '1f959', 'aliases': ['shawarma', 'souvlaki', 'stuffed_flatbread']}), - ("donut", {'code': '1f369', 'aliases': ['doughnut']}), - ("door", {'code': '1f6aa', 'aliases': []}), - ("dormouse", {'code': '1f42d', 'aliases': []}), - ("dotted_line_face", {'code': '1fae5', 'aliases': ['depressed', 'hide', 'introvert', 'invisible']}), - ("dotted_six_pointed_star", {'code': '1f52f', 'aliases': ['fortune']}), - ("double_down", {'code': '23ec', 'aliases': ['fast_down']}), - ("double_loop", {'code': '27bf', 'aliases': ['voicemail']}), - ("double_up", {'code': '23eb', 'aliases': ['fast_up']}), - ("dove", {'code': '1f54a', 'aliases': ['dove_of_peace']}), - ("down", {'code': '2b07', 'aliases': ['south']}), - ("downvote", {'code': '1f53d', 'aliases': ['down_button', 'decrease']}), - ("downwards_trend", {'code': '1f4c9', 'aliases': ['shrinking', 'decreasing']}), - ("dragon", {'code': '1f409', 'aliases': []}), - ("dragon_face", {'code': '1f432', 'aliases': []}), - ("dress", {'code': '1f457', 'aliases': []}), - ("drooling", {'code': '1f924', 'aliases': []}), - ("drop", {'code': '1f4a7', 'aliases': ['water_drop']}), - ("drop_of_blood", {'code': '1fa78', 'aliases': ['bleed', 'blood_donation', 'injury', 'menstruation']}), - ("drum", {'code': '1f941', 'aliases': []}), - ("drumstick", {'code': '1f357', 'aliases': ['poultry']}), - ("duck", {'code': '1f986', 'aliases': []}), - ("duel", {'code': '2694', 'aliases': ['swords']}), - ("dumpling", {'code': '1f95f', 'aliases': ['empanada', 'gyōza', 'jiaozi', 'pierogi', 'potsticker', 'gyoza']}), - ("dvd", {'code': '1f4c0', 'aliases': []}), - ("e-mail", {'code': '1f4e7', 'aliases': []}), - ("eagle", {'code': '1f985', 'aliases': []}), - ("ear", {'code': '1f442', 'aliases': []}), - ("ear_with_hearing_aid", {'code': '1f9bb', 'aliases': ['hard_of_hearing']}), - ("earth_africa", {'code': '1f30d', 'aliases': []}), - ("earth_americas", {'code': '1f30e', 'aliases': []}), - ("earth_asia", {'code': '1f30f', 'aliases': []}), - ("egg", {'code': '1f95a', 'aliases': []}), - ("eggplant", {'code': '1f346', 'aliases': []}), - ("eight", {'code': '0038-20e3', 'aliases': []}), - ("eight_pointed_star", {'code': '2734', 'aliases': []}), - ("eight_spoked_asterisk", {'code': '2733', 'aliases': []}), - ("eject_button", {'code': '23cf', 'aliases': ['eject']}), - ("electric_plug", {'code': '1f50c', 'aliases': []}), - ("elephant", {'code': '1f418', 'aliases': []}), - ("elevator", {'code': '1f6d7', 'aliases': ['hoist']}), - ("elf", {'code': '1f9dd', 'aliases': []}), - ("email", {'code': '2709', 'aliases': ['envelope', 'mail']}), - ("empty_nest", {'code': '1fab9', 'aliases': []}), - ("end", {'code': '1f51a', 'aliases': []}), - ("euro_banknotes", {'code': '1f4b6', 'aliases': []}), - ("evergreen_tree", {'code': '1f332', 'aliases': []}), - ("exchange", {'code': '1f4b1', 'aliases': []}), - ("exclamation", {'code': '2757', 'aliases': []}), - ("exhausted", {'code': '1f625', 'aliases': ['disappointed_relieved', 'stressed']}), - ("exploding_head", {'code': '1f92f', 'aliases': ['mind_blown', 'shocked']}), - ("expressionless", {'code': '1f611', 'aliases': []}), - ("eye", {'code': '1f441', 'aliases': []}), - ("eye_in_speech_bubble", {'code': '1f441-fe0f-200d-1f5e8-fe0f', 'aliases': ['speech', 'witness']}), - ("eyes", {'code': '1f440', 'aliases': ['looking']}), - ("face_exhaling", {'code': '1f62e-200d-1f4a8', 'aliases': ['exhale', 'gasp', 'groan', 'relief', 'whisper', 'whistle']}), - ("face_holding_back_tears", {'code': '1f979', 'aliases': ['resist']}), - ("face_in_clouds", {'code': '1f636-200d-1f32b', 'aliases': ['absentminded', 'face_in_the_fog', 'head_in_clouds']}), - ("face_palm", {'code': '1f926', 'aliases': []}), - ("face_vomiting", {'code': '1f92e', 'aliases': ['puke', 'vomit']}), - ("face_with_diagonal_mouth", {'code': '1fae4', 'aliases': ['meh', 'skeptical', 'unsure']}), - ("face_with_hand_over_mouth", {'code': '1f92d', 'aliases': ['whoops']}), - ("face_with_monocle", {'code': '1f9d0', 'aliases': ['monocle', 'stuffy']}), - ("face_with_open_eyes_and_hand_over_mouth", {'code': '1fae2', 'aliases': ['amazement', 'awe', 'embarrass']}), - ("face_with_peeking_eye", {'code': '1fae3', 'aliases': ['captivated', 'peep', 'stare']}), - ("face_with_raised_eyebrow", {'code': '1f928', 'aliases': ['distrust', 'skeptic']}), - ("face_with_spiral_eyes", {'code': '1f635-200d-1f4ab', 'aliases': ['hypnotized', 'trouble', 'whoa']}), - ("face_with_symbols_on_mouth", {'code': '1f92c', 'aliases': ['swearing']}), - ("factory", {'code': '1f3ed', 'aliases': []}), - ("factory_worker", {'code': '1f9d1-200d-1f3ed', 'aliases': []}), - ("fairy", {'code': '1f9da', 'aliases': []}), - ("falafel", {'code': '1f9c6', 'aliases': ['chickpea', 'meatball']}), - ("fallen_leaf", {'code': '1f342', 'aliases': []}), - ("family", {'code': '1f46a', 'aliases': []}), - ("family_man_boy", {'code': '1f468-200d-1f466', 'aliases': []}), - ("family_man_boy_boy", {'code': '1f468-200d-1f466-200d-1f466', 'aliases': []}), - ("family_man_girl", {'code': '1f468-200d-1f467', 'aliases': []}), - ("family_man_girl_boy", {'code': '1f468-200d-1f467-200d-1f466', 'aliases': []}), - ("family_man_girl_girl", {'code': '1f468-200d-1f467-200d-1f467', 'aliases': []}), - ("family_man_man_boy", {'code': '1f468-200d-1f468-200d-1f466', 'aliases': []}), - ("family_man_man_boy_boy", {'code': '1f468-200d-1f468-200d-1f466-200d-1f466', 'aliases': []}), - ("family_man_man_girl", {'code': '1f468-200d-1f468-200d-1f467', 'aliases': []}), - ("family_man_man_girl_boy", {'code': '1f468-200d-1f468-200d-1f467-200d-1f466', 'aliases': []}), - ("family_man_man_girl_girl", {'code': '1f468-200d-1f468-200d-1f467-200d-1f467', 'aliases': []}), - ("family_man_woman_boy", {'code': '1f468-200d-1f469-200d-1f466', 'aliases': []}), - ("family_man_woman_boy_boy", {'code': '1f468-200d-1f469-200d-1f466-200d-1f466', 'aliases': []}), - ("family_man_woman_girl", {'code': '1f468-200d-1f469-200d-1f467', 'aliases': []}), - ("family_man_woman_girl_boy", {'code': '1f468-200d-1f469-200d-1f467-200d-1f466', 'aliases': []}), - ("family_man_woman_girl_girl", {'code': '1f468-200d-1f469-200d-1f467-200d-1f467', 'aliases': []}), - ("family_woman_boy", {'code': '1f469-200d-1f466', 'aliases': []}), - ("family_woman_boy_boy", {'code': '1f469-200d-1f466-200d-1f466', 'aliases': []}), - ("family_woman_girl", {'code': '1f469-200d-1f467', 'aliases': []}), - ("family_woman_girl_boy", {'code': '1f469-200d-1f467-200d-1f466', 'aliases': []}), - ("family_woman_girl_girl", {'code': '1f469-200d-1f467-200d-1f467', 'aliases': []}), - ("family_woman_woman_boy", {'code': '1f469-200d-1f469-200d-1f466', 'aliases': []}), - ("family_woman_woman_boy_boy", {'code': '1f469-200d-1f469-200d-1f466-200d-1f466', 'aliases': []}), - ("family_woman_woman_girl", {'code': '1f469-200d-1f469-200d-1f467', 'aliases': []}), - ("family_woman_woman_girl_boy", {'code': '1f469-200d-1f469-200d-1f467-200d-1f466', 'aliases': []}), - ("family_woman_woman_girl_girl", {'code': '1f469-200d-1f469-200d-1f467-200d-1f467', 'aliases': []}), - ("farmer", {'code': '1f9d1-200d-1f33e', 'aliases': []}), - ("fast_forward", {'code': '23e9', 'aliases': []}), - ("fax", {'code': '1f4e0', 'aliases': []}), - ("fear", {'code': '1f628', 'aliases': ['scared', 'shock']}), - ("feather", {'code': '1fab6', 'aliases': ['flight', 'light', 'plumage']}), - ("female_sign", {'code': '2640', 'aliases': []}), - ("fencing", {'code': '1f93a', 'aliases': []}), - ("ferris_wheel", {'code': '1f3a1', 'aliases': []}), - ("ferry", {'code': '26f4', 'aliases': []}), - ("field_hockey", {'code': '1f3d1', 'aliases': []}), - ("file_cabinet", {'code': '1f5c4', 'aliases': []}), - ("film", {'code': '1f39e', 'aliases': []}), - ("fingers_crossed", {'code': '1f91e', 'aliases': []}), - ("fire", {'code': '1f525', 'aliases': ['lit', 'hot', 'flame']}), - ("fire_extinguisher", {'code': '1f9ef', 'aliases': ['extinguish', 'quench']}), - ("fire_truck", {'code': '1f692', 'aliases': ['fire_engine']}), - ("firecracker", {'code': '1f9e8', 'aliases': ['dynamite', 'explosive']}), - ("firefighter", {'code': '1f9d1-200d-1f692', 'aliases': []}), - ("fireworks", {'code': '1f386', 'aliases': []}), - ("first_place", {'code': '1f947', 'aliases': ['gold', 'number_one']}), - ("first_quarter_moon", {'code': '1f313', 'aliases': []}), - ("fish", {'code': '1f41f', 'aliases': []}), - ("fishing", {'code': '1f3a3', 'aliases': []}), - ("fist", {'code': '270a', 'aliases': ['power']}), - ("fist_bump", {'code': '1f44a', 'aliases': ['punch']}), - ("five", {'code': '0035-20e3', 'aliases': []}), - ("fixing", {'code': '1f527', 'aliases': ['wrench']}), - ("flag_afghanistan", {'code': '1f1e6-1f1eb', 'aliases': []}), - ("flag_albania", {'code': '1f1e6-1f1f1', 'aliases': []}), - ("flag_algeria", {'code': '1f1e9-1f1ff', 'aliases': []}), - ("flag_american_samoa", {'code': '1f1e6-1f1f8', 'aliases': []}), - ("flag_andorra", {'code': '1f1e6-1f1e9', 'aliases': []}), - ("flag_angola", {'code': '1f1e6-1f1f4', 'aliases': []}), - ("flag_anguilla", {'code': '1f1e6-1f1ee', 'aliases': []}), - ("flag_antarctica", {'code': '1f1e6-1f1f6', 'aliases': []}), - ("flag_antigua_and_barbuda", {'code': '1f1e6-1f1ec', 'aliases': []}), - ("flag_argentina", {'code': '1f1e6-1f1f7', 'aliases': []}), - ("flag_armenia", {'code': '1f1e6-1f1f2', 'aliases': []}), - ("flag_aruba", {'code': '1f1e6-1f1fc', 'aliases': []}), - ("flag_ascension_island", {'code': '1f1e6-1f1e8', 'aliases': []}), - ("flag_australia", {'code': '1f1e6-1f1fa', 'aliases': []}), - ("flag_austria", {'code': '1f1e6-1f1f9', 'aliases': []}), - ("flag_azerbaijan", {'code': '1f1e6-1f1ff', 'aliases': []}), - ("flag_bahamas", {'code': '1f1e7-1f1f8', 'aliases': []}), - ("flag_bahrain", {'code': '1f1e7-1f1ed', 'aliases': []}), - ("flag_bangladesh", {'code': '1f1e7-1f1e9', 'aliases': []}), - ("flag_barbados", {'code': '1f1e7-1f1e7', 'aliases': []}), - ("flag_belarus", {'code': '1f1e7-1f1fe', 'aliases': []}), - ("flag_belgium", {'code': '1f1e7-1f1ea', 'aliases': []}), - ("flag_belize", {'code': '1f1e7-1f1ff', 'aliases': []}), - ("flag_benin", {'code': '1f1e7-1f1ef', 'aliases': []}), - ("flag_bermuda", {'code': '1f1e7-1f1f2', 'aliases': []}), - ("flag_bhutan", {'code': '1f1e7-1f1f9', 'aliases': []}), - ("flag_bolivia", {'code': '1f1e7-1f1f4', 'aliases': []}), - ("flag_bosnia_and_herzegovina", {'code': '1f1e7-1f1e6', 'aliases': []}), - ("flag_botswana", {'code': '1f1e7-1f1fc', 'aliases': []}), - ("flag_bouvet_island", {'code': '1f1e7-1f1fb', 'aliases': []}), - ("flag_brazil", {'code': '1f1e7-1f1f7', 'aliases': []}), - ("flag_british_indian_ocean_territory", {'code': '1f1ee-1f1f4', 'aliases': []}), - ("flag_british_virgin_islands", {'code': '1f1fb-1f1ec', 'aliases': []}), - ("flag_brunei", {'code': '1f1e7-1f1f3', 'aliases': []}), - ("flag_bulgaria", {'code': '1f1e7-1f1ec', 'aliases': []}), - ("flag_burkina_faso", {'code': '1f1e7-1f1eb', 'aliases': []}), - ("flag_burundi", {'code': '1f1e7-1f1ee', 'aliases': []}), - ("flag_cambodia", {'code': '1f1f0-1f1ed', 'aliases': []}), - ("flag_cameroon", {'code': '1f1e8-1f1f2', 'aliases': []}), - ("flag_canada", {'code': '1f1e8-1f1e6', 'aliases': []}), - ("flag_canary_islands", {'code': '1f1ee-1f1e8', 'aliases': []}), - ("flag_cape_verde", {'code': '1f1e8-1f1fb', 'aliases': []}), - ("flag_caribbean_netherlands", {'code': '1f1e7-1f1f6', 'aliases': []}), - ("flag_cayman_islands", {'code': '1f1f0-1f1fe', 'aliases': []}), - ("flag_central_african_republic", {'code': '1f1e8-1f1eb', 'aliases': []}), - ("flag_ceuta_and_melilla", {'code': '1f1ea-1f1e6', 'aliases': []}), - ("flag_chad", {'code': '1f1f9-1f1e9', 'aliases': []}), - ("flag_chile", {'code': '1f1e8-1f1f1', 'aliases': []}), - ("flag_china", {'code': '1f1e8-1f1f3', 'aliases': []}), - ("flag_christmas_island", {'code': '1f1e8-1f1fd', 'aliases': []}), - ("flag_clipperton_island", {'code': '1f1e8-1f1f5', 'aliases': []}), - ("flag_cocos_keeling_islands", {'code': '1f1e8-1f1e8', 'aliases': []}), - ("flag_colombia", {'code': '1f1e8-1f1f4', 'aliases': []}), - ("flag_comoros", {'code': '1f1f0-1f1f2', 'aliases': []}), - ("flag_congo_brazzaville", {'code': '1f1e8-1f1ec', 'aliases': []}), - ("flag_congo_kinshasa", {'code': '1f1e8-1f1e9', 'aliases': []}), - ("flag_cook_islands", {'code': '1f1e8-1f1f0', 'aliases': []}), - ("flag_costa_rica", {'code': '1f1e8-1f1f7', 'aliases': []}), - ("flag_croatia", {'code': '1f1ed-1f1f7', 'aliases': []}), - ("flag_cuba", {'code': '1f1e8-1f1fa', 'aliases': []}), - ("flag_curaçao", {'code': '1f1e8-1f1fc', 'aliases': ['flag_curacao']}), - ("flag_cyprus", {'code': '1f1e8-1f1fe', 'aliases': []}), - ("flag_czechia", {'code': '1f1e8-1f1ff', 'aliases': []}), - ("flag_côte_divoire", {'code': '1f1e8-1f1ee', 'aliases': ['flag_cote_divoire']}), - ("flag_denmark", {'code': '1f1e9-1f1f0', 'aliases': []}), - ("flag_diego_garcia", {'code': '1f1e9-1f1ec', 'aliases': []}), - ("flag_djibouti", {'code': '1f1e9-1f1ef', 'aliases': []}), - ("flag_dominica", {'code': '1f1e9-1f1f2', 'aliases': []}), - ("flag_dominican_republic", {'code': '1f1e9-1f1f4', 'aliases': []}), - ("flag_ecuador", {'code': '1f1ea-1f1e8', 'aliases': []}), - ("flag_egypt", {'code': '1f1ea-1f1ec', 'aliases': []}), - ("flag_el_salvador", {'code': '1f1f8-1f1fb', 'aliases': []}), - ("flag_england", {'code': '1f3f4-e0067-e0062-e0065-e006e-e0067-e007f', 'aliases': []}), - ("flag_equatorial_guinea", {'code': '1f1ec-1f1f6', 'aliases': []}), - ("flag_eritrea", {'code': '1f1ea-1f1f7', 'aliases': []}), - ("flag_estonia", {'code': '1f1ea-1f1ea', 'aliases': []}), - ("flag_eswatini", {'code': '1f1f8-1f1ff', 'aliases': []}), - ("flag_ethiopia", {'code': '1f1ea-1f1f9', 'aliases': []}), - ("flag_european_union", {'code': '1f1ea-1f1fa', 'aliases': []}), - ("flag_falkland_islands", {'code': '1f1eb-1f1f0', 'aliases': []}), - ("flag_faroe_islands", {'code': '1f1eb-1f1f4', 'aliases': []}), - ("flag_fiji", {'code': '1f1eb-1f1ef', 'aliases': []}), - ("flag_finland", {'code': '1f1eb-1f1ee', 'aliases': []}), - ("flag_france", {'code': '1f1eb-1f1f7', 'aliases': []}), - ("flag_french_guiana", {'code': '1f1ec-1f1eb', 'aliases': []}), - ("flag_french_polynesia", {'code': '1f1f5-1f1eb', 'aliases': []}), - ("flag_french_southern_territories", {'code': '1f1f9-1f1eb', 'aliases': []}), - ("flag_gabon", {'code': '1f1ec-1f1e6', 'aliases': []}), - ("flag_gambia", {'code': '1f1ec-1f1f2', 'aliases': []}), - ("flag_georgia", {'code': '1f1ec-1f1ea', 'aliases': []}), - ("flag_germany", {'code': '1f1e9-1f1ea', 'aliases': []}), - ("flag_ghana", {'code': '1f1ec-1f1ed', 'aliases': []}), - ("flag_gibraltar", {'code': '1f1ec-1f1ee', 'aliases': []}), - ("flag_greece", {'code': '1f1ec-1f1f7', 'aliases': []}), - ("flag_greenland", {'code': '1f1ec-1f1f1', 'aliases': []}), - ("flag_grenada", {'code': '1f1ec-1f1e9', 'aliases': []}), - ("flag_guadeloupe", {'code': '1f1ec-1f1f5', 'aliases': []}), - ("flag_guam", {'code': '1f1ec-1f1fa', 'aliases': []}), - ("flag_guatemala", {'code': '1f1ec-1f1f9', 'aliases': []}), - ("flag_guernsey", {'code': '1f1ec-1f1ec', 'aliases': []}), - ("flag_guinea", {'code': '1f1ec-1f1f3', 'aliases': []}), - ("flag_guinea_bissau", {'code': '1f1ec-1f1fc', 'aliases': []}), - ("flag_guyana", {'code': '1f1ec-1f1fe', 'aliases': []}), - ("flag_haiti", {'code': '1f1ed-1f1f9', 'aliases': []}), - ("flag_heard_and_mcdonald_islands", {'code': '1f1ed-1f1f2', 'aliases': []}), - ("flag_honduras", {'code': '1f1ed-1f1f3', 'aliases': []}), - ("flag_hong_kong_sar_china", {'code': '1f1ed-1f1f0', 'aliases': []}), - ("flag_hungary", {'code': '1f1ed-1f1fa', 'aliases': []}), - ("flag_iceland", {'code': '1f1ee-1f1f8', 'aliases': []}), - ("flag_india", {'code': '1f1ee-1f1f3', 'aliases': []}), - ("flag_indonesia", {'code': '1f1ee-1f1e9', 'aliases': []}), - ("flag_iran", {'code': '1f1ee-1f1f7', 'aliases': []}), - ("flag_iraq", {'code': '1f1ee-1f1f6', 'aliases': []}), - ("flag_ireland", {'code': '1f1ee-1f1ea', 'aliases': []}), - ("flag_isle_of_man", {'code': '1f1ee-1f1f2', 'aliases': []}), - ("flag_israel", {'code': '1f1ee-1f1f1', 'aliases': []}), - ("flag_italy", {'code': '1f1ee-1f1f9', 'aliases': []}), - ("flag_jamaica", {'code': '1f1ef-1f1f2', 'aliases': []}), - ("flag_japan", {'code': '1f1ef-1f1f5', 'aliases': []}), - ("flag_jersey", {'code': '1f1ef-1f1ea', 'aliases': []}), - ("flag_jordan", {'code': '1f1ef-1f1f4', 'aliases': []}), - ("flag_kazakhstan", {'code': '1f1f0-1f1ff', 'aliases': []}), - ("flag_kenya", {'code': '1f1f0-1f1ea', 'aliases': []}), - ("flag_kiribati", {'code': '1f1f0-1f1ee', 'aliases': []}), - ("flag_kosovo", {'code': '1f1fd-1f1f0', 'aliases': []}), - ("flag_kuwait", {'code': '1f1f0-1f1fc', 'aliases': []}), - ("flag_kyrgyzstan", {'code': '1f1f0-1f1ec', 'aliases': []}), - ("flag_laos", {'code': '1f1f1-1f1e6', 'aliases': []}), - ("flag_latvia", {'code': '1f1f1-1f1fb', 'aliases': []}), - ("flag_lebanon", {'code': '1f1f1-1f1e7', 'aliases': []}), - ("flag_lesotho", {'code': '1f1f1-1f1f8', 'aliases': []}), - ("flag_liberia", {'code': '1f1f1-1f1f7', 'aliases': []}), - ("flag_libya", {'code': '1f1f1-1f1fe', 'aliases': []}), - ("flag_liechtenstein", {'code': '1f1f1-1f1ee', 'aliases': []}), - ("flag_lithuania", {'code': '1f1f1-1f1f9', 'aliases': []}), - ("flag_luxembourg", {'code': '1f1f1-1f1fa', 'aliases': []}), - ("flag_macao_sar_china", {'code': '1f1f2-1f1f4', 'aliases': []}), - ("flag_madagascar", {'code': '1f1f2-1f1ec', 'aliases': []}), - ("flag_malawi", {'code': '1f1f2-1f1fc', 'aliases': []}), - ("flag_malaysia", {'code': '1f1f2-1f1fe', 'aliases': []}), - ("flag_maldives", {'code': '1f1f2-1f1fb', 'aliases': []}), - ("flag_mali", {'code': '1f1f2-1f1f1', 'aliases': []}), - ("flag_malta", {'code': '1f1f2-1f1f9', 'aliases': []}), - ("flag_marshall_islands", {'code': '1f1f2-1f1ed', 'aliases': []}), - ("flag_martinique", {'code': '1f1f2-1f1f6', 'aliases': []}), - ("flag_mauritania", {'code': '1f1f2-1f1f7', 'aliases': []}), - ("flag_mauritius", {'code': '1f1f2-1f1fa', 'aliases': []}), - ("flag_mayotte", {'code': '1f1fe-1f1f9', 'aliases': []}), - ("flag_mexico", {'code': '1f1f2-1f1fd', 'aliases': []}), - ("flag_micronesia", {'code': '1f1eb-1f1f2', 'aliases': []}), - ("flag_moldova", {'code': '1f1f2-1f1e9', 'aliases': []}), - ("flag_monaco", {'code': '1f1f2-1f1e8', 'aliases': []}), - ("flag_mongolia", {'code': '1f1f2-1f1f3', 'aliases': []}), - ("flag_montenegro", {'code': '1f1f2-1f1ea', 'aliases': []}), - ("flag_montserrat", {'code': '1f1f2-1f1f8', 'aliases': []}), - ("flag_morocco", {'code': '1f1f2-1f1e6', 'aliases': []}), - ("flag_mozambique", {'code': '1f1f2-1f1ff', 'aliases': []}), - ("flag_myanmar_burma", {'code': '1f1f2-1f1f2', 'aliases': []}), - ("flag_namibia", {'code': '1f1f3-1f1e6', 'aliases': []}), - ("flag_nauru", {'code': '1f1f3-1f1f7', 'aliases': []}), - ("flag_nepal", {'code': '1f1f3-1f1f5', 'aliases': []}), - ("flag_netherlands", {'code': '1f1f3-1f1f1', 'aliases': []}), - ("flag_new_caledonia", {'code': '1f1f3-1f1e8', 'aliases': []}), - ("flag_new_zealand", {'code': '1f1f3-1f1ff', 'aliases': []}), - ("flag_nicaragua", {'code': '1f1f3-1f1ee', 'aliases': []}), - ("flag_niger", {'code': '1f1f3-1f1ea', 'aliases': []}), - ("flag_nigeria", {'code': '1f1f3-1f1ec', 'aliases': []}), - ("flag_niue", {'code': '1f1f3-1f1fa', 'aliases': []}), - ("flag_norfolk_island", {'code': '1f1f3-1f1eb', 'aliases': []}), - ("flag_north_korea", {'code': '1f1f0-1f1f5', 'aliases': []}), - ("flag_north_macedonia", {'code': '1f1f2-1f1f0', 'aliases': []}), - ("flag_northern_mariana_islands", {'code': '1f1f2-1f1f5', 'aliases': []}), - ("flag_norway", {'code': '1f1f3-1f1f4', 'aliases': []}), - ("flag_oman", {'code': '1f1f4-1f1f2', 'aliases': []}), - ("flag_pakistan", {'code': '1f1f5-1f1f0', 'aliases': []}), - ("flag_palau", {'code': '1f1f5-1f1fc', 'aliases': []}), - ("flag_palestinian_territories", {'code': '1f1f5-1f1f8', 'aliases': []}), - ("flag_panama", {'code': '1f1f5-1f1e6', 'aliases': []}), - ("flag_papua_new_guinea", {'code': '1f1f5-1f1ec', 'aliases': []}), - ("flag_paraguay", {'code': '1f1f5-1f1fe', 'aliases': []}), - ("flag_peru", {'code': '1f1f5-1f1ea', 'aliases': []}), - ("flag_philippines", {'code': '1f1f5-1f1ed', 'aliases': []}), - ("flag_pitcairn_islands", {'code': '1f1f5-1f1f3', 'aliases': []}), - ("flag_poland", {'code': '1f1f5-1f1f1', 'aliases': []}), - ("flag_portugal", {'code': '1f1f5-1f1f9', 'aliases': []}), - ("flag_puerto_rico", {'code': '1f1f5-1f1f7', 'aliases': []}), - ("flag_qatar", {'code': '1f1f6-1f1e6', 'aliases': []}), - ("flag_romania", {'code': '1f1f7-1f1f4', 'aliases': []}), - ("flag_russia", {'code': '1f1f7-1f1fa', 'aliases': []}), - ("flag_rwanda", {'code': '1f1f7-1f1fc', 'aliases': []}), - ("flag_réunion", {'code': '1f1f7-1f1ea', 'aliases': ['flag_reunion']}), - ("flag_samoa", {'code': '1f1fc-1f1f8', 'aliases': []}), - ("flag_san_marino", {'code': '1f1f8-1f1f2', 'aliases': []}), - ("flag_saudi_arabia", {'code': '1f1f8-1f1e6', 'aliases': []}), - ("flag_scotland", {'code': '1f3f4-e0067-e0062-e0073-e0063-e0074-e007f', 'aliases': []}), - ("flag_senegal", {'code': '1f1f8-1f1f3', 'aliases': []}), - ("flag_serbia", {'code': '1f1f7-1f1f8', 'aliases': []}), - ("flag_seychelles", {'code': '1f1f8-1f1e8', 'aliases': []}), - ("flag_sierra_leone", {'code': '1f1f8-1f1f1', 'aliases': []}), - ("flag_singapore", {'code': '1f1f8-1f1ec', 'aliases': []}), - ("flag_sint_maarten", {'code': '1f1f8-1f1fd', 'aliases': []}), - ("flag_slovakia", {'code': '1f1f8-1f1f0', 'aliases': []}), - ("flag_slovenia", {'code': '1f1f8-1f1ee', 'aliases': []}), - ("flag_solomon_islands", {'code': '1f1f8-1f1e7', 'aliases': []}), - ("flag_somalia", {'code': '1f1f8-1f1f4', 'aliases': []}), - ("flag_south_africa", {'code': '1f1ff-1f1e6', 'aliases': []}), - ("flag_south_georgia_and_south_sandwich_islands", {'code': '1f1ec-1f1f8', 'aliases': []}), - ("flag_south_korea", {'code': '1f1f0-1f1f7', 'aliases': []}), - ("flag_south_sudan", {'code': '1f1f8-1f1f8', 'aliases': []}), - ("flag_spain", {'code': '1f1ea-1f1f8', 'aliases': []}), - ("flag_sri_lanka", {'code': '1f1f1-1f1f0', 'aliases': []}), - ("flag_st_barthélemy", {'code': '1f1e7-1f1f1', 'aliases': ['flag_st_barthelemy']}), - ("flag_st_helena", {'code': '1f1f8-1f1ed', 'aliases': []}), - ("flag_st_kitts_and_nevis", {'code': '1f1f0-1f1f3', 'aliases': []}), - ("flag_st_lucia", {'code': '1f1f1-1f1e8', 'aliases': []}), - ("flag_st_martin", {'code': '1f1f2-1f1eb', 'aliases': []}), - ("flag_st_pierre_and_miquelon", {'code': '1f1f5-1f1f2', 'aliases': []}), - ("flag_st_vincent_and_grenadines", {'code': '1f1fb-1f1e8', 'aliases': []}), - ("flag_sudan", {'code': '1f1f8-1f1e9', 'aliases': []}), - ("flag_suriname", {'code': '1f1f8-1f1f7', 'aliases': []}), - ("flag_svalbard_and_jan_mayen", {'code': '1f1f8-1f1ef', 'aliases': []}), - ("flag_sweden", {'code': '1f1f8-1f1ea', 'aliases': []}), - ("flag_switzerland", {'code': '1f1e8-1f1ed', 'aliases': []}), - ("flag_syria", {'code': '1f1f8-1f1fe', 'aliases': []}), - ("flag_são_tomé_and_príncipe", {'code': '1f1f8-1f1f9', 'aliases': ['flag_sao_tome_and_principe']}), - ("flag_taiwan", {'code': '1f1f9-1f1fc', 'aliases': []}), - ("flag_tajikistan", {'code': '1f1f9-1f1ef', 'aliases': []}), - ("flag_tanzania", {'code': '1f1f9-1f1ff', 'aliases': []}), - ("flag_thailand", {'code': '1f1f9-1f1ed', 'aliases': []}), - ("flag_timor_leste", {'code': '1f1f9-1f1f1', 'aliases': []}), - ("flag_togo", {'code': '1f1f9-1f1ec', 'aliases': []}), - ("flag_tokelau", {'code': '1f1f9-1f1f0', 'aliases': []}), - ("flag_tonga", {'code': '1f1f9-1f1f4', 'aliases': []}), - ("flag_trinidad_and_tobago", {'code': '1f1f9-1f1f9', 'aliases': []}), - ("flag_tristan_da_cunha", {'code': '1f1f9-1f1e6', 'aliases': []}), - ("flag_tunisia", {'code': '1f1f9-1f1f3', 'aliases': []}), - ("flag_turkey", {'code': '1f1f9-1f1f7', 'aliases': []}), - ("flag_turkmenistan", {'code': '1f1f9-1f1f2', 'aliases': []}), - ("flag_turks_and_caicos_islands", {'code': '1f1f9-1f1e8', 'aliases': []}), - ("flag_tuvalu", {'code': '1f1f9-1f1fb', 'aliases': []}), - ("flag_uganda", {'code': '1f1fa-1f1ec', 'aliases': []}), - ("flag_ukraine", {'code': '1f1fa-1f1e6', 'aliases': []}), - ("flag_united_arab_emirates", {'code': '1f1e6-1f1ea', 'aliases': []}), - ("flag_united_kingdom", {'code': '1f1ec-1f1e7', 'aliases': []}), - ("flag_united_nations", {'code': '1f1fa-1f1f3', 'aliases': []}), - ("flag_united_states", {'code': '1f1fa-1f1f8', 'aliases': []}), - ("flag_uruguay", {'code': '1f1fa-1f1fe', 'aliases': []}), - ("flag_us_outlying_islands", {'code': '1f1fa-1f1f2', 'aliases': []}), - ("flag_us_virgin_islands", {'code': '1f1fb-1f1ee', 'aliases': []}), - ("flag_uzbekistan", {'code': '1f1fa-1f1ff', 'aliases': []}), - ("flag_vanuatu", {'code': '1f1fb-1f1fa', 'aliases': []}), - ("flag_vatican_city", {'code': '1f1fb-1f1e6', 'aliases': []}), - ("flag_venezuela", {'code': '1f1fb-1f1ea', 'aliases': []}), - ("flag_vietnam", {'code': '1f1fb-1f1f3', 'aliases': []}), - ("flag_wales", {'code': '1f3f4-e0067-e0062-e0077-e006c-e0073-e007f', 'aliases': []}), - ("flag_wallis_and_futuna", {'code': '1f1fc-1f1eb', 'aliases': []}), - ("flag_western_sahara", {'code': '1f1ea-1f1ed', 'aliases': []}), - ("flag_yemen", {'code': '1f1fe-1f1ea', 'aliases': []}), - ("flag_zambia", {'code': '1f1ff-1f1f2', 'aliases': []}), - ("flag_zimbabwe", {'code': '1f1ff-1f1fc', 'aliases': []}), - ("flag_åland_islands", {'code': '1f1e6-1f1fd', 'aliases': ['flag_aland_islands']}), - ("flamingo", {'code': '1f9a9', 'aliases': ['flamboyant']}), - ("flashlight", {'code': '1f526', 'aliases': []}), - ("flat_shoe", {'code': '1f97f', 'aliases': ['ballet_flat', 'slip_on', 'slipper']}), - ("flatbread", {'code': '1fad3', 'aliases': ['arepa', 'lavash', 'naan', 'pita']}), - ("fleur_de_lis", {'code': '269c', 'aliases': []}), - ("floppy_disk", {'code': '1f4be', 'aliases': []}), - ("flushed", {'code': '1f633', 'aliases': ['embarrassed', 'blushing']}), - ("fly", {'code': '1fab0', 'aliases': ['maggot', 'rotting']}), - ("flying_disc", {'code': '1f94f', 'aliases': ['ultimate']}), - ("flying_saucer", {'code': '1f6f8', 'aliases': []}), - ("fog", {'code': '1f32b', 'aliases': ['hazy']}), - ("foggy", {'code': '1f301', 'aliases': []}), - ("folder", {'code': '1f4c2', 'aliases': []}), - ("fondue", {'code': '1fad5', 'aliases': ['melted', 'swiss']}), - ("food", {'code': '1f372', 'aliases': ['soup', 'stew']}), - ("foot", {'code': '1f9b6', 'aliases': ['stomp']}), - ("football", {'code': '26bd', 'aliases': ['soccer']}), - ("footprints", {'code': '1f463', 'aliases': ['feet']}), - ("fork_and_knife", {'code': '1f374', 'aliases': ['eating_utensils']}), - ("fortune_cookie", {'code': '1f960', 'aliases': ['prophecy']}), - ("forward", {'code': '21aa', 'aliases': ['right_hook']}), - ("fountain", {'code': '26f2', 'aliases': []}), - ("fountain_pen", {'code': '1f58b', 'aliases': []}), - ("four", {'code': '0034-20e3', 'aliases': []}), - ("fox", {'code': '1f98a', 'aliases': []}), - ("free", {'code': '1f193', 'aliases': []}), - ("fries", {'code': '1f35f', 'aliases': []}), - ("frog", {'code': '1f438', 'aliases': []}), - ("frosty", {'code': '26c4', 'aliases': []}), - ("frown", {'code': '1f641', 'aliases': ['slight_frown']}), - ("frowning", {'code': '1f626', 'aliases': []}), - ("fuel_pump", {'code': '26fd', 'aliases': ['gas_pump', 'petrol_pump']}), - ("full_moon", {'code': '1f315', 'aliases': []}), - ("funeral_urn", {'code': '26b1', 'aliases': ['cremation']}), - ("garlic", {'code': '1f9c4', 'aliases': []}), - ("gear", {'code': '2699', 'aliases': ['settings', 'mechanical', 'engineer']}), - ("gem", {'code': '1f48e', 'aliases': ['crystal']}), - ("gemini", {'code': '264a', 'aliases': []}), - ("genie", {'code': '1f9de', 'aliases': []}), - ("ghost", {'code': '1f47b', 'aliases': ['boo', 'spooky', 'haunted']}), - ("gift", {'code': '1f381', 'aliases': ['present']}), - ("gift_heart", {'code': '1f49d', 'aliases': []}), - ("giraffe", {'code': '1f992', 'aliases': ['spots']}), - ("girl", {'code': '1f467', 'aliases': []}), - ("glasses", {'code': '1f453', 'aliases': ['spectacles']}), - ("gloves", {'code': '1f9e4', 'aliases': []}), - ("glowing_star", {'code': '1f31f', 'aliases': []}), - ("goat", {'code': '1f410', 'aliases': []}), - ("goblin", {'code': '1f47a', 'aliases': []}), - ("goggles", {'code': '1f97d', 'aliases': ['eye_protection', 'swimming', 'welding']}), - ("gold_record", {'code': '1f4bd', 'aliases': ['minidisc']}), - ("golf", {'code': '1f3cc', 'aliases': []}), - ("gondola", {'code': '1f6a0', 'aliases': ['mountain_cableway']}), - ("goodnight", {'code': '1f31b', 'aliases': []}), - ("gooooooooal", {'code': '1f945', 'aliases': ['goal']}), - ("gorilla", {'code': '1f98d', 'aliases': []}), - ("graduate", {'code': '1f393', 'aliases': ['mortar_board']}), - ("grapes", {'code': '1f347', 'aliases': []}), - ("green_apple", {'code': '1f34f', 'aliases': []}), - ("green_book", {'code': '1f4d7', 'aliases': []}), - ("green_circle", {'code': '1f7e2', 'aliases': ['green']}), - ("green_heart", {'code': '1f49a', 'aliases': ['envy']}), - ("green_large_square", {'code': '1f7e9', 'aliases': []}), - ("grey_exclamation", {'code': '2755', 'aliases': []}), - ("grey_question", {'code': '2754', 'aliases': []}), - ("grimacing", {'code': '1f62c', 'aliases': ['nervous', 'anxious']}), - ("grinning", {'code': '1f600', 'aliases': ['happy']}), - ("grinning_face_with_smiling_eyes", {'code': '1f601', 'aliases': []}), - ("gua_pi_mao", {'code': '1f472', 'aliases': []}), - ("guard", {'code': '1f482', 'aliases': []}), - ("guide_dog", {'code': '1f9ae', 'aliases': ['guide']}), - ("guitar", {'code': '1f3b8', 'aliases': []}), - ("gun", {'code': '1f52b', 'aliases': []}), - ("haircut", {'code': '1f487', 'aliases': []}), - ("hamburger", {'code': '1f354', 'aliases': []}), - ("hammer", {'code': '1f528', 'aliases': ['maintenance', 'handyman', 'handywoman']}), - ("hamsa", {'code': '1faac', 'aliases': ['amulet', 'fatima', 'mary', 'miriam', 'protection']}), - ("hamster", {'code': '1f439', 'aliases': []}), - ("hand", {'code': '270b', 'aliases': ['raised_hand']}), - ("hand_with_index_finger_and_thumb_crossed", {'code': '1faf0', 'aliases': ['expensive', 'snap']}), - ("handbag", {'code': '1f45c', 'aliases': []}), - ("handball", {'code': '1f93e', 'aliases': []}), - ("handshake", {'code': '1f91d', 'aliases': ['done_deal']}), - ("harvest", {'code': '1f33e', 'aliases': ['ear_of_rice']}), - ("hash", {'code': '0023-20e3', 'aliases': []}), - ("hat", {'code': '1f452', 'aliases': []}), - ("hatching", {'code': '1f423', 'aliases': ['hatching_chick']}), - ("heading_down", {'code': '2935', 'aliases': []}), - ("heading_up", {'code': '2934', 'aliases': []}), - ("headlines", {'code': '1f4f0', 'aliases': []}), - ("headphones", {'code': '1f3a7', 'aliases': []}), - ("headstone", {'code': '1faa6', 'aliases': ['cemetery', 'graveyard', 'tombstone']}), - ("health_worker", {'code': '1f9d1-200d-2695', 'aliases': []}), - ("hear_no_evil", {'code': '1f649', 'aliases': []}), - ("heart", {'code': '2764', 'aliases': ['love', 'love_you']}), - ("heart_box", {'code': '1f49f', 'aliases': []}), - ("heart_exclamation", {'code': '2763', 'aliases': []}), - ("heart_eyes", {'code': '1f60d', 'aliases': ['in_love']}), - ("heart_eyes_cat", {'code': '1f63b', 'aliases': []}), - ("heart_hands", {'code': '1faf6', 'aliases': []}), - ("heart_kiss", {'code': '1f618', 'aliases': ['blow_a_kiss']}), - ("heart_on_fire", {'code': '2764-200d-1f525', 'aliases': ['burn', 'lust', 'sacred_heart']}), - ("heart_pulse", {'code': '1f497', 'aliases': ['growing_heart']}), - ("heartbeat", {'code': '1f493', 'aliases': []}), - ("hearts", {'code': '2665', 'aliases': []}), - ("heavy_equals_sign", {'code': '1f7f0', 'aliases': ['equality', 'math']}), - ("hedgehog", {'code': '1f994', 'aliases': ['spiny']}), - ("helicopter", {'code': '1f681', 'aliases': []}), - ("helmet", {'code': '26d1', 'aliases': ['hard_hat', 'rescue_worker', 'safety_first', 'invincible']}), - ("herb", {'code': '1f33f', 'aliases': ['plant']}), - ("hibiscus", {'code': '1f33a', 'aliases': []}), - ("high_five", {'code': '1f590', 'aliases': ['palm']}), - ("high_heels", {'code': '1f460', 'aliases': []}), - ("high_speed_train", {'code': '1f684', 'aliases': []}), - ("high_voltage", {'code': '26a1', 'aliases': ['zap']}), - ("hiking_boot", {'code': '1f97e', 'aliases': ['backpacking', 'hiking']}), - ("hindu_temple", {'code': '1f6d5', 'aliases': ['hindu', 'temple']}), - ("hippopotamus", {'code': '1f99b', 'aliases': ['hippo']}), - ("hole", {'code': '1f573', 'aliases': []}), - ("hole_in_one", {'code': '26f3', 'aliases': []}), - ("holiday_tree", {'code': '1f384', 'aliases': []}), - ("honey", {'code': '1f36f', 'aliases': []}), - ("hook", {'code': '1fa9d', 'aliases': ['crook', 'curve', 'ensnare', 'selling_point']}), - ("horizontal_traffic_light", {'code': '1f6a5', 'aliases': []}), - ("horn", {'code': '1f4ef', 'aliases': []}), - ("horse", {'code': '1f40e', 'aliases': []}), - ("horse_racing", {'code': '1f3c7', 'aliases': ['horse_riding']}), - ("hospital", {'code': '1f3e5', 'aliases': []}), - ("hot_face", {'code': '1f975', 'aliases': ['feverish', 'heat_stroke', 'red_faced', 'sweating']}), - ("hot_pepper", {'code': '1f336', 'aliases': ['chili_pepper']}), - ("hot_springs", {'code': '2668', 'aliases': []}), - ("hotdog", {'code': '1f32d', 'aliases': []}), - ("hotel", {'code': '1f3e8', 'aliases': []}), - ("house", {'code': '1f3e0', 'aliases': []}), - ("houses", {'code': '1f3d8', 'aliases': []}), - ("hug", {'code': '1f917', 'aliases': ['arms_open']}), - ("humpback_whale", {'code': '1f40b', 'aliases': []}), - ("hungry", {'code': '1f37d', 'aliases': ['meal', 'table_setting', 'fork_and_knife_with_plate', 'lets_eat']}), - ("hurt", {'code': '1f915', 'aliases': ['head_bandage', 'injured']}), - ("hushed", {'code': '1f62f', 'aliases': []}), - ("hut", {'code': '1f6d6', 'aliases': ['roundhouse', 'yurt']}), - ("ice", {'code': '1f9ca', 'aliases': ['ice_cube', 'iceberg']}), - ("ice_cream", {'code': '1f368', 'aliases': ['gelato']}), - ("ice_hockey", {'code': '1f3d2', 'aliases': []}), - ("ice_skate", {'code': '26f8', 'aliases': []}), - ("id", {'code': '1f194', 'aliases': []}), - ("identification_card", {'code': '1faaa', 'aliases': ['credentials', 'license', 'security']}), - ("in_bed", {'code': '1f6cc', 'aliases': ['accommodations', 'guestrooms']}), - ("inbox", {'code': '1f4e5', 'aliases': []}), - ("inbox_zero", {'code': '1f4ed', 'aliases': ['empty_mailbox', 'no_mail']}), - ("index_pointing_at_the_viewer", {'code': '1faf5', 'aliases': ['point', 'you']}), - ("infinity", {'code': '267e', 'aliases': ['forever', 'unbounded', 'universal']}), - ("info", {'code': '2139', 'aliases': []}), - ("information_desk_person", {'code': '1f481', 'aliases': ['person_tipping_hand']}), - ("injection", {'code': '1f489', 'aliases': ['syringe']}), - ("innocent", {'code': '1f607', 'aliases': ['halo']}), - ("interrobang", {'code': '2049', 'aliases': []}), - ("island", {'code': '1f3dd', 'aliases': []}), - ("jack-o-lantern", {'code': '1f383', 'aliases': ['pumpkin']}), - ("japan", {'code': '1f5fe', 'aliases': []}), - ("japan_post", {'code': '1f3e3', 'aliases': []}), - ("japanese_acceptable_button", {'code': '1f251', 'aliases': ['accept']}), - ("japanese_application_button", {'code': '1f238', 'aliases': ['u7533']}), - ("japanese_bargain_button", {'code': '1f250', 'aliases': ['ideograph_advantage']}), - ("japanese_congratulations_button", {'code': '3297', 'aliases': ['congratulations']}), - ("japanese_discount_button", {'code': '1f239', 'aliases': ['u5272']}), - ("japanese_free_of_charge_button", {'code': '1f21a', 'aliases': ['u7121']}), - ("japanese_here_button", {'code': '1f201', 'aliases': ['here', 'ココ']}), - ("japanese_monthly_amount_button", {'code': '1f237', 'aliases': ['u6708']}), - ("japanese_no_vacancy_button", {'code': '1f235', 'aliases': ['u6e80']}), - ("japanese_not_free_of_charge_button", {'code': '1f236', 'aliases': ['u6709']}), - ("japanese_open_for_business_button", {'code': '1f23a', 'aliases': ['u55b6']}), - ("japanese_passing_grade_button", {'code': '1f234', 'aliases': ['u5408']}), - ("japanese_prohibited_button", {'code': '1f232', 'aliases': ['u7981']}), - ("japanese_reserved_button", {'code': '1f22f', 'aliases': ['reserved', '指']}), - ("japanese_secret_button", {'code': '3299', 'aliases': []}), - ("japanese_service_charge_button", {'code': '1f202', 'aliases': ['service_charge', 'サ']}), - ("japanese_vacancy_button", {'code': '1f233', 'aliases': ['vacancy', '空']}), - ("jar", {'code': '1fad9', 'aliases': ['container', 'sauce', 'store']}), - ("jeans", {'code': '1f456', 'aliases': ['denim']}), - ("joker", {'code': '1f0cf', 'aliases': []}), - ("joy", {'code': '1f602', 'aliases': ['tears', 'laughter_tears']}), - ("joy_cat", {'code': '1f639', 'aliases': []}), - ("joystick", {'code': '1f579', 'aliases': ['arcade']}), - ("judge", {'code': '1f9d1-200d-2696', 'aliases': []}), - ("juggling", {'code': '1f939', 'aliases': []}), - ("justice", {'code': '2696', 'aliases': ['scales', 'balance']}), - ("kaaba", {'code': '1f54b', 'aliases': []}), - ("kangaroo", {'code': '1f998', 'aliases': ['joey', 'jump', 'marsupial']}), - ("key", {'code': '1f511', 'aliases': []}), - ("keyboard", {'code': '2328', 'aliases': []}), - ("kick_scooter", {'code': '1f6f4', 'aliases': []}), - ("kimono", {'code': '1f458', 'aliases': []}), - ("kiss", {'code': '1f48f', 'aliases': []}), - ("kiss_man_man", {'code': '1f468-200d-2764-200d-1f48b-200d-1f468', 'aliases': []}), - ("kiss_smiling_eyes", {'code': '1f619', 'aliases': []}), - ("kiss_with_blush", {'code': '1f61a', 'aliases': []}), - ("kiss_woman_man", {'code': '1f469-200d-2764-200d-1f48b-200d-1f468', 'aliases': []}), - ("kiss_woman_woman", {'code': '1f469-200d-2764-200d-1f48b-200d-1f469', 'aliases': []}), - ("kissing_cat", {'code': '1f63d', 'aliases': []}), - ("kissing_face", {'code': '1f617', 'aliases': []}), - ("kite", {'code': '1fa81', 'aliases': ['soar']}), - ("kitten", {'code': '1f431', 'aliases': []}), - ("kiwi", {'code': '1f95d', 'aliases': []}), - ("knife", {'code': '1f52a', 'aliases': ['hocho', 'betrayed']}), - ("knot", {'code': '1faa2', 'aliases': ['rope', 'tangled', 'twine', 'twist']}), - ("koala", {'code': '1f428', 'aliases': []}), - ("lab_coat", {'code': '1f97c', 'aliases': []}), - ("label", {'code': '1f3f7', 'aliases': ['tag', 'price_tag']}), - ("lacrosse", {'code': '1f94d', 'aliases': []}), - ("ladder", {'code': '1fa9c', 'aliases': ['climb', 'rung', 'step']}), - ("lady_beetle", {'code': '1f41e', 'aliases': ['ladybird', 'ladybug']}), - ("landing", {'code': '1f6ec', 'aliases': ['arrival', 'airplane_arrival']}), - ("landline", {'code': '1f4de', 'aliases': ['home_phone']}), - ("lantern", {'code': '1f3ee', 'aliases': ['izakaya_lantern']}), - ("large_blue_diamond", {'code': '1f537', 'aliases': []}), - ("large_orange_diamond", {'code': '1f536', 'aliases': []}), - ("last_quarter_moon", {'code': '1f317', 'aliases': []}), - ("last_quarter_moon_face", {'code': '1f31c', 'aliases': []}), - ("laughing", {'code': '1f606', 'aliases': ['lol']}), - ("leafy_green", {'code': '1f96c', 'aliases': ['bok_choy', 'cabbage', 'kale', 'lettuce']}), - ("leaves", {'code': '1f343', 'aliases': ['wind', 'fall']}), - ("ledger", {'code': '1f4d2', 'aliases': ['spiral_notebook']}), - ("left", {'code': '2b05', 'aliases': ['west']}), - ("left_fist", {'code': '1f91b', 'aliases': []}), - ("left_right", {'code': '2194', 'aliases': ['swap']}), - ("leftwards_hand", {'code': '1faf2', 'aliases': ['leftward']}), - ("leg", {'code': '1f9b5', 'aliases': ['limb']}), - ("lemon", {'code': '1f34b', 'aliases': []}), - ("leo", {'code': '264c', 'aliases': []}), - ("leopard", {'code': '1f406', 'aliases': []}), - ("levitating", {'code': '1f574', 'aliases': ['hover']}), - ("libra", {'code': '264e', 'aliases': []}), - ("lift", {'code': '1f3cb', 'aliases': ['work_out', 'weight_lift', 'gym']}), - ("light_bulb", {'code': '1f4a1', 'aliases': ['bulb', 'idea']}), - ("light_rail", {'code': '1f688', 'aliases': []}), - ("lightning", {'code': '1f329', 'aliases': ['lightning_storm']}), - ("link", {'code': '1f517', 'aliases': []}), - ("lion", {'code': '1f981', 'aliases': []}), - ("lips", {'code': '1f444', 'aliases': ['mouth']}), - ("lipstick", {'code': '1f484', 'aliases': []}), - ("lipstick_kiss", {'code': '1f48b', 'aliases': []}), - ("living_room", {'code': '1f6cb', 'aliases': ['furniture', 'couch_and_lamp', 'lifestyles']}), - ("lizard", {'code': '1f98e', 'aliases': ['gecko']}), - ("llama", {'code': '1f999', 'aliases': ['alpaca', 'guanaco', 'vicuña', 'wool', 'vicuna']}), - ("lobster", {'code': '1f99e', 'aliases': ['bisque', 'claws', 'seafood']}), - ("locked", {'code': '1f512', 'aliases': []}), - ("locker", {'code': '1f6c5', 'aliases': ['locked_bag']}), - ("lollipop", {'code': '1f36d', 'aliases': []}), - ("long_drum", {'code': '1fa98', 'aliases': ['beat', 'conga', 'rhythm']}), - ("loop", {'code': '27b0', 'aliases': []}), - ("losing_money", {'code': '1f4b8', 'aliases': ['easy_come_easy_go', 'money_with_wings']}), - ("lotion_bottle", {'code': '1f9f4', 'aliases': ['lotion', 'moisturizer', 'shampoo', 'sunscreen']}), - ("lotus", {'code': '1fab7', 'aliases': ['purity']}), - ("louder", {'code': '1f50a', 'aliases': ['sound']}), - ("loudspeaker", {'code': '1f4e2', 'aliases': ['bullhorn']}), - ("love_hotel", {'code': '1f3e9', 'aliases': []}), - ("love_letter", {'code': '1f48c', 'aliases': []}), - ("love_you_gesture", {'code': '1f91f', 'aliases': ['ily']}), - ("low_battery", {'code': '1faab', 'aliases': ['electronic', 'low_energy']}), - ("low_brightness", {'code': '1f505', 'aliases': ['dim']}), - ("lower_left", {'code': '2199', 'aliases': ['south_west']}), - ("lower_right", {'code': '2198', 'aliases': ['south_east']}), - ("lucky", {'code': '1f340', 'aliases': ['four_leaf_clover']}), - ("luggage", {'code': '1f9f3', 'aliases': ['packing', 'travel']}), - ("lungs", {'code': '1fac1', 'aliases': ['breath', 'exhalation', 'inhalation', 'respiration']}), - ("lying", {'code': '1f925', 'aliases': []}), - ("mage", {'code': '1f9d9', 'aliases': []}), - ("magic_wand", {'code': '1fa84', 'aliases': ['magic']}), - ("magnet", {'code': '1f9f2', 'aliases': ['attraction', 'horseshoe']}), - ("magnifying_glass_tilted_right", {'code': '1f50e', 'aliases': ['magnifying']}), - ("mahjong", {'code': '1f004', 'aliases': []}), - ("mail_dropoff", {'code': '1f4ee', 'aliases': []}), - ("mail_received", {'code': '1f4e8', 'aliases': []}), - ("mail_sent", {'code': '1f4e9', 'aliases': ['sealed']}), - ("mailbox", {'code': '1f4eb', 'aliases': []}), - ("male_sign", {'code': '2642', 'aliases': []}), - ("mammoth", {'code': '1f9a3', 'aliases': ['tusk', 'woolly']}), - ("man", {'code': '1f468', 'aliases': []}), - ("man_and_woman_holding_hands", {'code': '1f46b', 'aliases': ['man_and_woman_couple']}), - ("man_artist", {'code': '1f468-200d-1f3a8', 'aliases': []}), - ("man_astronaut", {'code': '1f468-200d-1f680', 'aliases': []}), - ("man_bald", {'code': '1f468-200d-1f9b2', 'aliases': []}), - ("man_beard", {'code': '1f9d4-200d-2642', 'aliases': []}), - ("man_biking", {'code': '1f6b4-200d-2642', 'aliases': []}), - ("man_blond_hair", {'code': '1f471-200d-2642', 'aliases': ['blond_haired_man']}), - ("man_bouncing_ball", {'code': '26f9-fe0f-200d-2642-fe0f', 'aliases': []}), - ("man_bowing", {'code': '1f647-200d-2642', 'aliases': []}), - ("man_cartwheeling", {'code': '1f938-200d-2642', 'aliases': []}), - ("man_climbing", {'code': '1f9d7-200d-2642', 'aliases': []}), - ("man_construction_worker", {'code': '1f477-200d-2642', 'aliases': []}), - ("man_cook", {'code': '1f468-200d-1f373', 'aliases': []}), - ("man_curly_hair", {'code': '1f468-200d-1f9b1', 'aliases': []}), - ("man_detective", {'code': '1f575-fe0f-200d-2642-fe0f', 'aliases': []}), - ("man_elf", {'code': '1f9dd-200d-2642', 'aliases': []}), - ("man_facepalming", {'code': '1f926-200d-2642', 'aliases': []}), - ("man_factory_worker", {'code': '1f468-200d-1f3ed', 'aliases': []}), - ("man_fairy", {'code': '1f9da-200d-2642', 'aliases': []}), - ("man_farmer", {'code': '1f468-200d-1f33e', 'aliases': []}), - ("man_feeding_baby", {'code': '1f468-200d-1f37c', 'aliases': []}), - ("man_firefighter", {'code': '1f468-200d-1f692', 'aliases': []}), - ("man_frowning", {'code': '1f64d-200d-2642', 'aliases': []}), - ("man_genie", {'code': '1f9de-200d-2642', 'aliases': []}), - ("man_gesturing_no", {'code': '1f645-200d-2642', 'aliases': []}), - ("man_gesturing_ok", {'code': '1f646-200d-2642', 'aliases': []}), - ("man_getting_haircut", {'code': '1f487-200d-2642', 'aliases': []}), - ("man_getting_massage", {'code': '1f486-200d-2642', 'aliases': []}), - ("man_golfing", {'code': '1f3cc-fe0f-200d-2642-fe0f', 'aliases': []}), - ("man_guard", {'code': '1f482-200d-2642', 'aliases': []}), - ("man_health_worker", {'code': '1f468-200d-2695', 'aliases': []}), - ("man_in_lotus_position", {'code': '1f9d8-200d-2642', 'aliases': []}), - ("man_in_manual_wheelchair", {'code': '1f468-200d-1f9bd', 'aliases': []}), - ("man_in_motorized_wheelchair", {'code': '1f468-200d-1f9bc', 'aliases': []}), - ("man_in_steamy_room", {'code': '1f9d6-200d-2642', 'aliases': []}), - ("man_in_tuxedo", {'code': '1f935-200d-2642', 'aliases': []}), - ("man_judge", {'code': '1f468-200d-2696', 'aliases': []}), - ("man_juggling", {'code': '1f939-200d-2642', 'aliases': []}), - ("man_kneeling", {'code': '1f9ce-200d-2642', 'aliases': []}), - ("man_lifting_weights", {'code': '1f3cb-fe0f-200d-2642-fe0f', 'aliases': []}), - ("man_mage", {'code': '1f9d9-200d-2642', 'aliases': []}), - ("man_mechanic", {'code': '1f468-200d-1f527', 'aliases': []}), - ("man_mountain_biking", {'code': '1f6b5-200d-2642', 'aliases': []}), - ("man_office_worker", {'code': '1f468-200d-1f4bc', 'aliases': []}), - ("man_pilot", {'code': '1f468-200d-2708', 'aliases': []}), - ("man_playing_handball", {'code': '1f93e-200d-2642', 'aliases': []}), - ("man_playing_water_polo", {'code': '1f93d-200d-2642', 'aliases': []}), - ("man_police_officer", {'code': '1f46e-200d-2642', 'aliases': []}), - ("man_pouting", {'code': '1f64e-200d-2642', 'aliases': []}), - ("man_raising_hand", {'code': '1f64b-200d-2642', 'aliases': []}), - ("man_red_hair", {'code': '1f468-200d-1f9b0', 'aliases': []}), - ("man_rowing_boat", {'code': '1f6a3-200d-2642', 'aliases': []}), - ("man_running", {'code': '1f3c3-200d-2642', 'aliases': []}), - ("man_scientist", {'code': '1f468-200d-1f52c', 'aliases': []}), - ("man_shrugging", {'code': '1f937-200d-2642', 'aliases': []}), - ("man_singer", {'code': '1f468-200d-1f3a4', 'aliases': []}), - ("man_standing", {'code': '1f9cd-200d-2642', 'aliases': []}), - ("man_student", {'code': '1f468-200d-1f393', 'aliases': []}), - ("man_superhero", {'code': '1f9b8-200d-2642', 'aliases': []}), - ("man_supervillain", {'code': '1f9b9-200d-2642', 'aliases': []}), - ("man_surfing", {'code': '1f3c4-200d-2642', 'aliases': []}), - ("man_swimming", {'code': '1f3ca-200d-2642', 'aliases': []}), - ("man_teacher", {'code': '1f468-200d-1f3eb', 'aliases': []}), - ("man_technologist", {'code': '1f468-200d-1f4bb', 'aliases': []}), - ("man_tipping_hand", {'code': '1f481-200d-2642', 'aliases': []}), - ("man_vampire", {'code': '1f9db-200d-2642', 'aliases': []}), - ("man_walking", {'code': '1f6b6-200d-2642', 'aliases': []}), - ("man_wearing_turban", {'code': '1f473-200d-2642', 'aliases': []}), - ("man_white_hair", {'code': '1f468-200d-1f9b3', 'aliases': []}), - ("man_with_veil", {'code': '1f470-200d-2642', 'aliases': []}), - ("man_with_white_cane", {'code': '1f468-200d-1f9af', 'aliases': []}), - ("man_zombie", {'code': '1f9df-200d-2642', 'aliases': []}), - ("mango", {'code': '1f96d', 'aliases': ['fruit']}), - ("mantelpiece_clock", {'code': '1f570', 'aliases': []}), - ("manual_wheelchair", {'code': '1f9bd', 'aliases': []}), - ("map", {'code': '1f5fa', 'aliases': ['world_map', 'road_trip']}), - ("maple_leaf", {'code': '1f341', 'aliases': []}), - ("mask", {'code': '1f637', 'aliases': []}), - ("massage", {'code': '1f486', 'aliases': []}), - ("mate", {'code': '1f9c9', 'aliases': []}), - ("meat", {'code': '1f356', 'aliases': []}), - ("mechanic", {'code': '1f9d1-200d-1f527', 'aliases': []}), - ("mechanical_arm", {'code': '1f9be', 'aliases': []}), - ("mechanical_leg", {'code': '1f9bf', 'aliases': []}), - ("medal", {'code': '1f3c5', 'aliases': []}), - ("medical_symbol", {'code': '2695', 'aliases': ['aesculapius', 'staff']}), - ("medicine", {'code': '1f48a', 'aliases': ['pill']}), - ("megaphone", {'code': '1f4e3', 'aliases': ['shout']}), - ("melon", {'code': '1f348', 'aliases': []}), - ("melting_face", {'code': '1fae0', 'aliases': ['dissolve', 'liquid', 'melt']}), - ("memo", {'code': '1f4dd', 'aliases': ['note']}), - ("men_with_bunny_ears", {'code': '1f46f-200d-2642', 'aliases': []}), - ("men_wrestling", {'code': '1f93c-200d-2642', 'aliases': []}), - ("mending_heart", {'code': '2764-200d-1fa79', 'aliases': ['healthier', 'improving', 'mending', 'recovering', 'recuperating', 'well']}), - ("menorah", {'code': '1f54e', 'aliases': []}), - ("mens", {'code': '1f6b9', 'aliases': []}), - ("mermaid", {'code': '1f9dc-200d-2640', 'aliases': []}), - ("merman", {'code': '1f9dc-200d-2642', 'aliases': ['triton']}), - ("merperson", {'code': '1f9dc', 'aliases': []}), - ("metro", {'code': '24c2', 'aliases': ['m']}), - ("microbe", {'code': '1f9a0', 'aliases': ['amoeba']}), - ("microphone", {'code': '1f3a4', 'aliases': ['mike', 'mic']}), - ("middle_finger", {'code': '1f595', 'aliases': []}), - ("military_helmet", {'code': '1fa96', 'aliases': ['army', 'military', 'soldier', 'warrior']}), - ("military_medal", {'code': '1f396', 'aliases': []}), - ("milk", {'code': '1f95b', 'aliases': ['glass_of_milk']}), - ("milky_way", {'code': '1f30c', 'aliases': ['night_sky']}), - ("mine", {'code': '26cf', 'aliases': ['pick']}), - ("minibus", {'code': '1f690', 'aliases': []}), - ("minus", {'code': '2796', 'aliases': ['subtract']}), - ("mirror", {'code': '1fa9e', 'aliases': ['reflection', 'reflector', 'speculum']}), - ("mirror_ball", {'code': '1faa9', 'aliases': ['glitter']}), - ("mobile_phone", {'code': '1f4f1', 'aliases': ['smartphone', 'iphone', 'android']}), - ("money", {'code': '1f4b0', 'aliases': []}), - ("money_face", {'code': '1f911', 'aliases': ['kaching']}), - ("monkey", {'code': '1f412', 'aliases': []}), - ("monkey_face", {'code': '1f435', 'aliases': []}), - ("monorail", {'code': '1f69d', 'aliases': ['elevated_train']}), - ("moon", {'code': '1f319', 'aliases': []}), - ("moon_cake", {'code': '1f96e', 'aliases': ['autumn', 'festival', 'yuèbǐng', 'yuebing']}), - ("moon_ceremony", {'code': '1f391', 'aliases': []}), - ("moon_face", {'code': '1f31d', 'aliases': []}), - ("mosque", {'code': '1f54c', 'aliases': []}), - ("mosquito", {'code': '1f99f', 'aliases': ['malaria']}), - ("mostly_sunny", {'code': '1f324', 'aliases': []}), - ("mother_christmas", {'code': '1f936', 'aliases': ['mrs_claus']}), - ("motor_boat", {'code': '1f6e5', 'aliases': []}), - ("motorcycle", {'code': '1f3cd', 'aliases': []}), - ("motorized_wheelchair", {'code': '1f9bc', 'aliases': []}), - ("mount_fuji", {'code': '1f5fb', 'aliases': []}), - ("mountain", {'code': '26f0', 'aliases': []}), - ("mountain_biker", {'code': '1f6b5', 'aliases': []}), - ("mountain_railway", {'code': '1f69e', 'aliases': []}), - ("mountain_sunrise", {'code': '1f304', 'aliases': []}), - ("mouse", {'code': '1f401', 'aliases': []}), - ("mouse_trap", {'code': '1faa4', 'aliases': ['bait', 'mousetrap', 'snare', 'trap']}), - ("movie_camera", {'code': '1f3a5', 'aliases': []}), - ("moving_truck", {'code': '1f69a', 'aliases': []}), - ("multiplication", {'code': '2716', 'aliases': ['multiply']}), - ("muscle", {'code': '1f4aa', 'aliases': []}), - ("mushroom", {'code': '1f344', 'aliases': []}), - ("music", {'code': '1f3b5', 'aliases': []}), - ("musical_notes", {'code': '1f3b6', 'aliases': []}), - ("musical_score", {'code': '1f3bc', 'aliases': []}), - ("mute", {'code': '1f507', 'aliases': ['no_sound']}), - ("mute_notifications", {'code': '1f515', 'aliases': []}), - ("mx_claus", {'code': '1f9d1-200d-1f384', 'aliases': ['claus_christmas']}), - ("nail_polish", {'code': '1f485', 'aliases': ['nail_care']}), - ("name_badge", {'code': '1f4db', 'aliases': []}), - ("naruto", {'code': '1f365', 'aliases': []}), - ("national_park", {'code': '1f3de', 'aliases': []}), - ("nauseated", {'code': '1f922', 'aliases': ['queasy']}), - ("nazar_amulet", {'code': '1f9ff', 'aliases': ['bead', 'charm', 'evil_eye', 'nazar', 'talisman']}), - ("nerd", {'code': '1f913', 'aliases': ['geek']}), - ("nest_with_eggs", {'code': '1faba', 'aliases': []}), - ("nesting_dolls", {'code': '1fa86', 'aliases': ['doll', 'russia']}), - ("neutral", {'code': '1f610', 'aliases': []}), - ("new", {'code': '1f195', 'aliases': []}), - ("new_baby", {'code': '1f425', 'aliases': []}), - ("new_moon", {'code': '1f311', 'aliases': []}), - ("new_moon_face", {'code': '1f31a', 'aliases': []}), - ("newspaper", {'code': '1f5de', 'aliases': ['swat']}), - ("next_track", {'code': '23ed', 'aliases': ['skip_forward']}), - ("ng", {'code': '1f196', 'aliases': []}), - ("night", {'code': '1f303', 'aliases': []}), - ("nine", {'code': '0039-20e3', 'aliases': []}), - ("ninja", {'code': '1f977', 'aliases': ['fighter', 'hidden', 'stealth']}), - ("no_bicycles", {'code': '1f6b3', 'aliases': []}), - ("no_entry", {'code': '26d4', 'aliases': ['wrong_way']}), - ("no_pedestrians", {'code': '1f6b7', 'aliases': []}), - ("no_phones", {'code': '1f4f5', 'aliases': []}), - ("no_signal", {'code': '1f645', 'aliases': ['nope']}), - ("no_smoking", {'code': '1f6ad', 'aliases': []}), - ("non-potable_water", {'code': '1f6b1', 'aliases': []}), - ("nose", {'code': '1f443', 'aliases': []}), - ("notebook", {'code': '1f4d3', 'aliases': ['composition_book']}), - ("notifications", {'code': '1f514', 'aliases': ['bell']}), - ("nut_and_bolt", {'code': '1f529', 'aliases': ['screw']}), - ("o", {'code': '1f17e', 'aliases': []}), - ("ocean", {'code': '1f30a', 'aliases': []}), - ("octopus", {'code': '1f419', 'aliases': []}), - ("oden", {'code': '1f362', 'aliases': []}), - ("office", {'code': '1f3e2', 'aliases': []}), - ("office_supplies", {'code': '1f587', 'aliases': ['paperclip_chain', 'linked']}), - ("office_worker", {'code': '1f9d1-200d-1f4bc', 'aliases': []}), - ("ogre", {'code': '1f479', 'aliases': []}), - ("oh_no", {'code': '1f615', 'aliases': ['half_frown', 'concerned', 'confused']}), - ("oil_drum", {'code': '1f6e2', 'aliases': ['commodities']}), - ("ok", {'code': '1f44c', 'aliases': ['got_it']}), - ("ok_signal", {'code': '1f646', 'aliases': []}), - ("older_man", {'code': '1f474', 'aliases': ['elderly_man']}), - ("older_person", {'code': '1f9d3', 'aliases': ['old']}), - ("older_woman", {'code': '1f475', 'aliases': ['elderly_woman']}), - ("olive", {'code': '1fad2', 'aliases': []}), - ("om", {'code': '1f549', 'aliases': ['hinduism']}), - ("on", {'code': '1f51b', 'aliases': []}), - ("oncoming_bus", {'code': '1f68d', 'aliases': []}), - ("oncoming_car", {'code': '1f698', 'aliases': ['oncoming_automobile']}), - ("oncoming_police_car", {'code': '1f694', 'aliases': []}), - ("oncoming_taxi", {'code': '1f696', 'aliases': []}), - ("oncoming_train", {'code': '1f686', 'aliases': []}), - ("oncoming_tram", {'code': '1f68a', 'aliases': ['oncoming_streetcar', 'oncoming_trolley']}), - ("one", {'code': '0031-20e3', 'aliases': []}), - ("one_piece_swimsuit", {'code': '1fa71', 'aliases': []}), - ("onigiri", {'code': '1f359', 'aliases': []}), - ("onion", {'code': '1f9c5', 'aliases': []}), - ("open_hands", {'code': '1f450', 'aliases': []}), - ("open_mouth", {'code': '1f62e', 'aliases': ['surprise']}), - ("ophiuchus", {'code': '26ce', 'aliases': []}), - ("orange", {'code': '1f34a', 'aliases': ['tangerine', 'mandarin']}), - ("orange_book", {'code': '1f4d9', 'aliases': []}), - ("orange_circle", {'code': '1f7e0', 'aliases': []}), - ("orange_heart", {'code': '1f9e1', 'aliases': []}), - ("orange_square", {'code': '1f7e7', 'aliases': []}), - ("orangutan", {'code': '1f9a7', 'aliases': ['ape']}), - ("organize", {'code': '1f4c1', 'aliases': ['file_folder']}), - ("orthodox_cross", {'code': '2626', 'aliases': []}), - ("otter", {'code': '1f9a6', 'aliases': ['playful']}), - ("outbox", {'code': '1f4e4', 'aliases': []}), - ("owl", {'code': '1f989', 'aliases': []}), - ("ox", {'code': '1f402', 'aliases': ['bull']}), - ("oyster", {'code': '1f9aa', 'aliases': []}), - ("package", {'code': '1f4e6', 'aliases': []}), - ("paella", {'code': '1f958', 'aliases': []}), - ("page_with_curl", {'code': '1f4c3', 'aliases': ['curl']}), - ("pager", {'code': '1f4df', 'aliases': []}), - ("paintbrush", {'code': '1f58c', 'aliases': []}), - ("palm_down_hand", {'code': '1faf3', 'aliases': ['dismiss', 'shoo']}), - ("palm_tree", {'code': '1f334', 'aliases': []}), - ("palm_up_hand", {'code': '1faf4', 'aliases': ['beckon', 'come', 'offer']}), - ("palms_up_together", {'code': '1f932', 'aliases': ['prayer']}), - ("pancakes", {'code': '1f95e', 'aliases': ['breakfast']}), - ("panda", {'code': '1f43c', 'aliases': []}), - ("paperclip", {'code': '1f4ce', 'aliases': ['attachment']}), - ("parachute", {'code': '1fa82', 'aliases': ['hang_glide', 'parasail', 'skydive']}), - ("parking", {'code': '1f17f', 'aliases': ['p']}), - ("parrot", {'code': '1f99c', 'aliases': ['talk']}), - ("part_alternation", {'code': '303d', 'aliases': []}), - ("partly_sunny", {'code': '26c5', 'aliases': ['partly_cloudy']}), - ("partying_face", {'code': '1f973', 'aliases': []}), - ("pass", {'code': '1f3ab', 'aliases': []}), - ("passenger_ship", {'code': '1f6f3', 'aliases': ['yacht', 'cruise']}), - ("passport_control", {'code': '1f6c2', 'aliases': ['immigration']}), - ("pause", {'code': '23f8', 'aliases': []}), - ("paw_prints", {'code': '1f43e', 'aliases': ['paws']}), - ("peace", {'code': '262e', 'aliases': []}), - ("peace_sign", {'code': '270c', 'aliases': ['victory']}), - ("peach", {'code': '1f351', 'aliases': []}), - ("peacock", {'code': '1f99a', 'aliases': ['ostentatious', 'peahen']}), - ("peanuts", {'code': '1f95c', 'aliases': []}), - ("pear", {'code': '1f350', 'aliases': []}), - ("pen", {'code': '1f58a', 'aliases': ['ballpoint_pen']}), - ("pencil", {'code': '270f', 'aliases': []}), - ("penguin", {'code': '1f427', 'aliases': []}), - ("pensive", {'code': '1f614', 'aliases': ['tired']}), - ("people_holding_hands", {'code': '1f9d1-200d-1f91d-200d-1f9d1', 'aliases': ['hold', 'holding_hands']}), - ("people_hugging", {'code': '1fac2', 'aliases': ['goodbye', 'thanks']}), - ("performing_arts", {'code': '1f3ad', 'aliases': ['drama', 'theater']}), - ("persevere", {'code': '1f623', 'aliases': ['helpless']}), - ("person", {'code': '1f9d1', 'aliases': []}), - ("person_bald", {'code': '1f9d1-200d-1f9b2', 'aliases': []}), - ("person_beard", {'code': '1f9d4', 'aliases': []}), - ("person_blond_hair", {'code': '1f471', 'aliases': ['blond_haired_person']}), - ("person_climbing", {'code': '1f9d7', 'aliases': []}), - ("person_curly_hair", {'code': '1f9d1-200d-1f9b1', 'aliases': []}), - ("person_feeding_baby", {'code': '1f9d1-200d-1f37c', 'aliases': []}), - ("person_frowning", {'code': '1f64d', 'aliases': []}), - ("person_in_lotus_position", {'code': '1f9d8', 'aliases': []}), - ("person_in_manual_wheelchair", {'code': '1f9d1-200d-1f9bd', 'aliases': []}), - ("person_in_motorized_wheelchair", {'code': '1f9d1-200d-1f9bc', 'aliases': []}), - ("person_in_steamy_room", {'code': '1f9d6', 'aliases': []}), - ("person_kneeling", {'code': '1f9ce', 'aliases': ['kneel']}), - ("person_pouting", {'code': '1f64e', 'aliases': []}), - ("person_red_hair", {'code': '1f9d1-200d-1f9b0', 'aliases': []}), - ("person_standing", {'code': '1f9cd', 'aliases': ['stand']}), - ("person_white_hair", {'code': '1f9d1-200d-1f9b3', 'aliases': []}), - ("person_with_crown", {'code': '1fac5', 'aliases': ['monarch', 'noble', 'regal', 'royalty']}), - ("person_with_white_cane", {'code': '1f9d1-200d-1f9af', 'aliases': []}), - ("petri_dish", {'code': '1f9eb', 'aliases': ['biology', 'culture']}), - ("phone", {'code': '260e', 'aliases': ['telephone']}), - ("phone_off", {'code': '1f4f4', 'aliases': []}), - ("piano", {'code': '1f3b9', 'aliases': ['musical_keyboard']}), - ("pickup_truck", {'code': '1f6fb', 'aliases': ['pick_up', 'pickup']}), - ("picture", {'code': '1f5bc', 'aliases': ['framed_picture']}), - ("pie", {'code': '1f967', 'aliases': ['filling', 'pastry']}), - ("pig", {'code': '1f416', 'aliases': ['oink']}), - ("pig_nose", {'code': '1f43d', 'aliases': []}), - ("piglet", {'code': '1f437', 'aliases': []}), - ("pilot", {'code': '1f9d1-200d-2708', 'aliases': []}), - ("pin", {'code': '1f4cd', 'aliases': ['sewing_pin']}), - ("pinched_fingers", {'code': '1f90c', 'aliases': ['fingers', 'hand_gesture', 'interrogation', 'pinched', 'sarcastic']}), - ("pinching_hand", {'code': '1f90f', 'aliases': ['small_amount']}), - ("pineapple", {'code': '1f34d', 'aliases': []}), - ("ping_pong", {'code': '1f3d3', 'aliases': ['table_tennis']}), - ("pirate_flag", {'code': '1f3f4-200d-2620', 'aliases': ['jolly_roger', 'plunder']}), - ("pisces", {'code': '2653', 'aliases': []}), - ("pizza", {'code': '1f355', 'aliases': []}), - ("piñata", {'code': '1fa85', 'aliases': ['pinata']}), - ("placard", {'code': '1faa7', 'aliases': ['demonstration', 'picket', 'protest', 'sign']}), - ("place_holder", {'code': '1f4d1', 'aliases': []}), - ("place_of_worship", {'code': '1f6d0', 'aliases': []}), - ("play", {'code': '25b6', 'aliases': []}), - ("play_pause", {'code': '23ef', 'aliases': []}), - ("play_reverse", {'code': '25c0', 'aliases': []}), - ("playground_slide", {'code': '1f6dd', 'aliases': ['amusement_park']}), - ("playing_cards", {'code': '1f3b4', 'aliases': []}), - ("pleading_face", {'code': '1f97a', 'aliases': ['begging', 'mercy', 'puppy_eyes']}), - ("plunger", {'code': '1faa0', 'aliases': ['force_cup', 'suction']}), - ("plus", {'code': '2795', 'aliases': ['add']}), - ("point_down", {'code': '1f447', 'aliases': []}), - ("point_left", {'code': '1f448', 'aliases': []}), - ("point_right", {'code': '1f449', 'aliases': []}), - ("point_up", {'code': '1f446', 'aliases': ['this']}), - ("polar_bear", {'code': '1f43b-200d-2744', 'aliases': ['arctic']}), - ("police", {'code': '1f46e', 'aliases': ['cop']}), - ("police_car", {'code': '1f693', 'aliases': []}), - ("pony", {'code': '1f434', 'aliases': []}), - ("poodle", {'code': '1f429', 'aliases': []}), - ("poop", {'code': '1f4a9', 'aliases': ['pile_of_poo']}), - ("popcorn", {'code': '1f37f', 'aliases': []}), - ("post_office", {'code': '1f3e4', 'aliases': []}), - ("potable_water", {'code': '1f6b0', 'aliases': ['tap_water', 'drinking_water']}), - ("potato", {'code': '1f954', 'aliases': []}), - ("potted_plant", {'code': '1fab4', 'aliases': ['boring', 'grow', 'nurturing', 'useless']}), - ("pouch", {'code': '1f45d', 'aliases': []}), - ("pound_notes", {'code': '1f4b7', 'aliases': []}), - ("pouring_liquid", {'code': '1fad7', 'aliases': ['spill']}), - ("pray", {'code': '1f64f', 'aliases': ['welcome', 'thank_you', 'namaste']}), - ("prayer_beads", {'code': '1f4ff', 'aliases': []}), - ("pregnant", {'code': '1f930', 'aliases': ['expecting']}), - ("pregnant_man", {'code': '1fac3', 'aliases': []}), - ("pregnant_person", {'code': '1fac4', 'aliases': []}), - ("pretzel", {'code': '1f968', 'aliases': ['twisted']}), - ("previous_track", {'code': '23ee', 'aliases': ['skip_back']}), - ("prince", {'code': '1f934', 'aliases': []}), - ("princess", {'code': '1f478', 'aliases': []}), - ("printer", {'code': '1f5a8', 'aliases': []}), - ("privacy", {'code': '1f50f', 'aliases': ['key_signing', 'digital_security', 'protected']}), - ("prohibited", {'code': '1f6ab', 'aliases': ['not_allowed']}), - ("projector", {'code': '1f4fd', 'aliases': ['movie']}), - ("puppy", {'code': '1f436', 'aliases': []}), - ("purple_circle", {'code': '1f7e3', 'aliases': []}), - ("purple_heart", {'code': '1f49c', 'aliases': ['bravery']}), - ("purple_square", {'code': '1f7ea', 'aliases': []}), - ("purse", {'code': '1f45b', 'aliases': []}), - ("push_pin", {'code': '1f4cc', 'aliases': ['thumb_tack']}), - ("put_litter_in_its_place", {'code': '1f6ae', 'aliases': []}), - ("puzzle_piece", {'code': '1f9e9', 'aliases': ['interlocking', 'jigsaw', 'piece', 'puzzle']}), - ("question", {'code': '2753', 'aliases': []}), - ("rabbit", {'code': '1f407', 'aliases': []}), - ("raccoon", {'code': '1f99d', 'aliases': ['curious', 'sly']}), - ("racecar", {'code': '1f3ce', 'aliases': []}), - ("radio", {'code': '1f4fb', 'aliases': []}), - ("radio_button", {'code': '1f518', 'aliases': []}), - ("radioactive", {'code': '2622', 'aliases': ['nuclear']}), - ("rage", {'code': '1f621', 'aliases': ['mad', 'grumpy', 'very_angry']}), - ("railway_car", {'code': '1f683', 'aliases': ['train_car']}), - ("railway_track", {'code': '1f6e4', 'aliases': ['train_tracks']}), - ("rainbow", {'code': '1f308', 'aliases': ['pride', 'lgbtq']}), - ("rainbow_flag", {'code': '1f3f3-200d-1f308', 'aliases': []}), - ("rainy", {'code': '1f327', 'aliases': ['soaked', 'drenched']}), - ("raised_hands", {'code': '1f64c', 'aliases': ['praise']}), - ("raising_hand", {'code': '1f64b', 'aliases': ['pick_me']}), - ("ram", {'code': '1f40f', 'aliases': []}), - ("ramen", {'code': '1f35c', 'aliases': ['noodles']}), - ("rat", {'code': '1f400', 'aliases': []}), - ("razor", {'code': '1fa92', 'aliases': ['sharp', 'shave']}), - ("receipt", {'code': '1f9fe', 'aliases': ['accounting', 'bookkeeping', 'evidence', 'proof']}), - ("record", {'code': '23fa', 'aliases': []}), - ("recreational_vehicle", {'code': '1f699', 'aliases': ['jeep']}), - ("recycle", {'code': '267b', 'aliases': []}), - ("red_book", {'code': '1f4d5', 'aliases': ['closed_book']}), - ("red_circle", {'code': '1f534', 'aliases': []}), - ("red_envelope", {'code': '1f9e7', 'aliases': ['good_luck', 'hóngbāo', 'lai_see', 'hongbao']}), - ("red_square", {'code': '1f7e5', 'aliases': ['red']}), - ("red_triangle_down", {'code': '1f53b', 'aliases': []}), - ("red_triangle_up", {'code': '1f53a', 'aliases': []}), - ("registered", {'code': '00ae', 'aliases': ['r']}), - ("relieved", {'code': '1f60c', 'aliases': []}), - ("reminder_ribbon", {'code': '1f397', 'aliases': []}), - ("repeat", {'code': '1f501', 'aliases': []}), - ("repeat_one", {'code': '1f502', 'aliases': []}), - ("reply", {'code': '21a9', 'aliases': ['left_hook']}), - ("restroom", {'code': '1f6bb', 'aliases': []}), - ("revolving_hearts", {'code': '1f49e', 'aliases': []}), - ("rewind", {'code': '23ea', 'aliases': ['fast_reverse']}), - ("rhinoceros", {'code': '1f98f', 'aliases': []}), - ("ribbon", {'code': '1f380', 'aliases': ['decoration']}), - ("rice", {'code': '1f35a', 'aliases': []}), - ("right", {'code': '27a1', 'aliases': ['east']}), - ("right_fist", {'code': '1f91c', 'aliases': []}), - ("rightwards_hand", {'code': '1faf1', 'aliases': ['rightward']}), - ("ring", {'code': '1f48d', 'aliases': []}), - ("ring_buoy", {'code': '1f6df', 'aliases': ['float', 'life_preserver', 'life_saver', 'rescue']}), - ("ringed_planet", {'code': '1fa90', 'aliases': ['saturn', 'saturnine']}), - ("road", {'code': '1f6e3', 'aliases': ['motorway']}), - ("robot", {'code': '1f916', 'aliases': []}), - ("rock", {'code': '1faa8', 'aliases': ['boulder', 'heavy', 'solid', 'stone']}), - ("rock_carving", {'code': '1f5ff', 'aliases': ['moyai']}), - ("rock_on", {'code': '1f918', 'aliases': ['sign_of_the_horns']}), - ("rocket", {'code': '1f680', 'aliases': []}), - ("roll_of_paper", {'code': '1f9fb', 'aliases': ['paper_towels', 'toilet_paper']}), - ("roller_coaster", {'code': '1f3a2', 'aliases': []}), - ("roller_skate", {'code': '1f6fc', 'aliases': ['roller', 'skate']}), - ("rolling_eyes", {'code': '1f644', 'aliases': []}), - ("rolling_on_the_floor_laughing", {'code': '1f923', 'aliases': ['rofl']}), - ("rolodex", {'code': '1f4c7', 'aliases': ['card_index']}), - ("rooster", {'code': '1f413', 'aliases': ['alarm', 'cock-a-doodle-doo']}), - ("rose", {'code': '1f339', 'aliases': []}), - ("rosette", {'code': '1f3f5', 'aliases': []}), - ("rowboat", {'code': '1f6a3', 'aliases': ['crew', 'sculling', 'rowing']}), - ("rugby", {'code': '1f3c9', 'aliases': []}), - ("ruler", {'code': '1f4cf', 'aliases': ['straightedge']}), - ("running", {'code': '1f3c3', 'aliases': ['runner']}), - ("running_shirt", {'code': '1f3bd', 'aliases': []}), - ("sad", {'code': '2639', 'aliases': ['big_frown']}), - ("safety_pin", {'code': '1f9f7', 'aliases': ['diaper', 'punk_rock']}), - ("safety_vest", {'code': '1f9ba', 'aliases': ['emergency', 'vest']}), - ("sagittarius", {'code': '2650', 'aliases': []}), - ("sake", {'code': '1f376', 'aliases': []}), - ("salad", {'code': '1f957', 'aliases': []}), - ("salt", {'code': '1f9c2', 'aliases': ['shaker']}), - ("saluting_face", {'code': '1fae1', 'aliases': ['salute', 'troops', 'yes']}), - ("sandal", {'code': '1f461', 'aliases': ['flip_flops']}), - ("sandwich", {'code': '1f96a', 'aliases': []}), - ("santa", {'code': '1f385', 'aliases': []}), - ("sari", {'code': '1f97b', 'aliases': []}), - ("satellite", {'code': '1f6f0', 'aliases': []}), - ("satellite_antenna", {'code': '1f4e1', 'aliases': []}), - ("sauropod", {'code': '1f995', 'aliases': ['brachiosaurus', 'brontosaurus', 'diplodocus']}), - ("saxophone", {'code': '1f3b7', 'aliases': []}), - ("scarf", {'code': '1f9e3', 'aliases': ['neck']}), - ("school", {'code': '1f3eb', 'aliases': []}), - ("science", {'code': '1f52c', 'aliases': ['microscope']}), - ("scientist", {'code': '1f9d1-200d-1f52c', 'aliases': []}), - ("scissors", {'code': '2702', 'aliases': []}), - ("scooter", {'code': '1f6f5', 'aliases': ['motor_bike']}), - ("scorpion", {'code': '1f982', 'aliases': []}), - ("scorpius", {'code': '264f', 'aliases': []}), - ("scream", {'code': '1f631', 'aliases': []}), - ("scream_cat", {'code': '1f640', 'aliases': ['weary_cat']}), - ("screwdriver", {'code': '1fa9b', 'aliases': []}), - ("scroll", {'code': '1f4dc', 'aliases': []}), - ("seal", {'code': '1f9ad', 'aliases': ['sea_lion']}), - ("search", {'code': '1f50d', 'aliases': ['find', 'magnifying_glass']}), - ("seat", {'code': '1f4ba', 'aliases': []}), - ("second_place", {'code': '1f948', 'aliases': ['silver']}), - ("secret", {'code': '1f5dd', 'aliases': ['dungeon', 'old_key', 'encrypted', 'clue', 'hint']}), - ("secure", {'code': '1f510', 'aliases': ['lock_with_key', 'safe', 'commitment', 'loyalty']}), - ("see_no_evil", {'code': '1f648', 'aliases': []}), - ("seedling", {'code': '1f331', 'aliases': ['sprout']}), - ("seeing_stars", {'code': '1f4ab', 'aliases': []}), - ("selfie", {'code': '1f933', 'aliases': []}), - ("senbei", {'code': '1f358', 'aliases': ['rice_cracker']}), - ("service_dog", {'code': '1f415-200d-1f9ba', 'aliases': ['assistance', 'service']}), - ("seven", {'code': '0037-20e3', 'aliases': []}), - ("sewing_needle", {'code': '1faa1', 'aliases': ['embroidery', 'stitches', 'sutures', 'tailoring']}), - ("shamrock", {'code': '2618', 'aliases': ['clover']}), - ("shark", {'code': '1f988', 'aliases': []}), - ("shaved_ice", {'code': '1f367', 'aliases': []}), - ("sheep", {'code': '1f411', 'aliases': ['baa']}), - ("shell", {'code': '1f41a', 'aliases': ['seashell', 'conch', 'spiral_shell']}), - ("shield", {'code': '1f6e1', 'aliases': []}), - ("shinto_shrine", {'code': '26e9', 'aliases': []}), - ("ship", {'code': '1f6a2', 'aliases': []}), - ("shiro", {'code': '1f3ef', 'aliases': []}), - ("shirt", {'code': '1f455', 'aliases': ['tshirt']}), - ("shoe", {'code': '1f45e', 'aliases': []}), - ("shooting_star", {'code': '1f320', 'aliases': ['wish']}), - ("shopping_bags", {'code': '1f6cd', 'aliases': []}), - ("shopping_cart", {'code': '1f6d2', 'aliases': ['shopping_trolley']}), - ("shorts", {'code': '1fa73', 'aliases': ['pants']}), - ("shower", {'code': '1f6bf', 'aliases': []}), - ("shrimp", {'code': '1f990', 'aliases': []}), - ("shrug", {'code': '1f937', 'aliases': []}), - ("shuffle", {'code': '1f500', 'aliases': []}), - ("shushing_face", {'code': '1f92b', 'aliases': ['shush']}), - ("sick", {'code': '1f912', 'aliases': ['flu', 'face_with_thermometer', 'ill', 'fever']}), - ("silence", {'code': '1f910', 'aliases': ['quiet', 'hush', 'zip_it', 'lips_are_sealed']}), - ("silhouette", {'code': '1f464', 'aliases': ['shadow']}), - ("silhouettes", {'code': '1f465', 'aliases': ['shadows']}), - ("singer", {'code': '1f9d1-200d-1f3a4', 'aliases': []}), - ("siren", {'code': '1f6a8', 'aliases': ['rotating_light', 'alert']}), - ("six", {'code': '0036-20e3', 'aliases': []}), - ("skateboard", {'code': '1f6f9', 'aliases': ['board']}), - ("ski", {'code': '1f3bf', 'aliases': []}), - ("skier", {'code': '26f7', 'aliases': []}), - ("skull", {'code': '1f480', 'aliases': []}), - ("skull_and_crossbones", {'code': '2620', 'aliases': ['pirate', 'death', 'hazard', 'toxic', 'poison']}), - ("skunk", {'code': '1f9a8', 'aliases': ['stink']}), - ("sled", {'code': '1f6f7', 'aliases': ['sledge', 'sleigh']}), - ("sleeping", {'code': '1f634', 'aliases': []}), - ("sleepy", {'code': '1f62a', 'aliases': []}), - ("slot_machine", {'code': '1f3b0', 'aliases': []}), - ("sloth", {'code': '1f9a5', 'aliases': ['lazy', 'slow']}), - ("small_airplane", {'code': '1f6e9', 'aliases': []}), - ("small_blue_diamond", {'code': '1f539', 'aliases': []}), - ("small_glass", {'code': '1f943', 'aliases': []}), - ("small_orange_diamond", {'code': '1f538', 'aliases': []}), - ("smile", {'code': '1f642', 'aliases': []}), - ("smile_cat", {'code': '1f638', 'aliases': []}), - ("smiley", {'code': '1f603', 'aliases': []}), - ("smiley_cat", {'code': '1f63a', 'aliases': []}), - ("smiling_devil", {'code': '1f608', 'aliases': ['smiling_imp', 'smiling_face_with_horns']}), - ("smiling_face", {'code': '263a', 'aliases': ['relaxed']}), - ("smiling_face_with_hearts", {'code': '1f970', 'aliases': ['adore', 'crush']}), - ("smiling_face_with_tear", {'code': '1f972', 'aliases': ['grateful', 'smiling', 'tear', 'touched']}), - ("smirk", {'code': '1f60f', 'aliases': ['smug']}), - ("smirk_cat", {'code': '1f63c', 'aliases': ['smug_cat']}), - ("smoking", {'code': '1f6ac', 'aliases': []}), - ("snail", {'code': '1f40c', 'aliases': []}), - ("snake", {'code': '1f40d', 'aliases': ['hiss']}), - ("sneezing", {'code': '1f927', 'aliases': []}), - ("snowboarder", {'code': '1f3c2', 'aliases': []}), - ("snowflake", {'code': '2744', 'aliases': []}), - ("snowman", {'code': '2603', 'aliases': []}), - ("snowy", {'code': '1f328', 'aliases': ['snowstorm']}), - ("snowy_mountain", {'code': '1f3d4', 'aliases': []}), - ("soap", {'code': '1f9fc', 'aliases': ['bar', 'bathing', 'lather', 'soapdish']}), - ("sob", {'code': '1f62d', 'aliases': []}), - ("socks", {'code': '1f9e6', 'aliases': ['stocking']}), - ("soft_serve", {'code': '1f366', 'aliases': ['soft_ice_cream']}), - ("softball", {'code': '1f94e', 'aliases': ['glove', 'underarm']}), - ("softer", {'code': '1f509', 'aliases': []}), - ("soon", {'code': '1f51c', 'aliases': []}), - ("sort", {'code': '1f5c2', 'aliases': []}), - ("sos", {'code': '1f198', 'aliases': []}), - ("space_invader", {'code': '1f47e', 'aliases': []}), - ("spades", {'code': '2660', 'aliases': []}), - ("spaghetti", {'code': '1f35d', 'aliases': []}), - ("sparkle", {'code': '2747', 'aliases': []}), - ("sparkler", {'code': '1f387', 'aliases': []}), - ("sparkles", {'code': '2728', 'aliases': ['glamour']}), - ("sparkling_heart", {'code': '1f496', 'aliases': []}), - ("speak_no_evil", {'code': '1f64a', 'aliases': []}), - ("speaker", {'code': '1f508', 'aliases': []}), - ("speaking_head", {'code': '1f5e3', 'aliases': []}), - ("speech_bubble", {'code': '1f5e8', 'aliases': []}), - ("speechless", {'code': '1f636', 'aliases': ['no_mouth', 'blank', 'poker_face']}), - ("speedboat", {'code': '1f6a4', 'aliases': []}), - ("spider", {'code': '1f577', 'aliases': []}), - ("spiral_calendar", {'code': '1f5d3', 'aliases': ['pad']}), - ("spiral_notepad", {'code': '1f5d2', 'aliases': []}), - ("spock", {'code': '1f596', 'aliases': ['live_long_and_prosper']}), - ("sponge", {'code': '1f9fd', 'aliases': ['absorbing', 'porous']}), - ("spoon", {'code': '1f944', 'aliases': []}), - ("squared_ok", {'code': '1f197', 'aliases': []}), - ("squared_up", {'code': '1f199', 'aliases': []}), - ("squid", {'code': '1f991', 'aliases': []}), - ("stadium", {'code': '1f3df', 'aliases': []}), - ("star", {'code': '2b50', 'aliases': []}), - ("star_and_crescent", {'code': '262a', 'aliases': ['islam']}), - ("star_of_david", {'code': '2721', 'aliases': ['judaism']}), - ("star_struck", {'code': '1f929', 'aliases': []}), - ("station", {'code': '1f689', 'aliases': []}), - ("statue", {'code': '1f5fd', 'aliases': ['new_york', 'statue_of_liberty']}), - ("stethoscope", {'code': '1fa7a', 'aliases': []}), - ("stock_market", {'code': '1f4b9', 'aliases': []}), - ("stop", {'code': '1f91a', 'aliases': []}), - ("stop_button", {'code': '23f9', 'aliases': []}), - ("stop_sign", {'code': '1f6d1', 'aliases': ['octagonal_sign']}), - ("stopwatch", {'code': '23f1', 'aliases': []}), - ("strawberry", {'code': '1f353', 'aliases': []}), - ("strike", {'code': '1f3b3', 'aliases': ['bowling']}), - ("stuck_out_tongue", {'code': '1f61b', 'aliases': ['mischievous']}), - ("stuck_out_tongue_closed_eyes", {'code': '1f61d', 'aliases': []}), - ("stuck_out_tongue_wink", {'code': '1f61c', 'aliases': ['joking', 'crazy']}), - ("student", {'code': '1f9d1-200d-1f393', 'aliases': []}), - ("studio_microphone", {'code': '1f399', 'aliases': []}), - ("suburb", {'code': '1f3e1', 'aliases': []}), - ("subway", {'code': '1f687', 'aliases': []}), - ("sun_face", {'code': '1f31e', 'aliases': []}), - ("sunflower", {'code': '1f33b', 'aliases': []}), - ("sunglasses", {'code': '1f60e', 'aliases': []}), - ("sunny", {'code': '2600', 'aliases': []}), - ("sunrise", {'code': '1f305', 'aliases': ['ocean_sunrise']}), - ("sunset", {'code': '1f306', 'aliases': []}), - ("sunshowers", {'code': '1f326', 'aliases': ['sun_and_rain', 'partly_sunny_with_rain']}), - ("superhero", {'code': '1f9b8', 'aliases': []}), - ("supervillain", {'code': '1f9b9', 'aliases': []}), - ("surf", {'code': '1f3c4', 'aliases': []}), - ("sushi", {'code': '1f363', 'aliases': []}), - ("suspension_railway", {'code': '1f69f', 'aliases': []}), - ("swan", {'code': '1f9a2', 'aliases': ['cygnet', 'ugly_duckling']}), - ("sweat", {'code': '1f613', 'aliases': []}), - ("sweat_drops", {'code': '1f4a6', 'aliases': []}), - ("sweat_smile", {'code': '1f605', 'aliases': []}), - ("swim", {'code': '1f3ca', 'aliases': []}), - ("symbols", {'code': '1f523', 'aliases': []}), - ("synagogue", {'code': '1f54d', 'aliases': []}), - ("t_rex", {'code': '1f996', 'aliases': ['tyrannosaurus_rex']}), - ("taco", {'code': '1f32e', 'aliases': []}), - ("tada", {'code': '1f389', 'aliases': []}), - ("take_off", {'code': '1f6eb', 'aliases': ['departure', 'airplane_departure']}), - ("takeout_box", {'code': '1f961', 'aliases': ['oyster_pail']}), - ("taking_a_picture", {'code': '1f4f8', 'aliases': ['say_cheese']}), - ("tamale", {'code': '1fad4', 'aliases': ['mexican', 'wrapped']}), - ("taurus", {'code': '2649', 'aliases': []}), - ("taxi", {'code': '1f695', 'aliases': ['rideshare']}), - ("tea", {'code': '1f375', 'aliases': []}), - ("teacher", {'code': '1f9d1-200d-1f3eb', 'aliases': []}), - ("teapot", {'code': '1fad6', 'aliases': []}), - ("technologist", {'code': '1f9d1-200d-1f4bb', 'aliases': []}), - ("teddy_bear", {'code': '1f9f8', 'aliases': ['plaything', 'plush', 'stuffed']}), - ("telescope", {'code': '1f52d', 'aliases': []}), - ("temperature", {'code': '1f321', 'aliases': ['thermometer', 'warm']}), - ("tempura", {'code': '1f364', 'aliases': []}), - ("ten", {'code': '1f51f', 'aliases': []}), - ("tennis", {'code': '1f3be', 'aliases': []}), - ("tent", {'code': '26fa', 'aliases': ['camping']}), - ("test_tube", {'code': '1f9ea', 'aliases': ['chemistry']}), - ("thinking", {'code': '1f914', 'aliases': []}), - ("third_place", {'code': '1f949', 'aliases': ['bronze']}), - ("thong_sandal", {'code': '1fa74', 'aliases': ['beach_sandals', 'sandals', 'thong_sandals', 'thongs', 'zōri', 'zori']}), - ("thought", {'code': '1f4ad', 'aliases': ['dream']}), - ("thread", {'code': '1f9f5', 'aliases': ['spool', 'string']}), - ("three", {'code': '0033-20e3', 'aliases': []}), - ("thunderstorm", {'code': '26c8', 'aliases': ['thunder_and_rain']}), - ("ticket", {'code': '1f39f', 'aliases': []}), - ("tie", {'code': '1f454', 'aliases': []}), - ("tiger", {'code': '1f405', 'aliases': []}), - ("tiger_cub", {'code': '1f42f', 'aliases': []}), - ("time", {'code': '1f557', 'aliases': ['clock']}), - ("time_ticking", {'code': '23f3', 'aliases': ['hourglass']}), - ("timer", {'code': '23f2', 'aliases': []}), - ("times_up", {'code': '231b', 'aliases': ['hourglass_done']}), - ("tm", {'code': '2122', 'aliases': ['trademark']}), - ("toilet", {'code': '1f6bd', 'aliases': []}), - ("tomato", {'code': '1f345', 'aliases': []}), - ("tongue", {'code': '1f445', 'aliases': []}), - ("toolbox", {'code': '1f9f0', 'aliases': ['chest']}), - ("tooth", {'code': '1f9b7', 'aliases': ['dentist']}), - ("toothbrush", {'code': '1faa5', 'aliases': ['bathroom', 'brush', 'dental', 'hygiene', 'teeth']}), - ("top", {'code': '1f51d', 'aliases': []}), - ("top_hat", {'code': '1f3a9', 'aliases': []}), - ("tornado", {'code': '1f32a', 'aliases': []}), - ("tower", {'code': '1f5fc', 'aliases': ['tokyo_tower']}), - ("trackball", {'code': '1f5b2', 'aliases': []}), - ("tractor", {'code': '1f69c', 'aliases': []}), - ("traffic_light", {'code': '1f6a6', 'aliases': ['vertical_traffic_light']}), - ("train", {'code': '1f682', 'aliases': ['steam_locomotive']}), - ("tram", {'code': '1f68b', 'aliases': ['streetcar']}), - ("transgender_flag", {'code': '1f3f3-fe0f-200d-26a7-fe0f', 'aliases': ['light_blue', 'pink']}), - ("transgender_symbol", {'code': '26a7', 'aliases': []}), - ("tree", {'code': '1f333', 'aliases': ['deciduous_tree']}), - ("triangular_flag", {'code': '1f6a9', 'aliases': []}), - ("trident", {'code': '1f531', 'aliases': []}), - ("triumph", {'code': '1f624', 'aliases': []}), - ("troll", {'code': '1f9cc', 'aliases': ['fairy_tale', 'fantasy', 'monster']}), - ("trolley", {'code': '1f68e', 'aliases': []}), - ("trophy", {'code': '1f3c6', 'aliases': ['winner']}), - ("tropical_drink", {'code': '1f379', 'aliases': []}), - ("tropical_fish", {'code': '1f420', 'aliases': []}), - ("truck", {'code': '1f69b', 'aliases': ['tractor-trailer', 'big_rig', 'semi_truck', 'transport_truck']}), - ("trumpet", {'code': '1f3ba', 'aliases': []}), - ("tulip", {'code': '1f337', 'aliases': ['flower']}), - ("turban", {'code': '1f473', 'aliases': []}), - ("turkey", {'code': '1f983', 'aliases': []}), - ("turtle", {'code': '1f422', 'aliases': ['tortoise']}), - ("tuxedo", {'code': '1f935', 'aliases': []}), - ("tv", {'code': '1f4fa', 'aliases': ['television']}), - ("two", {'code': '0032-20e3', 'aliases': []}), - ("two_hearts", {'code': '1f495', 'aliases': []}), - ("two_men_holding_hands", {'code': '1f46c', 'aliases': ['men_couple']}), - ("two_women_holding_hands", {'code': '1f46d', 'aliases': ['women_couple']}), - ("umbrella", {'code': '2602', 'aliases': []}), - ("umbrella_with_rain", {'code': '2614', 'aliases': []}), - ("umm", {'code': '1f4ac', 'aliases': ['speech_balloon']}), - ("unamused", {'code': '1f612', 'aliases': []}), - ("underage", {'code': '1f51e', 'aliases': ['nc17']}), - ("unicorn", {'code': '1f984', 'aliases': []}), - ("unlocked", {'code': '1f513', 'aliases': []}), - ("unread_mail", {'code': '1f4ec', 'aliases': []}), - ("up", {'code': '2b06', 'aliases': ['north']}), - ("up_down", {'code': '2195', 'aliases': []}), - ("upper_left", {'code': '2196', 'aliases': ['north_west']}), - ("upper_right", {'code': '2197', 'aliases': ['north_east']}), - ("upside_down", {'code': '1f643', 'aliases': ['oops']}), - ("upvote", {'code': '1f53c', 'aliases': ['up_button', 'increase']}), - ("vampire", {'code': '1f9db', 'aliases': []}), - ("vase", {'code': '1f3fa', 'aliases': ['amphora']}), - ("vhs", {'code': '1f4fc', 'aliases': ['videocassette']}), - ("vibration_mode", {'code': '1f4f3', 'aliases': []}), - ("video_camera", {'code': '1f4f9', 'aliases': ['video_recorder']}), - ("video_game", {'code': '1f3ae', 'aliases': []}), - ("violin", {'code': '1f3bb', 'aliases': []}), - ("virgo", {'code': '264d', 'aliases': []}), - ("volcano", {'code': '1f30b', 'aliases': []}), - ("volleyball", {'code': '1f3d0', 'aliases': []}), - ("volume", {'code': '1f39a', 'aliases': ['level_slider']}), - ("vs", {'code': '1f19a', 'aliases': []}), - ("waffle", {'code': '1f9c7', 'aliases': ['indecisive', 'iron']}), - ("wait_one_second", {'code': '261d', 'aliases': ['point_of_information', 'asking_a_question']}), - ("walking", {'code': '1f6b6', 'aliases': ['pedestrian']}), - ("waning_crescent_moon", {'code': '1f318', 'aliases': []}), - ("waning_gibbous_moon", {'code': '1f316', 'aliases': ['gibbous']}), - ("warning", {'code': '26a0', 'aliases': ['caution', 'danger']}), - ("wastebasket", {'code': '1f5d1', 'aliases': ['trash_can']}), - ("watch", {'code': '231a', 'aliases': []}), - ("water_buffalo", {'code': '1f403', 'aliases': []}), - ("water_polo", {'code': '1f93d', 'aliases': []}), - ("watermelon", {'code': '1f349', 'aliases': []}), - ("wave", {'code': '1f44b', 'aliases': ['hello', 'hi']}), - ("wavy_dash", {'code': '3030', 'aliases': []}), - ("waxing_crescent_moon", {'code': '1f312', 'aliases': ['waxing']}), - ("waxing_moon", {'code': '1f314', 'aliases': []}), - ("wc", {'code': '1f6be', 'aliases': ['water_closet']}), - ("weary", {'code': '1f629', 'aliases': ['distraught']}), - ("web", {'code': '1f578', 'aliases': ['spider_web']}), - ("wedding", {'code': '1f492', 'aliases': []}), - ("whale", {'code': '1f433', 'aliases': []}), - ("wheel", {'code': '1f6de', 'aliases': ['tire', 'turn']}), - ("wheel_of_dharma", {'code': '2638', 'aliases': ['buddhism']}), - ("white_and_black_square", {'code': '1f532', 'aliases': []}), - ("white_cane", {'code': '1f9af', 'aliases': []}), - ("white_circle", {'code': '26aa', 'aliases': []}), - ("white_flag", {'code': '1f3f3', 'aliases': ['surrender']}), - ("white_flower", {'code': '1f4ae', 'aliases': []}), - ("white_heart", {'code': '1f90d', 'aliases': []}), - ("white_large_square", {'code': '2b1c', 'aliases': []}), - ("white_medium_small_square", {'code': '25fd', 'aliases': []}), - ("white_medium_square", {'code': '25fb', 'aliases': []}), - ("white_small_square", {'code': '25ab', 'aliases': []}), - ("wilted_flower", {'code': '1f940', 'aliases': ['crushed']}), - ("wind_chime", {'code': '1f390', 'aliases': []}), - ("window", {'code': '1fa9f', 'aliases': ['frame', 'fresh_air', 'opening', 'transparent', 'view']}), - ("windy", {'code': '1f32c', 'aliases': ['mother_nature']}), - ("wine", {'code': '1f377', 'aliases': []}), - ("wink", {'code': '1f609', 'aliases': []}), - ("wish_tree", {'code': '1f38b', 'aliases': ['tanabata_tree']}), - ("wolf", {'code': '1f43a', 'aliases': []}), - ("woman", {'code': '1f469', 'aliases': []}), - ("woman_artist", {'code': '1f469-200d-1f3a8', 'aliases': []}), - ("woman_astronaut", {'code': '1f469-200d-1f680', 'aliases': []}), - ("woman_bald", {'code': '1f469-200d-1f9b2', 'aliases': []}), - ("woman_beard", {'code': '1f9d4-200d-2640', 'aliases': []}), - ("woman_biking", {'code': '1f6b4-200d-2640', 'aliases': []}), - ("woman_blond_hair", {'code': '1f471-200d-2640', 'aliases': ['blond_haired_woman', 'blonde']}), - ("woman_bouncing_ball", {'code': '26f9-fe0f-200d-2640-fe0f', 'aliases': []}), - ("woman_bowing", {'code': '1f647-200d-2640', 'aliases': []}), - ("woman_cartwheeling", {'code': '1f938-200d-2640', 'aliases': []}), - ("woman_climbing", {'code': '1f9d7-200d-2640', 'aliases': []}), - ("woman_construction_worker", {'code': '1f477-200d-2640', 'aliases': []}), - ("woman_cook", {'code': '1f469-200d-1f373', 'aliases': []}), - ("woman_curly_hair", {'code': '1f469-200d-1f9b1', 'aliases': []}), - ("woman_detective", {'code': '1f575-fe0f-200d-2640-fe0f', 'aliases': []}), - ("woman_elf", {'code': '1f9dd-200d-2640', 'aliases': []}), - ("woman_facepalming", {'code': '1f926-200d-2640', 'aliases': []}), - ("woman_factory_worker", {'code': '1f469-200d-1f3ed', 'aliases': []}), - ("woman_fairy", {'code': '1f9da-200d-2640', 'aliases': []}), - ("woman_farmer", {'code': '1f469-200d-1f33e', 'aliases': []}), - ("woman_feeding_baby", {'code': '1f469-200d-1f37c', 'aliases': []}), - ("woman_firefighter", {'code': '1f469-200d-1f692', 'aliases': []}), - ("woman_frowning", {'code': '1f64d-200d-2640', 'aliases': []}), - ("woman_genie", {'code': '1f9de-200d-2640', 'aliases': []}), - ("woman_gesturing_no", {'code': '1f645-200d-2640', 'aliases': []}), - ("woman_gesturing_ok", {'code': '1f646-200d-2640', 'aliases': []}), - ("woman_getting_haircut", {'code': '1f487-200d-2640', 'aliases': []}), - ("woman_getting_massage", {'code': '1f486-200d-2640', 'aliases': []}), - ("woman_golfing", {'code': '1f3cc-fe0f-200d-2640-fe0f', 'aliases': []}), - ("woman_guard", {'code': '1f482-200d-2640', 'aliases': []}), - ("woman_health_worker", {'code': '1f469-200d-2695', 'aliases': []}), - ("woman_in_lotus_position", {'code': '1f9d8-200d-2640', 'aliases': []}), - ("woman_in_manual_wheelchair", {'code': '1f469-200d-1f9bd', 'aliases': []}), - ("woman_in_motorized_wheelchair", {'code': '1f469-200d-1f9bc', 'aliases': []}), - ("woman_in_steamy_room", {'code': '1f9d6-200d-2640', 'aliases': []}), - ("woman_in_tuxedo", {'code': '1f935-200d-2640', 'aliases': []}), - ("woman_judge", {'code': '1f469-200d-2696', 'aliases': []}), - ("woman_juggling", {'code': '1f939-200d-2640', 'aliases': []}), - ("woman_kneeling", {'code': '1f9ce-200d-2640', 'aliases': []}), - ("woman_lifting_weights", {'code': '1f3cb-fe0f-200d-2640-fe0f', 'aliases': []}), - ("woman_mage", {'code': '1f9d9-200d-2640', 'aliases': []}), - ("woman_mechanic", {'code': '1f469-200d-1f527', 'aliases': []}), - ("woman_mountain_biking", {'code': '1f6b5-200d-2640', 'aliases': []}), - ("woman_office_worker", {'code': '1f469-200d-1f4bc', 'aliases': []}), - ("woman_pilot", {'code': '1f469-200d-2708', 'aliases': []}), - ("woman_playing_handball", {'code': '1f93e-200d-2640', 'aliases': []}), - ("woman_playing_water_polo", {'code': '1f93d-200d-2640', 'aliases': []}), - ("woman_police_officer", {'code': '1f46e-200d-2640', 'aliases': []}), - ("woman_pouting", {'code': '1f64e-200d-2640', 'aliases': []}), - ("woman_raising_hand", {'code': '1f64b-200d-2640', 'aliases': []}), - ("woman_red_hair", {'code': '1f469-200d-1f9b0', 'aliases': []}), - ("woman_rowing_boat", {'code': '1f6a3-200d-2640', 'aliases': []}), - ("woman_running", {'code': '1f3c3-200d-2640', 'aliases': []}), - ("woman_scientist", {'code': '1f469-200d-1f52c', 'aliases': []}), - ("woman_shrugging", {'code': '1f937-200d-2640', 'aliases': []}), - ("woman_singer", {'code': '1f469-200d-1f3a4', 'aliases': []}), - ("woman_standing", {'code': '1f9cd-200d-2640', 'aliases': []}), - ("woman_student", {'code': '1f469-200d-1f393', 'aliases': []}), - ("woman_superhero", {'code': '1f9b8-200d-2640', 'aliases': []}), - ("woman_supervillain", {'code': '1f9b9-200d-2640', 'aliases': []}), - ("woman_surfing", {'code': '1f3c4-200d-2640', 'aliases': []}), - ("woman_swimming", {'code': '1f3ca-200d-2640', 'aliases': []}), - ("woman_teacher", {'code': '1f469-200d-1f3eb', 'aliases': []}), - ("woman_technologist", {'code': '1f469-200d-1f4bb', 'aliases': []}), - ("woman_tipping_hand", {'code': '1f481-200d-2640', 'aliases': []}), - ("woman_vampire", {'code': '1f9db-200d-2640', 'aliases': []}), - ("woman_walking", {'code': '1f6b6-200d-2640', 'aliases': []}), - ("woman_wearing_turban", {'code': '1f473-200d-2640', 'aliases': []}), - ("woman_white_hair", {'code': '1f469-200d-1f9b3', 'aliases': []}), - ("woman_with_headscarf", {'code': '1f9d5', 'aliases': ['headscarf', 'hijab', 'mantilla', 'tichel']}), - ("woman_with_veil", {'code': '1f470-200d-2640', 'aliases': []}), - ("woman_with_white_cane", {'code': '1f469-200d-1f9af', 'aliases': []}), - ("woman_zombie", {'code': '1f9df-200d-2640', 'aliases': []}), - ("women_with_bunny_ears", {'code': '1f46f-200d-2640', 'aliases': []}), - ("women_wrestling", {'code': '1f93c-200d-2640', 'aliases': []}), - ("womens", {'code': '1f6ba', 'aliases': []}), - ("wood", {'code': '1fab5', 'aliases': ['log', 'timber']}), - ("woozy_face", {'code': '1f974', 'aliases': ['intoxicated', 'tipsy', 'uneven_eyes', 'wavy_mouth']}), - ("work_in_progress", {'code': '1f6a7', 'aliases': ['construction_zone']}), - ("working_on_it", {'code': '1f6e0', 'aliases': ['hammer_and_wrench', 'tools']}), - ("worm", {'code': '1fab1', 'aliases': ['annelid', 'earthworm', 'parasite']}), - ("worried", {'code': '1f61f', 'aliases': []}), - ("wrestling", {'code': '1f93c', 'aliases': []}), - ("writing", {'code': '270d', 'aliases': []}), - ("www", {'code': '1f310', 'aliases': ['globe']}), - ("x", {'code': '274e', 'aliases': []}), - ("x_ray", {'code': '1fa7b', 'aliases': ['bones', 'medical']}), - ("yam", {'code': '1f360', 'aliases': ['sweet_potato']}), - ("yarn", {'code': '1f9f6', 'aliases': ['crochet', 'knit']}), - ("yawning_face", {'code': '1f971', 'aliases': ['bored', 'yawn']}), - ("yellow_circle", {'code': '1f7e1', 'aliases': ['yellow']}), - ("yellow_heart", {'code': '1f49b', 'aliases': ['heart_of_gold']}), - ("yellow_large_square", {'code': '1f7e8', 'aliases': []}), - ("yen_banknotes", {'code': '1f4b4', 'aliases': []}), - ("yin_yang", {'code': '262f', 'aliases': []}), - ("yo_yo", {'code': '1fa80', 'aliases': ['fluctuate']}), - ("yum", {'code': '1f60b', 'aliases': []}), - ("zany_face", {'code': '1f92a', 'aliases': ['goofy', 'small']}), - ("zebra", {'code': '1f993', 'aliases': ['stripe']}), - ("zero", {'code': '0030-20e3', 'aliases': []}), - ("zombie", {'code': '1f9df', 'aliases': []}), - ("zzz", {'code': '1f4a4', 'aliases': []}), - ] -) +EMOJI_DATA = { + "+1": {'code': '1f44d', 'aliases': ['thumbs_up', 'like']}, + "-1": {'code': '1f44e', 'aliases': ['thumbs_down']}, + "100": {'code': '1f4af', 'aliases': ['hundred']}, + "1234": {'code': '1f522', 'aliases': ['numbers']}, + "a": {'code': '1f170', 'aliases': []}, + "ab": {'code': '1f18e', 'aliases': []}, + "abacus": {'code': '1f9ee', 'aliases': ['calculation']}, + "abc": {'code': '1f524', 'aliases': []}, + "abcd": {'code': '1f521', 'aliases': ['alphabet']}, + "accessible": {'code': '267f', 'aliases': ['wheelchair', 'disabled']}, + "accordion": {'code': '1fa97', 'aliases': ['concertina', 'squeeze_box']}, + "action": {'code': '1f3ac', 'aliases': []}, + "adhesive_bandage": {'code': '1fa79', 'aliases': ['bandage']}, + "aerial_tramway": {'code': '1f6a1', 'aliases': ['ski_lift']}, + "airplane": {'code': '2708', 'aliases': []}, + "alarm_clock": {'code': '23f0', 'aliases': []}, + "alchemy": {'code': '2697', 'aliases': ['alembic']}, + "alien": {'code': '1f47d', 'aliases': ['ufo']}, + "ambulance": {'code': '1f691', 'aliases': []}, + "american_football": {'code': '1f3c8', 'aliases': []}, + "anatomical_heart": {'code': '1fac0', 'aliases': ['anatomical', 'cardiology', 'pulse']}, + "anchor": {'code': '2693', 'aliases': []}, + "angel": {'code': '1f47c', 'aliases': []}, + "anger": {'code': '1f4a2', 'aliases': ['bam', 'pow']}, + "anger_bubble": {'code': '1f5ef', 'aliases': []}, + "angry": {'code': '1f620', 'aliases': []}, + "angry_cat": {'code': '1f63e', 'aliases': ['pouting_cat']}, + "anguish": {'code': '1f62b', 'aliases': []}, + "anguished": {'code': '1f627', 'aliases': ['pained']}, + "ant": {'code': '1f41c', 'aliases': []}, + "apple": {'code': '1f34e', 'aliases': []}, + "aquarius": {'code': '2652', 'aliases': []}, + "arabian_camel": {'code': '1f42a', 'aliases': []}, + "archive": {'code': '1f5c3', 'aliases': []}, + "aries": {'code': '2648', 'aliases': []}, + "art": {'code': '1f3a8', 'aliases': ['palette', 'painting']}, + "artist": {'code': '1f9d1-200d-1f3a8', 'aliases': []}, + "asterisk": {'code': '002a-20e3', 'aliases': []}, + "astonished": {'code': '1f632', 'aliases': []}, + "astronaut": {'code': '1f9d1-200d-1f680', 'aliases': []}, + "at_work": {'code': '2692', 'aliases': ['hammer_and_pick']}, + "athletic_shoe": {'code': '1f45f', 'aliases': ['sneaker', 'running_shoe']}, + "atm": {'code': '1f3e7', 'aliases': []}, + "atom": {'code': '269b', 'aliases': ['physics']}, + "auto_rickshaw": {'code': '1f6fa', 'aliases': ['tuk_tuk']}, + "avocado": {'code': '1f951', 'aliases': []}, + "axe": {'code': '1fa93', 'aliases': ['hatchet', 'split']}, + "b": {'code': '1f171', 'aliases': []}, + "baby": {'code': '1f476', 'aliases': []}, + "baby_bottle": {'code': '1f37c', 'aliases': []}, + "baby_change_station": {'code': '1f6bc', 'aliases': ['nursery']}, + "back": {'code': '1f519', 'aliases': []}, + "backpack": {'code': '1f392', 'aliases': ['satchel']}, + "bacon": {'code': '1f953', 'aliases': []}, + "badger": {'code': '1f9a1', 'aliases': ['honey_badger', 'pester']}, + "badminton": {'code': '1f3f8', 'aliases': []}, + "bagel": {'code': '1f96f', 'aliases': ['schmear']}, + "baggage_claim": {'code': '1f6c4', 'aliases': []}, + "baguette": {'code': '1f956', 'aliases': []}, + "ball": {'code': '26f9', 'aliases': ['sports']}, + "ballet_shoes": {'code': '1fa70', 'aliases': ['ballet']}, + "balloon": {'code': '1f388', 'aliases': ['celebration']}, + "ballot_box": {'code': '1f5f3', 'aliases': []}, + "bamboo": {'code': '1f38d', 'aliases': []}, + "banana": {'code': '1f34c', 'aliases': []}, + "bangbang": {'code': '203c', 'aliases': ['double_exclamation']}, + "banjo": {'code': '1fa95', 'aliases': ['stringed']}, + "bank": {'code': '1f3e6', 'aliases': []}, + "bar_chart": {'code': '1f4ca', 'aliases': []}, + "barber": {'code': '1f488', 'aliases': ['striped_pole']}, + "baseball": {'code': '26be', 'aliases': []}, + "basket": {'code': '1f9fa', 'aliases': ['farming', 'laundry', 'picnic']}, + "basketball": {'code': '1f3c0', 'aliases': []}, + "bat": {'code': '1f987', 'aliases': []}, + "bath": {'code': '1f6c0', 'aliases': []}, + "bathtub": {'code': '1f6c1', 'aliases': []}, + "battery": {'code': '1f50b', 'aliases': ['full_battery']}, + "beach": {'code': '1f3d6', 'aliases': []}, + "beach_umbrella": {'code': '26f1', 'aliases': []}, + "beans": {'code': '1fad8', 'aliases': ['kidney', 'legume']}, + "bear": {'code': '1f43b', 'aliases': []}, + "beaver": {'code': '1f9ab', 'aliases': ['dam']}, + "bed": {'code': '1f6cf', 'aliases': ['bedroom']}, + "bee": {'code': '1f41d', 'aliases': ['buzz', 'honeybee']}, + "beer": {'code': '1f37a', 'aliases': []}, + "beers": {'code': '1f37b', 'aliases': []}, + "beetle": {'code': '1fab2', 'aliases': []}, + "beginner": {'code': '1f530', 'aliases': []}, + "bell_pepper": {'code': '1fad1', 'aliases': ['capsicum', 'pepper', 'vegetable']}, + "bellhop_bell": {'code': '1f6ce', 'aliases': ['reception', 'services', 'ding']}, + "bento": {'code': '1f371', 'aliases': []}, + "beverage_box": {'code': '1f9c3', 'aliases': ['beverage', 'box', 'straw']}, + "big_smile": {'code': '1f604', 'aliases': []}, + "bike": {'code': '1f6b2', 'aliases': ['bicycle']}, + "bikini": {'code': '1f459', 'aliases': []}, + "billed_cap": {'code': '1f9e2', 'aliases': ['baseball_cap']}, + "billiards": {'code': '1f3b1', 'aliases': ['pool', '8_ball']}, + "biohazard": {'code': '2623', 'aliases': []}, + "bird": {'code': '1f426', 'aliases': []}, + "birthday": {'code': '1f382', 'aliases': []}, + "bison": {'code': '1f9ac', 'aliases': ['buffalo', 'herd', 'wisent']}, + "biting_lip": {'code': '1fae6', 'aliases': ['flirting', 'uncomfortable']}, + "black_and_white_square": {'code': '1f533', 'aliases': []}, + "black_belt": {'code': '1f94b', 'aliases': ['keikogi', 'dogi', 'martial_arts']}, + "black_cat": {'code': '1f408-200d-2b1b', 'aliases': ['black', 'unlucky']}, + "black_circle": {'code': '26ab', 'aliases': []}, + "black_flag": {'code': '1f3f4', 'aliases': []}, + "black_heart": {'code': '1f5a4', 'aliases': []}, + "black_large_square": {'code': '2b1b', 'aliases': []}, + "black_medium_small_square": {'code': '25fe', 'aliases': []}, + "black_medium_square": {'code': '25fc', 'aliases': []}, + "black_nib": {'code': '2712', 'aliases': ['nib']}, + "black_small_square": {'code': '25aa', 'aliases': []}, + "blossom": {'code': '1f33c', 'aliases': []}, + "blowfish": {'code': '1f421', 'aliases': []}, + "blue_book": {'code': '1f4d8', 'aliases': []}, + "blue_circle": {'code': '1f535', 'aliases': []}, + "blue_heart": {'code': '1f499', 'aliases': []}, + "blue_square": {'code': '1f7e6', 'aliases': []}, + "blueberries": {'code': '1fad0', 'aliases': ['berry', 'bilberry', 'blueberry']}, + "blush": {'code': '1f60a', 'aliases': []}, + "boar": {'code': '1f417', 'aliases': []}, + "boat": {'code': '26f5', 'aliases': ['sailboat']}, + "bomb": {'code': '1f4a3', 'aliases': []}, + "bone": {'code': '1f9b4', 'aliases': []}, + "book": {'code': '1f4d6', 'aliases': ['open_book']}, + "bookmark": {'code': '1f516', 'aliases': []}, + "books": {'code': '1f4da', 'aliases': []}, + "boom": {'code': '1f4a5', 'aliases': ['explosion', 'crash', 'collision']}, + "boomerang": {'code': '1fa83', 'aliases': ['rebound', 'repercussion']}, + "boot": {'code': '1f462', 'aliases': []}, + "bouquet": {'code': '1f490', 'aliases': []}, + "bow": {'code': '1f647', 'aliases': []}, + "bow_and_arrow": {'code': '1f3f9', 'aliases': ['archery']}, + "bowl_with_spoon": {'code': '1f963', 'aliases': ['cereal', 'congee']}, + "boxing_glove": {'code': '1f94a', 'aliases': []}, + "boy": {'code': '1f466', 'aliases': []}, + "brain": {'code': '1f9e0', 'aliases': ['intelligent']}, + "bread": {'code': '1f35e', 'aliases': []}, + "breast_feeding": {'code': '1f931', 'aliases': ['breast']}, + "brick": {'code': '1f9f1', 'aliases': ['bricks', 'clay', 'mortar', 'wall']}, + "bride": {'code': '1f470', 'aliases': []}, + "bridge": {'code': '1f309', 'aliases': []}, + "briefcase": {'code': '1f4bc', 'aliases': []}, + "briefs": {'code': '1fa72', 'aliases': ['one_piece', 'swimsuit']}, + "brightness": {'code': '1f506', 'aliases': ['high_brightness']}, + "broccoli": {'code': '1f966', 'aliases': ['wild_cabbage']}, + "broken_heart": {'code': '1f494', 'aliases': ['heartache']}, + "broom": {'code': '1f9f9', 'aliases': ['sweeping']}, + "brown_circle": {'code': '1f7e4', 'aliases': []}, + "brown_heart": {'code': '1f90e', 'aliases': []}, + "brown_square": {'code': '1f7eb', 'aliases': []}, + "bubble_tea": {'code': '1f9cb', 'aliases': []}, + "bubbles": {'code': '1fae7', 'aliases': ['burp', 'underwater']}, + "bucket": {'code': '1faa3', 'aliases': ['cask', 'pail', 'vat']}, + "bug": {'code': '1f41b', 'aliases': ['caterpillar']}, + "bullet_train": {'code': '1f685', 'aliases': []}, + "bunny": {'code': '1f430', 'aliases': []}, + "burrito": {'code': '1f32f', 'aliases': []}, + "bus": {'code': '1f68c', 'aliases': ['school_bus']}, + "bus_stop": {'code': '1f68f', 'aliases': []}, + "butter": {'code': '1f9c8', 'aliases': ['dairy']}, + "butterfly": {'code': '1f98b', 'aliases': []}, + "cactus": {'code': '1f335', 'aliases': []}, + "cake": {'code': '1f370', 'aliases': []}, + "calendar": {'code': '1f4c5', 'aliases': []}, + "calf": {'code': '1f42e', 'aliases': []}, + "call_me": {'code': '1f919', 'aliases': []}, + "calling": {'code': '1f4f2', 'aliases': []}, + "camel": {'code': '1f42b', 'aliases': []}, + "camera": {'code': '1f4f7', 'aliases': []}, + "campsite": {'code': '1f3d5', 'aliases': []}, + "cancer": {'code': '264b', 'aliases': []}, + "candle": {'code': '1f56f', 'aliases': []}, + "candy": {'code': '1f36c', 'aliases': []}, + "canned_food": {'code': '1f96b', 'aliases': ['can']}, + "canoe": {'code': '1f6f6', 'aliases': []}, + "capital_abcd": {'code': '1f520', 'aliases': ['capital_letters']}, + "capricorn": {'code': '2651', 'aliases': []}, + "car": {'code': '1f697', 'aliases': []}, + "carousel": {'code': '1f3a0', 'aliases': ['merry_go_round']}, + "carp_streamer": {'code': '1f38f', 'aliases': ['flags']}, + "carpenter_square": {'code': '1f4d0', 'aliases': ['triangular_ruler']}, + "carpentry_saw": {'code': '1fa9a', 'aliases': ['carpenter', 'saw']}, + "carrot": {'code': '1f955', 'aliases': []}, + "cartwheel": {'code': '1f938', 'aliases': ['acrobatics', 'gymnastics', 'tumbling']}, + "castle": {'code': '1f3f0', 'aliases': []}, + "cat": {'code': '1f408', 'aliases': ['meow']}, + "cd": {'code': '1f4bf', 'aliases': []}, + "cell_reception": {'code': '1f4f6', 'aliases': ['signal_strength', 'signal_bars']}, + "chains": {'code': '26d3', 'aliases': []}, + "chair": {'code': '1fa91', 'aliases': ['sit']}, + "champagne": {'code': '1f37e', 'aliases': []}, + "chart": {'code': '1f4c8', 'aliases': ['upwards_trend', 'growing', 'increasing']}, + "check": {'code': '2705', 'aliases': ['all_good', 'approved']}, + "check_mark": {'code': '2714', 'aliases': []}, + "checkbox": {'code': '2611', 'aliases': []}, + "checkered_flag": {'code': '1f3c1', 'aliases': ['race', 'go', 'start']}, + "cheese": {'code': '1f9c0', 'aliases': []}, + "cherries": {'code': '1f352', 'aliases': []}, + "cherry_blossom": {'code': '1f338', 'aliases': []}, + "chess_pawn": {'code': '265f', 'aliases': ['chess', 'dupe', 'expendable']}, + "chestnut": {'code': '1f330', 'aliases': []}, + "chick": {'code': '1f424', 'aliases': ['baby_chick']}, + "chicken": {'code': '1f414', 'aliases': ['cluck']}, + "child": {'code': '1f9d2', 'aliases': ['young']}, + "children_crossing": {'code': '1f6b8', 'aliases': ['school_crossing', 'drive_with_care']}, + "chipmunk": {'code': '1f43f', 'aliases': []}, + "chocolate": {'code': '1f36b', 'aliases': []}, + "chopsticks": {'code': '1f962', 'aliases': ['hashi']}, + "church": {'code': '26ea', 'aliases': []}, + "cinema": {'code': '1f3a6', 'aliases': ['movie_theater']}, + "circle": {'code': '2b55', 'aliases': []}, + "circus": {'code': '1f3aa', 'aliases': []}, + "city": {'code': '1f3d9', 'aliases': ['skyline']}, + "city_sunrise": {'code': '1f307', 'aliases': []}, + "cl": {'code': '1f191', 'aliases': []}, + "clap": {'code': '1f44f', 'aliases': ['applause']}, + "classical_building": {'code': '1f3db', 'aliases': []}, + "clink": {'code': '1f942', 'aliases': ['toast']}, + "clipboard": {'code': '1f4cb', 'aliases': []}, + "clockwise": {'code': '1f503', 'aliases': []}, + "closed_mailbox": {'code': '1f4ea', 'aliases': []}, + "closed_umbrella": {'code': '1f302', 'aliases': []}, + "clothing": {'code': '1f45a', 'aliases': []}, + "cloud": {'code': '2601', 'aliases': ['overcast']}, + "cloudy": {'code': '1f325', 'aliases': []}, + "clown": {'code': '1f921', 'aliases': []}, + "clubs": {'code': '2663', 'aliases': []}, + "coat": {'code': '1f9e5', 'aliases': ['jacket']}, + "cockroach": {'code': '1fab3', 'aliases': ['roach']}, + "cocktail": {'code': '1f378', 'aliases': []}, + "coconut": {'code': '1f965', 'aliases': ['piña_colada', 'pina_colada']}, + "coffee": {'code': '2615', 'aliases': []}, + "coffin": {'code': '26b0', 'aliases': ['burial', 'grave']}, + "coin": {'code': '1fa99', 'aliases': ['metal']}, + "cold_face": {'code': '1f976', 'aliases': ['blue_faced', 'freezing', 'frostbite', 'icicles']}, + "cold_sweat": {'code': '1f630', 'aliases': []}, + "comet": {'code': '2604', 'aliases': ['meteor']}, + "compass": {'code': '1f9ed', 'aliases': ['navigation', 'orienteering']}, + "compression": {'code': '1f5dc', 'aliases': ['vise']}, + "computer": {'code': '1f4bb', 'aliases': ['laptop']}, + "computer_mouse": {'code': '1f5b1', 'aliases': []}, + "confetti": {'code': '1f38a', 'aliases': ['party_ball']}, + "confounded": {'code': '1f616', 'aliases': ['agony']}, + "construction": {'code': '1f3d7', 'aliases': []}, + "construction_worker": {'code': '1f477', 'aliases': []}, + "control_knobs": {'code': '1f39b', 'aliases': []}, + "convenience_store": {'code': '1f3ea', 'aliases': []}, + "cook": {'code': '1f9d1-200d-1f373', 'aliases': []}, + "cookie": {'code': '1f36a', 'aliases': []}, + "cooking": {'code': '1f373', 'aliases': []}, + "cool": {'code': '1f192', 'aliases': []}, + "copyright": {'code': '00a9', 'aliases': ['c']}, + "coral": {'code': '1fab8', 'aliases': ['reef']}, + "corn": {'code': '1f33d', 'aliases': ['maize']}, + "counterclockwise": {'code': '1f504', 'aliases': ['return']}, + "couple_with_heart": {'code': '1f491', 'aliases': []}, + "couple_with_heart_man_man": {'code': '1f468-200d-2764-200d-1f468', 'aliases': []}, + "couple_with_heart_woman_man": {'code': '1f469-200d-2764-200d-1f468', 'aliases': []}, + "couple_with_heart_woman_woman": {'code': '1f469-200d-2764-200d-1f469', 'aliases': []}, + "cow": {'code': '1f404', 'aliases': []}, + "cowboy": {'code': '1f920', 'aliases': []}, + "crab": {'code': '1f980', 'aliases': []}, + "crayon": {'code': '1f58d', 'aliases': []}, + "credit_card": {'code': '1f4b3', 'aliases': ['debit_card']}, + "cricket": {'code': '1f997', 'aliases': ['grasshopper']}, + "cricket_game": {'code': '1f3cf', 'aliases': []}, + "crocodile": {'code': '1f40a', 'aliases': []}, + "croissant": {'code': '1f950', 'aliases': []}, + "cross": {'code': '271d', 'aliases': ['christianity']}, + "cross_mark": {'code': '274c', 'aliases': ['incorrect', 'wrong']}, + "crossed_flags": {'code': '1f38c', 'aliases': ['solidarity']}, + "crown": {'code': '1f451', 'aliases': ['queen', 'king']}, + "crutch": {'code': '1fa7c', 'aliases': ['cane', 'disability', 'mobility_aid']}, + "cry": {'code': '1f622', 'aliases': []}, + "crying_cat": {'code': '1f63f', 'aliases': []}, + "crystal_ball": {'code': '1f52e', 'aliases': ['oracle', 'future', 'fortune_telling']}, + "cucumber": {'code': '1f952', 'aliases': []}, + "cup_with_straw": {'code': '1f964', 'aliases': ['soda']}, + "cupcake": {'code': '1f9c1', 'aliases': []}, + "cupid": {'code': '1f498', 'aliases': ['smitten', 'heart_arrow']}, + "curling_stone": {'code': '1f94c', 'aliases': []}, + "curry": {'code': '1f35b', 'aliases': []}, + "custard": {'code': '1f36e', 'aliases': ['flan']}, + "customs": {'code': '1f6c3', 'aliases': []}, + "cut_of_meat": {'code': '1f969', 'aliases': ['lambchop', 'porkchop', 'steak']}, + "cute": {'code': '1f4a0', 'aliases': ['kawaii', 'diamond_with_a_dot']}, + "cyclist": {'code': '1f6b4', 'aliases': []}, + "cyclone": {'code': '1f300', 'aliases': ['hurricane', 'typhoon']}, + "dagger": {'code': '1f5e1', 'aliases': ['rated_for_violence']}, + "dancer": {'code': '1f483', 'aliases': []}, + "dancers": {'code': '1f46f', 'aliases': []}, + "dancing": {'code': '1f57a', 'aliases': ['disco']}, + "dango": {'code': '1f361', 'aliases': []}, + "dark_sunglasses": {'code': '1f576', 'aliases': []}, + "dash": {'code': '1f4a8', 'aliases': []}, + "date": {'code': '1f4c6', 'aliases': []}, + "deaf_man": {'code': '1f9cf-200d-2642', 'aliases': []}, + "deaf_person": {'code': '1f9cf', 'aliases': ['hear']}, + "deaf_woman": {'code': '1f9cf-200d-2640', 'aliases': []}, + "decorative_notebook": {'code': '1f4d4', 'aliases': []}, + "deer": {'code': '1f98c', 'aliases': []}, + "department_store": {'code': '1f3ec', 'aliases': []}, + "derelict_house": {'code': '1f3da', 'aliases': ['condemned']}, + "desert": {'code': '1f3dc', 'aliases': []}, + "desktop_computer": {'code': '1f5a5', 'aliases': []}, + "detective": {'code': '1f575', 'aliases': ['spy', 'sleuth', 'agent', 'sneaky']}, + "devil": {'code': '1f47f', 'aliases': ['imp', 'angry_devil']}, + "diamonds": {'code': '2666', 'aliases': []}, + "dice": {'code': '1f3b2', 'aliases': ['die']}, + "direct_hit": {'code': '1f3af', 'aliases': ['darts', 'bulls_eye']}, + "disappointed": {'code': '1f61e', 'aliases': []}, + "disguised_face": {'code': '1f978', 'aliases': ['disguise', 'incognito']}, + "diving_mask": {'code': '1f93f', 'aliases': ['scuba', 'snorkeling']}, + "division": {'code': '2797', 'aliases': ['divide']}, + "diya_lamp": {'code': '1fa94', 'aliases': ['diya', 'lamp', 'oil']}, + "dizzy": {'code': '1f635', 'aliases': []}, + "dna": {'code': '1f9ec', 'aliases': ['evolution', 'gene', 'genetics', 'life']}, + "do_not_litter": {'code': '1f6af', 'aliases': []}, + "document": {'code': '1f4c4', 'aliases': ['paper', 'file', 'page']}, + "dodo": {'code': '1f9a4', 'aliases': ['mauritius']}, + "dog": {'code': '1f415', 'aliases': ['woof']}, + "dollar_bills": {'code': '1f4b5', 'aliases': []}, + "dollars": {'code': '1f4b2', 'aliases': []}, + "dolls": {'code': '1f38e', 'aliases': []}, + "dolphin": {'code': '1f42c', 'aliases': ['flipper']}, + "doner_kebab": {'code': '1f959', 'aliases': ['shawarma', 'souvlaki', 'stuffed_flatbread']}, + "donut": {'code': '1f369', 'aliases': ['doughnut']}, + "door": {'code': '1f6aa', 'aliases': []}, + "dormouse": {'code': '1f42d', 'aliases': []}, + "dotted_line_face": {'code': '1fae5', 'aliases': ['depressed', 'hide', 'introvert', 'invisible']}, + "dotted_six_pointed_star": {'code': '1f52f', 'aliases': ['fortune']}, + "double_down": {'code': '23ec', 'aliases': ['fast_down']}, + "double_loop": {'code': '27bf', 'aliases': ['voicemail']}, + "double_up": {'code': '23eb', 'aliases': ['fast_up']}, + "dove": {'code': '1f54a', 'aliases': ['dove_of_peace']}, + "down": {'code': '2b07', 'aliases': ['south']}, + "downvote": {'code': '1f53d', 'aliases': ['down_button', 'decrease']}, + "downwards_trend": {'code': '1f4c9', 'aliases': ['shrinking', 'decreasing']}, + "dragon": {'code': '1f409', 'aliases': []}, + "dragon_face": {'code': '1f432', 'aliases': []}, + "dress": {'code': '1f457', 'aliases': []}, + "drooling": {'code': '1f924', 'aliases': []}, + "drop": {'code': '1f4a7', 'aliases': ['water_drop']}, + "drop_of_blood": {'code': '1fa78', 'aliases': ['bleed', 'blood_donation', 'injury', 'menstruation']}, + "drum": {'code': '1f941', 'aliases': []}, + "drumstick": {'code': '1f357', 'aliases': ['poultry']}, + "duck": {'code': '1f986', 'aliases': []}, + "duel": {'code': '2694', 'aliases': ['swords']}, + "dumpling": {'code': '1f95f', 'aliases': ['empanada', 'gyōza', 'jiaozi', 'pierogi', 'potsticker', 'gyoza']}, + "dvd": {'code': '1f4c0', 'aliases': []}, + "e-mail": {'code': '1f4e7', 'aliases': []}, + "eagle": {'code': '1f985', 'aliases': []}, + "ear": {'code': '1f442', 'aliases': []}, + "ear_with_hearing_aid": {'code': '1f9bb', 'aliases': ['hard_of_hearing']}, + "earth_africa": {'code': '1f30d', 'aliases': []}, + "earth_americas": {'code': '1f30e', 'aliases': []}, + "earth_asia": {'code': '1f30f', 'aliases': []}, + "egg": {'code': '1f95a', 'aliases': []}, + "eggplant": {'code': '1f346', 'aliases': []}, + "eight": {'code': '0038-20e3', 'aliases': []}, + "eight_pointed_star": {'code': '2734', 'aliases': []}, + "eight_spoked_asterisk": {'code': '2733', 'aliases': []}, + "eject_button": {'code': '23cf', 'aliases': ['eject']}, + "electric_plug": {'code': '1f50c', 'aliases': []}, + "elephant": {'code': '1f418', 'aliases': []}, + "elevator": {'code': '1f6d7', 'aliases': ['hoist']}, + "elf": {'code': '1f9dd', 'aliases': []}, + "email": {'code': '2709', 'aliases': ['envelope', 'mail']}, + "empty_nest": {'code': '1fab9', 'aliases': []}, + "end": {'code': '1f51a', 'aliases': []}, + "euro_banknotes": {'code': '1f4b6', 'aliases': []}, + "evergreen_tree": {'code': '1f332', 'aliases': []}, + "exchange": {'code': '1f4b1', 'aliases': []}, + "exclamation": {'code': '2757', 'aliases': []}, + "exhausted": {'code': '1f625', 'aliases': ['disappointed_relieved', 'stressed']}, + "exploding_head": {'code': '1f92f', 'aliases': ['mind_blown', 'shocked']}, + "expressionless": {'code': '1f611', 'aliases': []}, + "eye": {'code': '1f441', 'aliases': []}, + "eye_in_speech_bubble": {'code': '1f441-fe0f-200d-1f5e8-fe0f', 'aliases': ['speech', 'witness']}, + "eyes": {'code': '1f440', 'aliases': ['looking']}, + "face_exhaling": {'code': '1f62e-200d-1f4a8', 'aliases': ['exhale', 'gasp', 'groan', 'relief', 'whisper', 'whistle']}, + "face_holding_back_tears": {'code': '1f979', 'aliases': ['resist']}, + "face_in_clouds": {'code': '1f636-200d-1f32b', 'aliases': ['absentminded', 'face_in_the_fog', 'head_in_clouds']}, + "face_palm": {'code': '1f926', 'aliases': []}, + "face_vomiting": {'code': '1f92e', 'aliases': ['puke', 'vomit']}, + "face_with_diagonal_mouth": {'code': '1fae4', 'aliases': ['meh', 'skeptical', 'unsure']}, + "face_with_hand_over_mouth": {'code': '1f92d', 'aliases': ['whoops']}, + "face_with_monocle": {'code': '1f9d0', 'aliases': ['monocle', 'stuffy']}, + "face_with_open_eyes_and_hand_over_mouth": {'code': '1fae2', 'aliases': ['amazement', 'awe', 'embarrass']}, + "face_with_peeking_eye": {'code': '1fae3', 'aliases': ['captivated', 'peep', 'stare']}, + "face_with_raised_eyebrow": {'code': '1f928', 'aliases': ['distrust', 'skeptic']}, + "face_with_spiral_eyes": {'code': '1f635-200d-1f4ab', 'aliases': ['hypnotized', 'trouble', 'whoa']}, + "face_with_symbols_on_mouth": {'code': '1f92c', 'aliases': ['swearing']}, + "factory": {'code': '1f3ed', 'aliases': []}, + "factory_worker": {'code': '1f9d1-200d-1f3ed', 'aliases': []}, + "fairy": {'code': '1f9da', 'aliases': []}, + "falafel": {'code': '1f9c6', 'aliases': ['chickpea', 'meatball']}, + "fallen_leaf": {'code': '1f342', 'aliases': []}, + "family": {'code': '1f46a', 'aliases': []}, + "family_man_boy": {'code': '1f468-200d-1f466', 'aliases': []}, + "family_man_boy_boy": {'code': '1f468-200d-1f466-200d-1f466', 'aliases': []}, + "family_man_girl": {'code': '1f468-200d-1f467', 'aliases': []}, + "family_man_girl_boy": {'code': '1f468-200d-1f467-200d-1f466', 'aliases': []}, + "family_man_girl_girl": {'code': '1f468-200d-1f467-200d-1f467', 'aliases': []}, + "family_man_man_boy": {'code': '1f468-200d-1f468-200d-1f466', 'aliases': []}, + "family_man_man_boy_boy": {'code': '1f468-200d-1f468-200d-1f466-200d-1f466', 'aliases': []}, + "family_man_man_girl": {'code': '1f468-200d-1f468-200d-1f467', 'aliases': []}, + "family_man_man_girl_boy": {'code': '1f468-200d-1f468-200d-1f467-200d-1f466', 'aliases': []}, + "family_man_man_girl_girl": {'code': '1f468-200d-1f468-200d-1f467-200d-1f467', 'aliases': []}, + "family_man_woman_boy": {'code': '1f468-200d-1f469-200d-1f466', 'aliases': []}, + "family_man_woman_boy_boy": {'code': '1f468-200d-1f469-200d-1f466-200d-1f466', 'aliases': []}, + "family_man_woman_girl": {'code': '1f468-200d-1f469-200d-1f467', 'aliases': []}, + "family_man_woman_girl_boy": {'code': '1f468-200d-1f469-200d-1f467-200d-1f466', 'aliases': []}, + "family_man_woman_girl_girl": {'code': '1f468-200d-1f469-200d-1f467-200d-1f467', 'aliases': []}, + "family_woman_boy": {'code': '1f469-200d-1f466', 'aliases': []}, + "family_woman_boy_boy": {'code': '1f469-200d-1f466-200d-1f466', 'aliases': []}, + "family_woman_girl": {'code': '1f469-200d-1f467', 'aliases': []}, + "family_woman_girl_boy": {'code': '1f469-200d-1f467-200d-1f466', 'aliases': []}, + "family_woman_girl_girl": {'code': '1f469-200d-1f467-200d-1f467', 'aliases': []}, + "family_woman_woman_boy": {'code': '1f469-200d-1f469-200d-1f466', 'aliases': []}, + "family_woman_woman_boy_boy": {'code': '1f469-200d-1f469-200d-1f466-200d-1f466', 'aliases': []}, + "family_woman_woman_girl": {'code': '1f469-200d-1f469-200d-1f467', 'aliases': []}, + "family_woman_woman_girl_boy": {'code': '1f469-200d-1f469-200d-1f467-200d-1f466', 'aliases': []}, + "family_woman_woman_girl_girl": {'code': '1f469-200d-1f469-200d-1f467-200d-1f467', 'aliases': []}, + "farmer": {'code': '1f9d1-200d-1f33e', 'aliases': []}, + "fast_forward": {'code': '23e9', 'aliases': []}, + "fax": {'code': '1f4e0', 'aliases': []}, + "fear": {'code': '1f628', 'aliases': ['scared', 'shock']}, + "feather": {'code': '1fab6', 'aliases': ['flight', 'light', 'plumage']}, + "female_sign": {'code': '2640', 'aliases': []}, + "fencing": {'code': '1f93a', 'aliases': []}, + "ferris_wheel": {'code': '1f3a1', 'aliases': []}, + "ferry": {'code': '26f4', 'aliases': []}, + "field_hockey": {'code': '1f3d1', 'aliases': []}, + "file_cabinet": {'code': '1f5c4', 'aliases': []}, + "film": {'code': '1f39e', 'aliases': []}, + "fingers_crossed": {'code': '1f91e', 'aliases': []}, + "fire": {'code': '1f525', 'aliases': ['lit', 'hot', 'flame']}, + "fire_extinguisher": {'code': '1f9ef', 'aliases': ['extinguish', 'quench']}, + "fire_truck": {'code': '1f692', 'aliases': ['fire_engine']}, + "firecracker": {'code': '1f9e8', 'aliases': ['dynamite', 'explosive']}, + "firefighter": {'code': '1f9d1-200d-1f692', 'aliases': []}, + "fireworks": {'code': '1f386', 'aliases': []}, + "first_place": {'code': '1f947', 'aliases': ['gold', 'number_one']}, + "first_quarter_moon": {'code': '1f313', 'aliases': []}, + "fish": {'code': '1f41f', 'aliases': []}, + "fishing": {'code': '1f3a3', 'aliases': []}, + "fist": {'code': '270a', 'aliases': ['power']}, + "fist_bump": {'code': '1f44a', 'aliases': ['punch']}, + "five": {'code': '0035-20e3', 'aliases': []}, + "fixing": {'code': '1f527', 'aliases': ['wrench']}, + "flag_afghanistan": {'code': '1f1e6-1f1eb', 'aliases': []}, + "flag_albania": {'code': '1f1e6-1f1f1', 'aliases': []}, + "flag_algeria": {'code': '1f1e9-1f1ff', 'aliases': []}, + "flag_american_samoa": {'code': '1f1e6-1f1f8', 'aliases': []}, + "flag_andorra": {'code': '1f1e6-1f1e9', 'aliases': []}, + "flag_angola": {'code': '1f1e6-1f1f4', 'aliases': []}, + "flag_anguilla": {'code': '1f1e6-1f1ee', 'aliases': []}, + "flag_antarctica": {'code': '1f1e6-1f1f6', 'aliases': []}, + "flag_antigua_and_barbuda": {'code': '1f1e6-1f1ec', 'aliases': []}, + "flag_argentina": {'code': '1f1e6-1f1f7', 'aliases': []}, + "flag_armenia": {'code': '1f1e6-1f1f2', 'aliases': []}, + "flag_aruba": {'code': '1f1e6-1f1fc', 'aliases': []}, + "flag_ascension_island": {'code': '1f1e6-1f1e8', 'aliases': []}, + "flag_australia": {'code': '1f1e6-1f1fa', 'aliases': []}, + "flag_austria": {'code': '1f1e6-1f1f9', 'aliases': []}, + "flag_azerbaijan": {'code': '1f1e6-1f1ff', 'aliases': []}, + "flag_bahamas": {'code': '1f1e7-1f1f8', 'aliases': []}, + "flag_bahrain": {'code': '1f1e7-1f1ed', 'aliases': []}, + "flag_bangladesh": {'code': '1f1e7-1f1e9', 'aliases': []}, + "flag_barbados": {'code': '1f1e7-1f1e7', 'aliases': []}, + "flag_belarus": {'code': '1f1e7-1f1fe', 'aliases': []}, + "flag_belgium": {'code': '1f1e7-1f1ea', 'aliases': []}, + "flag_belize": {'code': '1f1e7-1f1ff', 'aliases': []}, + "flag_benin": {'code': '1f1e7-1f1ef', 'aliases': []}, + "flag_bermuda": {'code': '1f1e7-1f1f2', 'aliases': []}, + "flag_bhutan": {'code': '1f1e7-1f1f9', 'aliases': []}, + "flag_bolivia": {'code': '1f1e7-1f1f4', 'aliases': []}, + "flag_bosnia_and_herzegovina": {'code': '1f1e7-1f1e6', 'aliases': []}, + "flag_botswana": {'code': '1f1e7-1f1fc', 'aliases': []}, + "flag_bouvet_island": {'code': '1f1e7-1f1fb', 'aliases': []}, + "flag_brazil": {'code': '1f1e7-1f1f7', 'aliases': []}, + "flag_british_indian_ocean_territory": {'code': '1f1ee-1f1f4', 'aliases': []}, + "flag_british_virgin_islands": {'code': '1f1fb-1f1ec', 'aliases': []}, + "flag_brunei": {'code': '1f1e7-1f1f3', 'aliases': []}, + "flag_bulgaria": {'code': '1f1e7-1f1ec', 'aliases': []}, + "flag_burkina_faso": {'code': '1f1e7-1f1eb', 'aliases': []}, + "flag_burundi": {'code': '1f1e7-1f1ee', 'aliases': []}, + "flag_cambodia": {'code': '1f1f0-1f1ed', 'aliases': []}, + "flag_cameroon": {'code': '1f1e8-1f1f2', 'aliases': []}, + "flag_canada": {'code': '1f1e8-1f1e6', 'aliases': []}, + "flag_canary_islands": {'code': '1f1ee-1f1e8', 'aliases': []}, + "flag_cape_verde": {'code': '1f1e8-1f1fb', 'aliases': []}, + "flag_caribbean_netherlands": {'code': '1f1e7-1f1f6', 'aliases': []}, + "flag_cayman_islands": {'code': '1f1f0-1f1fe', 'aliases': []}, + "flag_central_african_republic": {'code': '1f1e8-1f1eb', 'aliases': []}, + "flag_ceuta_and_melilla": {'code': '1f1ea-1f1e6', 'aliases': []}, + "flag_chad": {'code': '1f1f9-1f1e9', 'aliases': []}, + "flag_chile": {'code': '1f1e8-1f1f1', 'aliases': []}, + "flag_china": {'code': '1f1e8-1f1f3', 'aliases': []}, + "flag_christmas_island": {'code': '1f1e8-1f1fd', 'aliases': []}, + "flag_clipperton_island": {'code': '1f1e8-1f1f5', 'aliases': []}, + "flag_cocos_keeling_islands": {'code': '1f1e8-1f1e8', 'aliases': []}, + "flag_colombia": {'code': '1f1e8-1f1f4', 'aliases': []}, + "flag_comoros": {'code': '1f1f0-1f1f2', 'aliases': []}, + "flag_congo_brazzaville": {'code': '1f1e8-1f1ec', 'aliases': []}, + "flag_congo_kinshasa": {'code': '1f1e8-1f1e9', 'aliases': []}, + "flag_cook_islands": {'code': '1f1e8-1f1f0', 'aliases': []}, + "flag_costa_rica": {'code': '1f1e8-1f1f7', 'aliases': []}, + "flag_croatia": {'code': '1f1ed-1f1f7', 'aliases': []}, + "flag_cuba": {'code': '1f1e8-1f1fa', 'aliases': []}, + "flag_curaçao": {'code': '1f1e8-1f1fc', 'aliases': ['flag_curacao']}, + "flag_cyprus": {'code': '1f1e8-1f1fe', 'aliases': []}, + "flag_czechia": {'code': '1f1e8-1f1ff', 'aliases': []}, + "flag_côte_divoire": {'code': '1f1e8-1f1ee', 'aliases': ['flag_cote_divoire']}, + "flag_denmark": {'code': '1f1e9-1f1f0', 'aliases': []}, + "flag_diego_garcia": {'code': '1f1e9-1f1ec', 'aliases': []}, + "flag_djibouti": {'code': '1f1e9-1f1ef', 'aliases': []}, + "flag_dominica": {'code': '1f1e9-1f1f2', 'aliases': []}, + "flag_dominican_republic": {'code': '1f1e9-1f1f4', 'aliases': []}, + "flag_ecuador": {'code': '1f1ea-1f1e8', 'aliases': []}, + "flag_egypt": {'code': '1f1ea-1f1ec', 'aliases': []}, + "flag_el_salvador": {'code': '1f1f8-1f1fb', 'aliases': []}, + "flag_england": {'code': '1f3f4-e0067-e0062-e0065-e006e-e0067-e007f', 'aliases': []}, + "flag_equatorial_guinea": {'code': '1f1ec-1f1f6', 'aliases': []}, + "flag_eritrea": {'code': '1f1ea-1f1f7', 'aliases': []}, + "flag_estonia": {'code': '1f1ea-1f1ea', 'aliases': []}, + "flag_eswatini": {'code': '1f1f8-1f1ff', 'aliases': []}, + "flag_ethiopia": {'code': '1f1ea-1f1f9', 'aliases': []}, + "flag_european_union": {'code': '1f1ea-1f1fa', 'aliases': []}, + "flag_falkland_islands": {'code': '1f1eb-1f1f0', 'aliases': []}, + "flag_faroe_islands": {'code': '1f1eb-1f1f4', 'aliases': []}, + "flag_fiji": {'code': '1f1eb-1f1ef', 'aliases': []}, + "flag_finland": {'code': '1f1eb-1f1ee', 'aliases': []}, + "flag_france": {'code': '1f1eb-1f1f7', 'aliases': []}, + "flag_french_guiana": {'code': '1f1ec-1f1eb', 'aliases': []}, + "flag_french_polynesia": {'code': '1f1f5-1f1eb', 'aliases': []}, + "flag_french_southern_territories": {'code': '1f1f9-1f1eb', 'aliases': []}, + "flag_gabon": {'code': '1f1ec-1f1e6', 'aliases': []}, + "flag_gambia": {'code': '1f1ec-1f1f2', 'aliases': []}, + "flag_georgia": {'code': '1f1ec-1f1ea', 'aliases': []}, + "flag_germany": {'code': '1f1e9-1f1ea', 'aliases': []}, + "flag_ghana": {'code': '1f1ec-1f1ed', 'aliases': []}, + "flag_gibraltar": {'code': '1f1ec-1f1ee', 'aliases': []}, + "flag_greece": {'code': '1f1ec-1f1f7', 'aliases': []}, + "flag_greenland": {'code': '1f1ec-1f1f1', 'aliases': []}, + "flag_grenada": {'code': '1f1ec-1f1e9', 'aliases': []}, + "flag_guadeloupe": {'code': '1f1ec-1f1f5', 'aliases': []}, + "flag_guam": {'code': '1f1ec-1f1fa', 'aliases': []}, + "flag_guatemala": {'code': '1f1ec-1f1f9', 'aliases': []}, + "flag_guernsey": {'code': '1f1ec-1f1ec', 'aliases': []}, + "flag_guinea": {'code': '1f1ec-1f1f3', 'aliases': []}, + "flag_guinea_bissau": {'code': '1f1ec-1f1fc', 'aliases': []}, + "flag_guyana": {'code': '1f1ec-1f1fe', 'aliases': []}, + "flag_haiti": {'code': '1f1ed-1f1f9', 'aliases': []}, + "flag_heard_and_mcdonald_islands": {'code': '1f1ed-1f1f2', 'aliases': []}, + "flag_honduras": {'code': '1f1ed-1f1f3', 'aliases': []}, + "flag_hong_kong_sar_china": {'code': '1f1ed-1f1f0', 'aliases': []}, + "flag_hungary": {'code': '1f1ed-1f1fa', 'aliases': []}, + "flag_iceland": {'code': '1f1ee-1f1f8', 'aliases': []}, + "flag_india": {'code': '1f1ee-1f1f3', 'aliases': []}, + "flag_indonesia": {'code': '1f1ee-1f1e9', 'aliases': []}, + "flag_iran": {'code': '1f1ee-1f1f7', 'aliases': []}, + "flag_iraq": {'code': '1f1ee-1f1f6', 'aliases': []}, + "flag_ireland": {'code': '1f1ee-1f1ea', 'aliases': []}, + "flag_isle_of_man": {'code': '1f1ee-1f1f2', 'aliases': []}, + "flag_israel": {'code': '1f1ee-1f1f1', 'aliases': []}, + "flag_italy": {'code': '1f1ee-1f1f9', 'aliases': []}, + "flag_jamaica": {'code': '1f1ef-1f1f2', 'aliases': []}, + "flag_japan": {'code': '1f1ef-1f1f5', 'aliases': []}, + "flag_jersey": {'code': '1f1ef-1f1ea', 'aliases': []}, + "flag_jordan": {'code': '1f1ef-1f1f4', 'aliases': []}, + "flag_kazakhstan": {'code': '1f1f0-1f1ff', 'aliases': []}, + "flag_kenya": {'code': '1f1f0-1f1ea', 'aliases': []}, + "flag_kiribati": {'code': '1f1f0-1f1ee', 'aliases': []}, + "flag_kosovo": {'code': '1f1fd-1f1f0', 'aliases': []}, + "flag_kuwait": {'code': '1f1f0-1f1fc', 'aliases': []}, + "flag_kyrgyzstan": {'code': '1f1f0-1f1ec', 'aliases': []}, + "flag_laos": {'code': '1f1f1-1f1e6', 'aliases': []}, + "flag_latvia": {'code': '1f1f1-1f1fb', 'aliases': []}, + "flag_lebanon": {'code': '1f1f1-1f1e7', 'aliases': []}, + "flag_lesotho": {'code': '1f1f1-1f1f8', 'aliases': []}, + "flag_liberia": {'code': '1f1f1-1f1f7', 'aliases': []}, + "flag_libya": {'code': '1f1f1-1f1fe', 'aliases': []}, + "flag_liechtenstein": {'code': '1f1f1-1f1ee', 'aliases': []}, + "flag_lithuania": {'code': '1f1f1-1f1f9', 'aliases': []}, + "flag_luxembourg": {'code': '1f1f1-1f1fa', 'aliases': []}, + "flag_macao_sar_china": {'code': '1f1f2-1f1f4', 'aliases': []}, + "flag_madagascar": {'code': '1f1f2-1f1ec', 'aliases': []}, + "flag_malawi": {'code': '1f1f2-1f1fc', 'aliases': []}, + "flag_malaysia": {'code': '1f1f2-1f1fe', 'aliases': []}, + "flag_maldives": {'code': '1f1f2-1f1fb', 'aliases': []}, + "flag_mali": {'code': '1f1f2-1f1f1', 'aliases': []}, + "flag_malta": {'code': '1f1f2-1f1f9', 'aliases': []}, + "flag_marshall_islands": {'code': '1f1f2-1f1ed', 'aliases': []}, + "flag_martinique": {'code': '1f1f2-1f1f6', 'aliases': []}, + "flag_mauritania": {'code': '1f1f2-1f1f7', 'aliases': []}, + "flag_mauritius": {'code': '1f1f2-1f1fa', 'aliases': []}, + "flag_mayotte": {'code': '1f1fe-1f1f9', 'aliases': []}, + "flag_mexico": {'code': '1f1f2-1f1fd', 'aliases': []}, + "flag_micronesia": {'code': '1f1eb-1f1f2', 'aliases': []}, + "flag_moldova": {'code': '1f1f2-1f1e9', 'aliases': []}, + "flag_monaco": {'code': '1f1f2-1f1e8', 'aliases': []}, + "flag_mongolia": {'code': '1f1f2-1f1f3', 'aliases': []}, + "flag_montenegro": {'code': '1f1f2-1f1ea', 'aliases': []}, + "flag_montserrat": {'code': '1f1f2-1f1f8', 'aliases': []}, + "flag_morocco": {'code': '1f1f2-1f1e6', 'aliases': []}, + "flag_mozambique": {'code': '1f1f2-1f1ff', 'aliases': []}, + "flag_myanmar_burma": {'code': '1f1f2-1f1f2', 'aliases': []}, + "flag_namibia": {'code': '1f1f3-1f1e6', 'aliases': []}, + "flag_nauru": {'code': '1f1f3-1f1f7', 'aliases': []}, + "flag_nepal": {'code': '1f1f3-1f1f5', 'aliases': []}, + "flag_netherlands": {'code': '1f1f3-1f1f1', 'aliases': []}, + "flag_new_caledonia": {'code': '1f1f3-1f1e8', 'aliases': []}, + "flag_new_zealand": {'code': '1f1f3-1f1ff', 'aliases': []}, + "flag_nicaragua": {'code': '1f1f3-1f1ee', 'aliases': []}, + "flag_niger": {'code': '1f1f3-1f1ea', 'aliases': []}, + "flag_nigeria": {'code': '1f1f3-1f1ec', 'aliases': []}, + "flag_niue": {'code': '1f1f3-1f1fa', 'aliases': []}, + "flag_norfolk_island": {'code': '1f1f3-1f1eb', 'aliases': []}, + "flag_north_korea": {'code': '1f1f0-1f1f5', 'aliases': []}, + "flag_north_macedonia": {'code': '1f1f2-1f1f0', 'aliases': []}, + "flag_northern_mariana_islands": {'code': '1f1f2-1f1f5', 'aliases': []}, + "flag_norway": {'code': '1f1f3-1f1f4', 'aliases': []}, + "flag_oman": {'code': '1f1f4-1f1f2', 'aliases': []}, + "flag_pakistan": {'code': '1f1f5-1f1f0', 'aliases': []}, + "flag_palau": {'code': '1f1f5-1f1fc', 'aliases': []}, + "flag_palestinian_territories": {'code': '1f1f5-1f1f8', 'aliases': []}, + "flag_panama": {'code': '1f1f5-1f1e6', 'aliases': []}, + "flag_papua_new_guinea": {'code': '1f1f5-1f1ec', 'aliases': []}, + "flag_paraguay": {'code': '1f1f5-1f1fe', 'aliases': []}, + "flag_peru": {'code': '1f1f5-1f1ea', 'aliases': []}, + "flag_philippines": {'code': '1f1f5-1f1ed', 'aliases': []}, + "flag_pitcairn_islands": {'code': '1f1f5-1f1f3', 'aliases': []}, + "flag_poland": {'code': '1f1f5-1f1f1', 'aliases': []}, + "flag_portugal": {'code': '1f1f5-1f1f9', 'aliases': []}, + "flag_puerto_rico": {'code': '1f1f5-1f1f7', 'aliases': []}, + "flag_qatar": {'code': '1f1f6-1f1e6', 'aliases': []}, + "flag_romania": {'code': '1f1f7-1f1f4', 'aliases': []}, + "flag_russia": {'code': '1f1f7-1f1fa', 'aliases': []}, + "flag_rwanda": {'code': '1f1f7-1f1fc', 'aliases': []}, + "flag_réunion": {'code': '1f1f7-1f1ea', 'aliases': ['flag_reunion']}, + "flag_samoa": {'code': '1f1fc-1f1f8', 'aliases': []}, + "flag_san_marino": {'code': '1f1f8-1f1f2', 'aliases': []}, + "flag_saudi_arabia": {'code': '1f1f8-1f1e6', 'aliases': []}, + "flag_scotland": {'code': '1f3f4-e0067-e0062-e0073-e0063-e0074-e007f', 'aliases': []}, + "flag_senegal": {'code': '1f1f8-1f1f3', 'aliases': []}, + "flag_serbia": {'code': '1f1f7-1f1f8', 'aliases': []}, + "flag_seychelles": {'code': '1f1f8-1f1e8', 'aliases': []}, + "flag_sierra_leone": {'code': '1f1f8-1f1f1', 'aliases': []}, + "flag_singapore": {'code': '1f1f8-1f1ec', 'aliases': []}, + "flag_sint_maarten": {'code': '1f1f8-1f1fd', 'aliases': []}, + "flag_slovakia": {'code': '1f1f8-1f1f0', 'aliases': []}, + "flag_slovenia": {'code': '1f1f8-1f1ee', 'aliases': []}, + "flag_solomon_islands": {'code': '1f1f8-1f1e7', 'aliases': []}, + "flag_somalia": {'code': '1f1f8-1f1f4', 'aliases': []}, + "flag_south_africa": {'code': '1f1ff-1f1e6', 'aliases': []}, + "flag_south_georgia_and_south_sandwich_islands": {'code': '1f1ec-1f1f8', 'aliases': []}, + "flag_south_korea": {'code': '1f1f0-1f1f7', 'aliases': []}, + "flag_south_sudan": {'code': '1f1f8-1f1f8', 'aliases': []}, + "flag_spain": {'code': '1f1ea-1f1f8', 'aliases': []}, + "flag_sri_lanka": {'code': '1f1f1-1f1f0', 'aliases': []}, + "flag_st_barthélemy": {'code': '1f1e7-1f1f1', 'aliases': ['flag_st_barthelemy']}, + "flag_st_helena": {'code': '1f1f8-1f1ed', 'aliases': []}, + "flag_st_kitts_and_nevis": {'code': '1f1f0-1f1f3', 'aliases': []}, + "flag_st_lucia": {'code': '1f1f1-1f1e8', 'aliases': []}, + "flag_st_martin": {'code': '1f1f2-1f1eb', 'aliases': []}, + "flag_st_pierre_and_miquelon": {'code': '1f1f5-1f1f2', 'aliases': []}, + "flag_st_vincent_and_grenadines": {'code': '1f1fb-1f1e8', 'aliases': []}, + "flag_sudan": {'code': '1f1f8-1f1e9', 'aliases': []}, + "flag_suriname": {'code': '1f1f8-1f1f7', 'aliases': []}, + "flag_svalbard_and_jan_mayen": {'code': '1f1f8-1f1ef', 'aliases': []}, + "flag_sweden": {'code': '1f1f8-1f1ea', 'aliases': []}, + "flag_switzerland": {'code': '1f1e8-1f1ed', 'aliases': []}, + "flag_syria": {'code': '1f1f8-1f1fe', 'aliases': []}, + "flag_são_tomé_and_príncipe": {'code': '1f1f8-1f1f9', 'aliases': ['flag_sao_tome_and_principe']}, + "flag_taiwan": {'code': '1f1f9-1f1fc', 'aliases': []}, + "flag_tajikistan": {'code': '1f1f9-1f1ef', 'aliases': []}, + "flag_tanzania": {'code': '1f1f9-1f1ff', 'aliases': []}, + "flag_thailand": {'code': '1f1f9-1f1ed', 'aliases': []}, + "flag_timor_leste": {'code': '1f1f9-1f1f1', 'aliases': []}, + "flag_togo": {'code': '1f1f9-1f1ec', 'aliases': []}, + "flag_tokelau": {'code': '1f1f9-1f1f0', 'aliases': []}, + "flag_tonga": {'code': '1f1f9-1f1f4', 'aliases': []}, + "flag_trinidad_and_tobago": {'code': '1f1f9-1f1f9', 'aliases': []}, + "flag_tristan_da_cunha": {'code': '1f1f9-1f1e6', 'aliases': []}, + "flag_tunisia": {'code': '1f1f9-1f1f3', 'aliases': []}, + "flag_turkey": {'code': '1f1f9-1f1f7', 'aliases': []}, + "flag_turkmenistan": {'code': '1f1f9-1f1f2', 'aliases': []}, + "flag_turks_and_caicos_islands": {'code': '1f1f9-1f1e8', 'aliases': []}, + "flag_tuvalu": {'code': '1f1f9-1f1fb', 'aliases': []}, + "flag_uganda": {'code': '1f1fa-1f1ec', 'aliases': []}, + "flag_ukraine": {'code': '1f1fa-1f1e6', 'aliases': []}, + "flag_united_arab_emirates": {'code': '1f1e6-1f1ea', 'aliases': []}, + "flag_united_kingdom": {'code': '1f1ec-1f1e7', 'aliases': []}, + "flag_united_nations": {'code': '1f1fa-1f1f3', 'aliases': []}, + "flag_united_states": {'code': '1f1fa-1f1f8', 'aliases': []}, + "flag_uruguay": {'code': '1f1fa-1f1fe', 'aliases': []}, + "flag_us_outlying_islands": {'code': '1f1fa-1f1f2', 'aliases': []}, + "flag_us_virgin_islands": {'code': '1f1fb-1f1ee', 'aliases': []}, + "flag_uzbekistan": {'code': '1f1fa-1f1ff', 'aliases': []}, + "flag_vanuatu": {'code': '1f1fb-1f1fa', 'aliases': []}, + "flag_vatican_city": {'code': '1f1fb-1f1e6', 'aliases': []}, + "flag_venezuela": {'code': '1f1fb-1f1ea', 'aliases': []}, + "flag_vietnam": {'code': '1f1fb-1f1f3', 'aliases': []}, + "flag_wales": {'code': '1f3f4-e0067-e0062-e0077-e006c-e0073-e007f', 'aliases': []}, + "flag_wallis_and_futuna": {'code': '1f1fc-1f1eb', 'aliases': []}, + "flag_western_sahara": {'code': '1f1ea-1f1ed', 'aliases': []}, + "flag_yemen": {'code': '1f1fe-1f1ea', 'aliases': []}, + "flag_zambia": {'code': '1f1ff-1f1f2', 'aliases': []}, + "flag_zimbabwe": {'code': '1f1ff-1f1fc', 'aliases': []}, + "flag_åland_islands": {'code': '1f1e6-1f1fd', 'aliases': ['flag_aland_islands']}, + "flamingo": {'code': '1f9a9', 'aliases': ['flamboyant']}, + "flashlight": {'code': '1f526', 'aliases': []}, + "flat_shoe": {'code': '1f97f', 'aliases': ['ballet_flat', 'slip_on', 'slipper']}, + "flatbread": {'code': '1fad3', 'aliases': ['arepa', 'lavash', 'naan', 'pita']}, + "fleur_de_lis": {'code': '269c', 'aliases': []}, + "floppy_disk": {'code': '1f4be', 'aliases': []}, + "flushed": {'code': '1f633', 'aliases': ['embarrassed', 'blushing']}, + "fly": {'code': '1fab0', 'aliases': ['maggot', 'rotting']}, + "flying_disc": {'code': '1f94f', 'aliases': ['ultimate']}, + "flying_saucer": {'code': '1f6f8', 'aliases': []}, + "fog": {'code': '1f32b', 'aliases': ['hazy']}, + "foggy": {'code': '1f301', 'aliases': []}, + "folder": {'code': '1f4c2', 'aliases': []}, + "fondue": {'code': '1fad5', 'aliases': ['melted', 'swiss']}, + "food": {'code': '1f372', 'aliases': ['soup', 'stew']}, + "foot": {'code': '1f9b6', 'aliases': ['stomp']}, + "football": {'code': '26bd', 'aliases': ['soccer']}, + "footprints": {'code': '1f463', 'aliases': ['feet']}, + "fork_and_knife": {'code': '1f374', 'aliases': ['eating_utensils']}, + "fortune_cookie": {'code': '1f960', 'aliases': ['prophecy']}, + "forward": {'code': '21aa', 'aliases': ['right_hook']}, + "fountain": {'code': '26f2', 'aliases': []}, + "fountain_pen": {'code': '1f58b', 'aliases': []}, + "four": {'code': '0034-20e3', 'aliases': []}, + "fox": {'code': '1f98a', 'aliases': []}, + "free": {'code': '1f193', 'aliases': []}, + "fries": {'code': '1f35f', 'aliases': []}, + "frog": {'code': '1f438', 'aliases': []}, + "frosty": {'code': '26c4', 'aliases': []}, + "frown": {'code': '1f641', 'aliases': ['slight_frown']}, + "frowning": {'code': '1f626', 'aliases': []}, + "fuel_pump": {'code': '26fd', 'aliases': ['gas_pump', 'petrol_pump']}, + "full_moon": {'code': '1f315', 'aliases': []}, + "funeral_urn": {'code': '26b1', 'aliases': ['cremation']}, + "garlic": {'code': '1f9c4', 'aliases': []}, + "gear": {'code': '2699', 'aliases': ['settings', 'mechanical', 'engineer']}, + "gem": {'code': '1f48e', 'aliases': ['crystal']}, + "gemini": {'code': '264a', 'aliases': []}, + "genie": {'code': '1f9de', 'aliases': []}, + "ghost": {'code': '1f47b', 'aliases': ['boo', 'spooky', 'haunted']}, + "gift": {'code': '1f381', 'aliases': ['present']}, + "gift_heart": {'code': '1f49d', 'aliases': []}, + "giraffe": {'code': '1f992', 'aliases': ['spots']}, + "girl": {'code': '1f467', 'aliases': []}, + "glasses": {'code': '1f453', 'aliases': ['spectacles']}, + "gloves": {'code': '1f9e4', 'aliases': []}, + "glowing_star": {'code': '1f31f', 'aliases': []}, + "goat": {'code': '1f410', 'aliases': []}, + "goblin": {'code': '1f47a', 'aliases': []}, + "goggles": {'code': '1f97d', 'aliases': ['eye_protection', 'swimming', 'welding']}, + "gold_record": {'code': '1f4bd', 'aliases': ['minidisc']}, + "golf": {'code': '1f3cc', 'aliases': []}, + "gondola": {'code': '1f6a0', 'aliases': ['mountain_cableway']}, + "goodnight": {'code': '1f31b', 'aliases': []}, + "gooooooooal": {'code': '1f945', 'aliases': ['goal']}, + "gorilla": {'code': '1f98d', 'aliases': []}, + "graduate": {'code': '1f393', 'aliases': ['mortar_board']}, + "grapes": {'code': '1f347', 'aliases': []}, + "green_apple": {'code': '1f34f', 'aliases': []}, + "green_book": {'code': '1f4d7', 'aliases': []}, + "green_circle": {'code': '1f7e2', 'aliases': ['green']}, + "green_heart": {'code': '1f49a', 'aliases': ['envy']}, + "green_large_square": {'code': '1f7e9', 'aliases': []}, + "grey_exclamation": {'code': '2755', 'aliases': []}, + "grey_question": {'code': '2754', 'aliases': []}, + "grimacing": {'code': '1f62c', 'aliases': ['nervous', 'anxious']}, + "grinning": {'code': '1f600', 'aliases': ['happy']}, + "grinning_face_with_smiling_eyes": {'code': '1f601', 'aliases': []}, + "gua_pi_mao": {'code': '1f472', 'aliases': []}, + "guard": {'code': '1f482', 'aliases': []}, + "guide_dog": {'code': '1f9ae', 'aliases': ['guide']}, + "guitar": {'code': '1f3b8', 'aliases': []}, + "gun": {'code': '1f52b', 'aliases': []}, + "haircut": {'code': '1f487', 'aliases': []}, + "hamburger": {'code': '1f354', 'aliases': []}, + "hammer": {'code': '1f528', 'aliases': ['maintenance', 'handyman', 'handywoman']}, + "hamsa": {'code': '1faac', 'aliases': ['amulet', 'fatima', 'mary', 'miriam', 'protection']}, + "hamster": {'code': '1f439', 'aliases': []}, + "hand": {'code': '270b', 'aliases': ['raised_hand']}, + "hand_with_index_finger_and_thumb_crossed": {'code': '1faf0', 'aliases': ['expensive', 'snap']}, + "handbag": {'code': '1f45c', 'aliases': []}, + "handball": {'code': '1f93e', 'aliases': []}, + "handshake": {'code': '1f91d', 'aliases': ['done_deal']}, + "harvest": {'code': '1f33e', 'aliases': ['ear_of_rice']}, + "hash": {'code': '0023-20e3', 'aliases': []}, + "hat": {'code': '1f452', 'aliases': []}, + "hatching": {'code': '1f423', 'aliases': ['hatching_chick']}, + "heading_down": {'code': '2935', 'aliases': []}, + "heading_up": {'code': '2934', 'aliases': []}, + "headlines": {'code': '1f4f0', 'aliases': []}, + "headphones": {'code': '1f3a7', 'aliases': []}, + "headstone": {'code': '1faa6', 'aliases': ['cemetery', 'graveyard', 'tombstone']}, + "health_worker": {'code': '1f9d1-200d-2695', 'aliases': []}, + "hear_no_evil": {'code': '1f649', 'aliases': []}, + "heart": {'code': '2764', 'aliases': ['love', 'love_you']}, + "heart_box": {'code': '1f49f', 'aliases': []}, + "heart_exclamation": {'code': '2763', 'aliases': []}, + "heart_eyes": {'code': '1f60d', 'aliases': ['in_love']}, + "heart_eyes_cat": {'code': '1f63b', 'aliases': []}, + "heart_hands": {'code': '1faf6', 'aliases': []}, + "heart_kiss": {'code': '1f618', 'aliases': ['blow_a_kiss']}, + "heart_on_fire": {'code': '2764-200d-1f525', 'aliases': ['burn', 'lust', 'sacred_heart']}, + "heart_pulse": {'code': '1f497', 'aliases': ['growing_heart']}, + "heartbeat": {'code': '1f493', 'aliases': []}, + "hearts": {'code': '2665', 'aliases': []}, + "heavy_equals_sign": {'code': '1f7f0', 'aliases': ['equality', 'math']}, + "hedgehog": {'code': '1f994', 'aliases': ['spiny']}, + "helicopter": {'code': '1f681', 'aliases': []}, + "helmet": {'code': '26d1', 'aliases': ['hard_hat', 'rescue_worker', 'safety_first', 'invincible']}, + "herb": {'code': '1f33f', 'aliases': ['plant']}, + "hibiscus": {'code': '1f33a', 'aliases': []}, + "high_five": {'code': '1f590', 'aliases': ['palm']}, + "high_heels": {'code': '1f460', 'aliases': []}, + "high_speed_train": {'code': '1f684', 'aliases': []}, + "high_voltage": {'code': '26a1', 'aliases': ['zap']}, + "hiking_boot": {'code': '1f97e', 'aliases': ['backpacking', 'hiking']}, + "hindu_temple": {'code': '1f6d5', 'aliases': ['hindu', 'temple']}, + "hippopotamus": {'code': '1f99b', 'aliases': ['hippo']}, + "hole": {'code': '1f573', 'aliases': []}, + "hole_in_one": {'code': '26f3', 'aliases': []}, + "holiday_tree": {'code': '1f384', 'aliases': []}, + "honey": {'code': '1f36f', 'aliases': []}, + "hook": {'code': '1fa9d', 'aliases': ['crook', 'curve', 'ensnare', 'selling_point']}, + "horizontal_traffic_light": {'code': '1f6a5', 'aliases': []}, + "horn": {'code': '1f4ef', 'aliases': []}, + "horse": {'code': '1f40e', 'aliases': []}, + "horse_racing": {'code': '1f3c7', 'aliases': ['horse_riding']}, + "hospital": {'code': '1f3e5', 'aliases': []}, + "hot_face": {'code': '1f975', 'aliases': ['feverish', 'heat_stroke', 'red_faced', 'sweating']}, + "hot_pepper": {'code': '1f336', 'aliases': ['chili_pepper']}, + "hot_springs": {'code': '2668', 'aliases': []}, + "hotdog": {'code': '1f32d', 'aliases': []}, + "hotel": {'code': '1f3e8', 'aliases': []}, + "house": {'code': '1f3e0', 'aliases': []}, + "houses": {'code': '1f3d8', 'aliases': []}, + "hug": {'code': '1f917', 'aliases': ['arms_open']}, + "humpback_whale": {'code': '1f40b', 'aliases': []}, + "hungry": {'code': '1f37d', 'aliases': ['meal', 'table_setting', 'fork_and_knife_with_plate', 'lets_eat']}, + "hurt": {'code': '1f915', 'aliases': ['head_bandage', 'injured']}, + "hushed": {'code': '1f62f', 'aliases': []}, + "hut": {'code': '1f6d6', 'aliases': ['roundhouse', 'yurt']}, + "ice": {'code': '1f9ca', 'aliases': ['ice_cube', 'iceberg']}, + "ice_cream": {'code': '1f368', 'aliases': ['gelato']}, + "ice_hockey": {'code': '1f3d2', 'aliases': []}, + "ice_skate": {'code': '26f8', 'aliases': []}, + "id": {'code': '1f194', 'aliases': []}, + "identification_card": {'code': '1faaa', 'aliases': ['credentials', 'license', 'security']}, + "in_bed": {'code': '1f6cc', 'aliases': ['accommodations', 'guestrooms']}, + "inbox": {'code': '1f4e5', 'aliases': []}, + "inbox_zero": {'code': '1f4ed', 'aliases': ['empty_mailbox', 'no_mail']}, + "index_pointing_at_the_viewer": {'code': '1faf5', 'aliases': ['point', 'you']}, + "infinity": {'code': '267e', 'aliases': ['forever', 'unbounded', 'universal']}, + "info": {'code': '2139', 'aliases': []}, + "information_desk_person": {'code': '1f481', 'aliases': ['person_tipping_hand']}, + "injection": {'code': '1f489', 'aliases': ['syringe']}, + "innocent": {'code': '1f607', 'aliases': ['halo']}, + "interrobang": {'code': '2049', 'aliases': []}, + "island": {'code': '1f3dd', 'aliases': []}, + "jack-o-lantern": {'code': '1f383', 'aliases': ['pumpkin']}, + "japan": {'code': '1f5fe', 'aliases': []}, + "japan_post": {'code': '1f3e3', 'aliases': []}, + "japanese_acceptable_button": {'code': '1f251', 'aliases': ['accept']}, + "japanese_application_button": {'code': '1f238', 'aliases': ['u7533']}, + "japanese_bargain_button": {'code': '1f250', 'aliases': ['ideograph_advantage']}, + "japanese_congratulations_button": {'code': '3297', 'aliases': ['congratulations']}, + "japanese_discount_button": {'code': '1f239', 'aliases': ['u5272']}, + "japanese_free_of_charge_button": {'code': '1f21a', 'aliases': ['u7121']}, + "japanese_here_button": {'code': '1f201', 'aliases': ['here', 'ココ']}, + "japanese_monthly_amount_button": {'code': '1f237', 'aliases': ['u6708']}, + "japanese_no_vacancy_button": {'code': '1f235', 'aliases': ['u6e80']}, + "japanese_not_free_of_charge_button": {'code': '1f236', 'aliases': ['u6709']}, + "japanese_open_for_business_button": {'code': '1f23a', 'aliases': ['u55b6']}, + "japanese_passing_grade_button": {'code': '1f234', 'aliases': ['u5408']}, + "japanese_prohibited_button": {'code': '1f232', 'aliases': ['u7981']}, + "japanese_reserved_button": {'code': '1f22f', 'aliases': ['reserved', '指']}, + "japanese_secret_button": {'code': '3299', 'aliases': []}, + "japanese_service_charge_button": {'code': '1f202', 'aliases': ['service_charge', 'サ']}, + "japanese_vacancy_button": {'code': '1f233', 'aliases': ['vacancy', '空']}, + "jar": {'code': '1fad9', 'aliases': ['container', 'sauce', 'store']}, + "jeans": {'code': '1f456', 'aliases': ['denim']}, + "joker": {'code': '1f0cf', 'aliases': []}, + "joy": {'code': '1f602', 'aliases': ['tears', 'laughter_tears']}, + "joy_cat": {'code': '1f639', 'aliases': []}, + "joystick": {'code': '1f579', 'aliases': ['arcade']}, + "judge": {'code': '1f9d1-200d-2696', 'aliases': []}, + "juggling": {'code': '1f939', 'aliases': []}, + "justice": {'code': '2696', 'aliases': ['scales', 'balance']}, + "kaaba": {'code': '1f54b', 'aliases': []}, + "kangaroo": {'code': '1f998', 'aliases': ['joey', 'jump', 'marsupial']}, + "key": {'code': '1f511', 'aliases': []}, + "keyboard": {'code': '2328', 'aliases': []}, + "kick_scooter": {'code': '1f6f4', 'aliases': []}, + "kimono": {'code': '1f458', 'aliases': []}, + "kiss": {'code': '1f48f', 'aliases': []}, + "kiss_man_man": {'code': '1f468-200d-2764-200d-1f48b-200d-1f468', 'aliases': []}, + "kiss_smiling_eyes": {'code': '1f619', 'aliases': []}, + "kiss_with_blush": {'code': '1f61a', 'aliases': []}, + "kiss_woman_man": {'code': '1f469-200d-2764-200d-1f48b-200d-1f468', 'aliases': []}, + "kiss_woman_woman": {'code': '1f469-200d-2764-200d-1f48b-200d-1f469', 'aliases': []}, + "kissing_cat": {'code': '1f63d', 'aliases': []}, + "kissing_face": {'code': '1f617', 'aliases': []}, + "kite": {'code': '1fa81', 'aliases': ['soar']}, + "kitten": {'code': '1f431', 'aliases': []}, + "kiwi": {'code': '1f95d', 'aliases': []}, + "knife": {'code': '1f52a', 'aliases': ['hocho', 'betrayed']}, + "knot": {'code': '1faa2', 'aliases': ['rope', 'tangled', 'twine', 'twist']}, + "koala": {'code': '1f428', 'aliases': []}, + "lab_coat": {'code': '1f97c', 'aliases': []}, + "label": {'code': '1f3f7', 'aliases': ['tag', 'price_tag']}, + "lacrosse": {'code': '1f94d', 'aliases': []}, + "ladder": {'code': '1fa9c', 'aliases': ['climb', 'rung', 'step']}, + "lady_beetle": {'code': '1f41e', 'aliases': ['ladybird', 'ladybug']}, + "landing": {'code': '1f6ec', 'aliases': ['arrival', 'airplane_arrival']}, + "landline": {'code': '1f4de', 'aliases': ['home_phone']}, + "lantern": {'code': '1f3ee', 'aliases': ['izakaya_lantern']}, + "large_blue_diamond": {'code': '1f537', 'aliases': []}, + "large_orange_diamond": {'code': '1f536', 'aliases': []}, + "last_quarter_moon": {'code': '1f317', 'aliases': []}, + "last_quarter_moon_face": {'code': '1f31c', 'aliases': []}, + "laughing": {'code': '1f606', 'aliases': ['lol']}, + "leafy_green": {'code': '1f96c', 'aliases': ['bok_choy', 'cabbage', 'kale', 'lettuce']}, + "leaves": {'code': '1f343', 'aliases': ['wind', 'fall']}, + "ledger": {'code': '1f4d2', 'aliases': ['spiral_notebook']}, + "left": {'code': '2b05', 'aliases': ['west']}, + "left_fist": {'code': '1f91b', 'aliases': []}, + "left_right": {'code': '2194', 'aliases': ['swap']}, + "leftwards_hand": {'code': '1faf2', 'aliases': ['leftward']}, + "leg": {'code': '1f9b5', 'aliases': ['limb']}, + "lemon": {'code': '1f34b', 'aliases': []}, + "leo": {'code': '264c', 'aliases': []}, + "leopard": {'code': '1f406', 'aliases': []}, + "levitating": {'code': '1f574', 'aliases': ['hover']}, + "libra": {'code': '264e', 'aliases': []}, + "lift": {'code': '1f3cb', 'aliases': ['work_out', 'weight_lift', 'gym']}, + "light_bulb": {'code': '1f4a1', 'aliases': ['bulb', 'idea']}, + "light_rail": {'code': '1f688', 'aliases': []}, + "lightning": {'code': '1f329', 'aliases': ['lightning_storm']}, + "link": {'code': '1f517', 'aliases': []}, + "lion": {'code': '1f981', 'aliases': []}, + "lips": {'code': '1f444', 'aliases': ['mouth']}, + "lipstick": {'code': '1f484', 'aliases': []}, + "lipstick_kiss": {'code': '1f48b', 'aliases': []}, + "living_room": {'code': '1f6cb', 'aliases': ['furniture', 'couch_and_lamp', 'lifestyles']}, + "lizard": {'code': '1f98e', 'aliases': ['gecko']}, + "llama": {'code': '1f999', 'aliases': ['alpaca', 'guanaco', 'vicuña', 'wool', 'vicuna']}, + "lobster": {'code': '1f99e', 'aliases': ['bisque', 'claws', 'seafood']}, + "locked": {'code': '1f512', 'aliases': []}, + "locker": {'code': '1f6c5', 'aliases': ['locked_bag']}, + "lollipop": {'code': '1f36d', 'aliases': []}, + "long_drum": {'code': '1fa98', 'aliases': ['beat', 'conga', 'rhythm']}, + "loop": {'code': '27b0', 'aliases': []}, + "losing_money": {'code': '1f4b8', 'aliases': ['easy_come_easy_go', 'money_with_wings']}, + "lotion_bottle": {'code': '1f9f4', 'aliases': ['lotion', 'moisturizer', 'shampoo', 'sunscreen']}, + "lotus": {'code': '1fab7', 'aliases': ['purity']}, + "louder": {'code': '1f50a', 'aliases': ['sound']}, + "loudspeaker": {'code': '1f4e2', 'aliases': ['bullhorn']}, + "love_hotel": {'code': '1f3e9', 'aliases': []}, + "love_letter": {'code': '1f48c', 'aliases': []}, + "love_you_gesture": {'code': '1f91f', 'aliases': ['ily']}, + "low_battery": {'code': '1faab', 'aliases': ['electronic', 'low_energy']}, + "low_brightness": {'code': '1f505', 'aliases': ['dim']}, + "lower_left": {'code': '2199', 'aliases': ['south_west']}, + "lower_right": {'code': '2198', 'aliases': ['south_east']}, + "lucky": {'code': '1f340', 'aliases': ['four_leaf_clover']}, + "luggage": {'code': '1f9f3', 'aliases': ['packing', 'travel']}, + "lungs": {'code': '1fac1', 'aliases': ['breath', 'exhalation', 'inhalation', 'respiration']}, + "lying": {'code': '1f925', 'aliases': []}, + "mage": {'code': '1f9d9', 'aliases': []}, + "magic_wand": {'code': '1fa84', 'aliases': ['magic']}, + "magnet": {'code': '1f9f2', 'aliases': ['attraction', 'horseshoe']}, + "magnifying_glass_tilted_right": {'code': '1f50e', 'aliases': ['magnifying']}, + "mahjong": {'code': '1f004', 'aliases': []}, + "mail_dropoff": {'code': '1f4ee', 'aliases': []}, + "mail_received": {'code': '1f4e8', 'aliases': []}, + "mail_sent": {'code': '1f4e9', 'aliases': ['sealed']}, + "mailbox": {'code': '1f4eb', 'aliases': []}, + "male_sign": {'code': '2642', 'aliases': []}, + "mammoth": {'code': '1f9a3', 'aliases': ['tusk', 'woolly']}, + "man": {'code': '1f468', 'aliases': []}, + "man_and_woman_holding_hands": {'code': '1f46b', 'aliases': ['man_and_woman_couple']}, + "man_artist": {'code': '1f468-200d-1f3a8', 'aliases': []}, + "man_astronaut": {'code': '1f468-200d-1f680', 'aliases': []}, + "man_bald": {'code': '1f468-200d-1f9b2', 'aliases': []}, + "man_beard": {'code': '1f9d4-200d-2642', 'aliases': []}, + "man_biking": {'code': '1f6b4-200d-2642', 'aliases': []}, + "man_blond_hair": {'code': '1f471-200d-2642', 'aliases': ['blond_haired_man']}, + "man_bouncing_ball": {'code': '26f9-fe0f-200d-2642-fe0f', 'aliases': []}, + "man_bowing": {'code': '1f647-200d-2642', 'aliases': []}, + "man_cartwheeling": {'code': '1f938-200d-2642', 'aliases': []}, + "man_climbing": {'code': '1f9d7-200d-2642', 'aliases': []}, + "man_construction_worker": {'code': '1f477-200d-2642', 'aliases': []}, + "man_cook": {'code': '1f468-200d-1f373', 'aliases': []}, + "man_curly_hair": {'code': '1f468-200d-1f9b1', 'aliases': []}, + "man_detective": {'code': '1f575-fe0f-200d-2642-fe0f', 'aliases': []}, + "man_elf": {'code': '1f9dd-200d-2642', 'aliases': []}, + "man_facepalming": {'code': '1f926-200d-2642', 'aliases': []}, + "man_factory_worker": {'code': '1f468-200d-1f3ed', 'aliases': []}, + "man_fairy": {'code': '1f9da-200d-2642', 'aliases': []}, + "man_farmer": {'code': '1f468-200d-1f33e', 'aliases': []}, + "man_feeding_baby": {'code': '1f468-200d-1f37c', 'aliases': []}, + "man_firefighter": {'code': '1f468-200d-1f692', 'aliases': []}, + "man_frowning": {'code': '1f64d-200d-2642', 'aliases': []}, + "man_genie": {'code': '1f9de-200d-2642', 'aliases': []}, + "man_gesturing_no": {'code': '1f645-200d-2642', 'aliases': []}, + "man_gesturing_ok": {'code': '1f646-200d-2642', 'aliases': []}, + "man_getting_haircut": {'code': '1f487-200d-2642', 'aliases': []}, + "man_getting_massage": {'code': '1f486-200d-2642', 'aliases': []}, + "man_golfing": {'code': '1f3cc-fe0f-200d-2642-fe0f', 'aliases': []}, + "man_guard": {'code': '1f482-200d-2642', 'aliases': []}, + "man_health_worker": {'code': '1f468-200d-2695', 'aliases': []}, + "man_in_lotus_position": {'code': '1f9d8-200d-2642', 'aliases': []}, + "man_in_manual_wheelchair": {'code': '1f468-200d-1f9bd', 'aliases': []}, + "man_in_motorized_wheelchair": {'code': '1f468-200d-1f9bc', 'aliases': []}, + "man_in_steamy_room": {'code': '1f9d6-200d-2642', 'aliases': []}, + "man_in_tuxedo": {'code': '1f935-200d-2642', 'aliases': []}, + "man_judge": {'code': '1f468-200d-2696', 'aliases': []}, + "man_juggling": {'code': '1f939-200d-2642', 'aliases': []}, + "man_kneeling": {'code': '1f9ce-200d-2642', 'aliases': []}, + "man_lifting_weights": {'code': '1f3cb-fe0f-200d-2642-fe0f', 'aliases': []}, + "man_mage": {'code': '1f9d9-200d-2642', 'aliases': []}, + "man_mechanic": {'code': '1f468-200d-1f527', 'aliases': []}, + "man_mountain_biking": {'code': '1f6b5-200d-2642', 'aliases': []}, + "man_office_worker": {'code': '1f468-200d-1f4bc', 'aliases': []}, + "man_pilot": {'code': '1f468-200d-2708', 'aliases': []}, + "man_playing_handball": {'code': '1f93e-200d-2642', 'aliases': []}, + "man_playing_water_polo": {'code': '1f93d-200d-2642', 'aliases': []}, + "man_police_officer": {'code': '1f46e-200d-2642', 'aliases': []}, + "man_pouting": {'code': '1f64e-200d-2642', 'aliases': []}, + "man_raising_hand": {'code': '1f64b-200d-2642', 'aliases': []}, + "man_red_hair": {'code': '1f468-200d-1f9b0', 'aliases': []}, + "man_rowing_boat": {'code': '1f6a3-200d-2642', 'aliases': []}, + "man_running": {'code': '1f3c3-200d-2642', 'aliases': []}, + "man_scientist": {'code': '1f468-200d-1f52c', 'aliases': []}, + "man_shrugging": {'code': '1f937-200d-2642', 'aliases': []}, + "man_singer": {'code': '1f468-200d-1f3a4', 'aliases': []}, + "man_standing": {'code': '1f9cd-200d-2642', 'aliases': []}, + "man_student": {'code': '1f468-200d-1f393', 'aliases': []}, + "man_superhero": {'code': '1f9b8-200d-2642', 'aliases': []}, + "man_supervillain": {'code': '1f9b9-200d-2642', 'aliases': []}, + "man_surfing": {'code': '1f3c4-200d-2642', 'aliases': []}, + "man_swimming": {'code': '1f3ca-200d-2642', 'aliases': []}, + "man_teacher": {'code': '1f468-200d-1f3eb', 'aliases': []}, + "man_technologist": {'code': '1f468-200d-1f4bb', 'aliases': []}, + "man_tipping_hand": {'code': '1f481-200d-2642', 'aliases': []}, + "man_vampire": {'code': '1f9db-200d-2642', 'aliases': []}, + "man_walking": {'code': '1f6b6-200d-2642', 'aliases': []}, + "man_wearing_turban": {'code': '1f473-200d-2642', 'aliases': []}, + "man_white_hair": {'code': '1f468-200d-1f9b3', 'aliases': []}, + "man_with_veil": {'code': '1f470-200d-2642', 'aliases': []}, + "man_with_white_cane": {'code': '1f468-200d-1f9af', 'aliases': []}, + "man_zombie": {'code': '1f9df-200d-2642', 'aliases': []}, + "mango": {'code': '1f96d', 'aliases': ['fruit']}, + "mantelpiece_clock": {'code': '1f570', 'aliases': []}, + "manual_wheelchair": {'code': '1f9bd', 'aliases': []}, + "map": {'code': '1f5fa', 'aliases': ['world_map', 'road_trip']}, + "maple_leaf": {'code': '1f341', 'aliases': []}, + "mask": {'code': '1f637', 'aliases': []}, + "massage": {'code': '1f486', 'aliases': []}, + "mate": {'code': '1f9c9', 'aliases': []}, + "meat": {'code': '1f356', 'aliases': []}, + "mechanic": {'code': '1f9d1-200d-1f527', 'aliases': []}, + "mechanical_arm": {'code': '1f9be', 'aliases': []}, + "mechanical_leg": {'code': '1f9bf', 'aliases': []}, + "medal": {'code': '1f3c5', 'aliases': []}, + "medical_symbol": {'code': '2695', 'aliases': ['aesculapius', 'staff']}, + "medicine": {'code': '1f48a', 'aliases': ['pill']}, + "megaphone": {'code': '1f4e3', 'aliases': ['shout']}, + "melon": {'code': '1f348', 'aliases': []}, + "melting_face": {'code': '1fae0', 'aliases': ['dissolve', 'liquid', 'melt']}, + "memo": {'code': '1f4dd', 'aliases': ['note']}, + "men_with_bunny_ears": {'code': '1f46f-200d-2642', 'aliases': []}, + "men_wrestling": {'code': '1f93c-200d-2642', 'aliases': []}, + "mending_heart": {'code': '2764-200d-1fa79', 'aliases': ['healthier', 'improving', 'mending', 'recovering', 'recuperating', 'well']}, + "menorah": {'code': '1f54e', 'aliases': []}, + "mens": {'code': '1f6b9', 'aliases': []}, + "mermaid": {'code': '1f9dc-200d-2640', 'aliases': []}, + "merman": {'code': '1f9dc-200d-2642', 'aliases': ['triton']}, + "merperson": {'code': '1f9dc', 'aliases': []}, + "metro": {'code': '24c2', 'aliases': ['m']}, + "microbe": {'code': '1f9a0', 'aliases': ['amoeba']}, + "microphone": {'code': '1f3a4', 'aliases': ['mike', 'mic']}, + "middle_finger": {'code': '1f595', 'aliases': []}, + "military_helmet": {'code': '1fa96', 'aliases': ['army', 'military', 'soldier', 'warrior']}, + "military_medal": {'code': '1f396', 'aliases': []}, + "milk": {'code': '1f95b', 'aliases': ['glass_of_milk']}, + "milky_way": {'code': '1f30c', 'aliases': ['night_sky']}, + "mine": {'code': '26cf', 'aliases': ['pick']}, + "minibus": {'code': '1f690', 'aliases': []}, + "minus": {'code': '2796', 'aliases': ['subtract']}, + "mirror": {'code': '1fa9e', 'aliases': ['reflection', 'reflector', 'speculum']}, + "mirror_ball": {'code': '1faa9', 'aliases': ['glitter']}, + "mobile_phone": {'code': '1f4f1', 'aliases': ['smartphone', 'iphone', 'android']}, + "money": {'code': '1f4b0', 'aliases': []}, + "money_face": {'code': '1f911', 'aliases': ['kaching']}, + "monkey": {'code': '1f412', 'aliases': []}, + "monkey_face": {'code': '1f435', 'aliases': []}, + "monorail": {'code': '1f69d', 'aliases': ['elevated_train']}, + "moon": {'code': '1f319', 'aliases': []}, + "moon_cake": {'code': '1f96e', 'aliases': ['autumn', 'festival', 'yuèbǐng', 'yuebing']}, + "moon_ceremony": {'code': '1f391', 'aliases': []}, + "moon_face": {'code': '1f31d', 'aliases': []}, + "mosque": {'code': '1f54c', 'aliases': []}, + "mosquito": {'code': '1f99f', 'aliases': ['malaria']}, + "mostly_sunny": {'code': '1f324', 'aliases': []}, + "mother_christmas": {'code': '1f936', 'aliases': ['mrs_claus']}, + "motor_boat": {'code': '1f6e5', 'aliases': []}, + "motorcycle": {'code': '1f3cd', 'aliases': []}, + "motorized_wheelchair": {'code': '1f9bc', 'aliases': []}, + "mount_fuji": {'code': '1f5fb', 'aliases': []}, + "mountain": {'code': '26f0', 'aliases': []}, + "mountain_biker": {'code': '1f6b5', 'aliases': []}, + "mountain_railway": {'code': '1f69e', 'aliases': []}, + "mountain_sunrise": {'code': '1f304', 'aliases': []}, + "mouse": {'code': '1f401', 'aliases': []}, + "mouse_trap": {'code': '1faa4', 'aliases': ['bait', 'mousetrap', 'snare', 'trap']}, + "movie_camera": {'code': '1f3a5', 'aliases': []}, + "moving_truck": {'code': '1f69a', 'aliases': []}, + "multiplication": {'code': '2716', 'aliases': ['multiply']}, + "muscle": {'code': '1f4aa', 'aliases': []}, + "mushroom": {'code': '1f344', 'aliases': []}, + "music": {'code': '1f3b5', 'aliases': []}, + "musical_notes": {'code': '1f3b6', 'aliases': []}, + "musical_score": {'code': '1f3bc', 'aliases': []}, + "mute": {'code': '1f507', 'aliases': ['no_sound']}, + "mute_notifications": {'code': '1f515', 'aliases': []}, + "mx_claus": {'code': '1f9d1-200d-1f384', 'aliases': ['claus_christmas']}, + "nail_polish": {'code': '1f485', 'aliases': ['nail_care']}, + "name_badge": {'code': '1f4db', 'aliases': []}, + "naruto": {'code': '1f365', 'aliases': []}, + "national_park": {'code': '1f3de', 'aliases': []}, + "nauseated": {'code': '1f922', 'aliases': ['queasy']}, + "nazar_amulet": {'code': '1f9ff', 'aliases': ['bead', 'charm', 'evil_eye', 'nazar', 'talisman']}, + "nerd": {'code': '1f913', 'aliases': ['geek']}, + "nest_with_eggs": {'code': '1faba', 'aliases': []}, + "nesting_dolls": {'code': '1fa86', 'aliases': ['doll', 'russia']}, + "neutral": {'code': '1f610', 'aliases': []}, + "new": {'code': '1f195', 'aliases': []}, + "new_baby": {'code': '1f425', 'aliases': []}, + "new_moon": {'code': '1f311', 'aliases': []}, + "new_moon_face": {'code': '1f31a', 'aliases': []}, + "newspaper": {'code': '1f5de', 'aliases': ['swat']}, + "next_track": {'code': '23ed', 'aliases': ['skip_forward']}, + "ng": {'code': '1f196', 'aliases': []}, + "night": {'code': '1f303', 'aliases': []}, + "nine": {'code': '0039-20e3', 'aliases': []}, + "ninja": {'code': '1f977', 'aliases': ['fighter', 'hidden', 'stealth']}, + "no_bicycles": {'code': '1f6b3', 'aliases': []}, + "no_entry": {'code': '26d4', 'aliases': ['wrong_way']}, + "no_pedestrians": {'code': '1f6b7', 'aliases': []}, + "no_phones": {'code': '1f4f5', 'aliases': []}, + "no_signal": {'code': '1f645', 'aliases': ['nope']}, + "no_smoking": {'code': '1f6ad', 'aliases': []}, + "non-potable_water": {'code': '1f6b1', 'aliases': []}, + "nose": {'code': '1f443', 'aliases': []}, + "notebook": {'code': '1f4d3', 'aliases': ['composition_book']}, + "notifications": {'code': '1f514', 'aliases': ['bell']}, + "nut_and_bolt": {'code': '1f529', 'aliases': ['screw']}, + "o": {'code': '1f17e', 'aliases': []}, + "ocean": {'code': '1f30a', 'aliases': []}, + "octopus": {'code': '1f419', 'aliases': []}, + "oden": {'code': '1f362', 'aliases': []}, + "office": {'code': '1f3e2', 'aliases': []}, + "office_supplies": {'code': '1f587', 'aliases': ['paperclip_chain', 'linked']}, + "office_worker": {'code': '1f9d1-200d-1f4bc', 'aliases': []}, + "ogre": {'code': '1f479', 'aliases': []}, + "oh_no": {'code': '1f615', 'aliases': ['half_frown', 'concerned', 'confused']}, + "oil_drum": {'code': '1f6e2', 'aliases': ['commodities']}, + "ok": {'code': '1f44c', 'aliases': ['got_it']}, + "ok_signal": {'code': '1f646', 'aliases': []}, + "older_man": {'code': '1f474', 'aliases': ['elderly_man']}, + "older_person": {'code': '1f9d3', 'aliases': ['old']}, + "older_woman": {'code': '1f475', 'aliases': ['elderly_woman']}, + "olive": {'code': '1fad2', 'aliases': []}, + "om": {'code': '1f549', 'aliases': ['hinduism']}, + "on": {'code': '1f51b', 'aliases': []}, + "oncoming_bus": {'code': '1f68d', 'aliases': []}, + "oncoming_car": {'code': '1f698', 'aliases': ['oncoming_automobile']}, + "oncoming_police_car": {'code': '1f694', 'aliases': []}, + "oncoming_taxi": {'code': '1f696', 'aliases': []}, + "oncoming_train": {'code': '1f686', 'aliases': []}, + "oncoming_tram": {'code': '1f68a', 'aliases': ['oncoming_streetcar', 'oncoming_trolley']}, + "one": {'code': '0031-20e3', 'aliases': []}, + "one_piece_swimsuit": {'code': '1fa71', 'aliases': []}, + "onigiri": {'code': '1f359', 'aliases': []}, + "onion": {'code': '1f9c5', 'aliases': []}, + "open_hands": {'code': '1f450', 'aliases': []}, + "open_mouth": {'code': '1f62e', 'aliases': ['surprise']}, + "ophiuchus": {'code': '26ce', 'aliases': []}, + "orange": {'code': '1f34a', 'aliases': ['tangerine', 'mandarin']}, + "orange_book": {'code': '1f4d9', 'aliases': []}, + "orange_circle": {'code': '1f7e0', 'aliases': []}, + "orange_heart": {'code': '1f9e1', 'aliases': []}, + "orange_square": {'code': '1f7e7', 'aliases': []}, + "orangutan": {'code': '1f9a7', 'aliases': ['ape']}, + "organize": {'code': '1f4c1', 'aliases': ['file_folder']}, + "orthodox_cross": {'code': '2626', 'aliases': []}, + "otter": {'code': '1f9a6', 'aliases': ['playful']}, + "outbox": {'code': '1f4e4', 'aliases': []}, + "owl": {'code': '1f989', 'aliases': []}, + "ox": {'code': '1f402', 'aliases': ['bull']}, + "oyster": {'code': '1f9aa', 'aliases': []}, + "package": {'code': '1f4e6', 'aliases': []}, + "paella": {'code': '1f958', 'aliases': []}, + "page_with_curl": {'code': '1f4c3', 'aliases': ['curl']}, + "pager": {'code': '1f4df', 'aliases': []}, + "paintbrush": {'code': '1f58c', 'aliases': []}, + "palm_down_hand": {'code': '1faf3', 'aliases': ['dismiss', 'shoo']}, + "palm_tree": {'code': '1f334', 'aliases': []}, + "palm_up_hand": {'code': '1faf4', 'aliases': ['beckon', 'come', 'offer']}, + "palms_up_together": {'code': '1f932', 'aliases': ['prayer']}, + "pancakes": {'code': '1f95e', 'aliases': ['breakfast']}, + "panda": {'code': '1f43c', 'aliases': []}, + "paperclip": {'code': '1f4ce', 'aliases': ['attachment']}, + "parachute": {'code': '1fa82', 'aliases': ['hang_glide', 'parasail', 'skydive']}, + "parking": {'code': '1f17f', 'aliases': ['p']}, + "parrot": {'code': '1f99c', 'aliases': ['talk']}, + "part_alternation": {'code': '303d', 'aliases': []}, + "partly_sunny": {'code': '26c5', 'aliases': ['partly_cloudy']}, + "partying_face": {'code': '1f973', 'aliases': []}, + "pass": {'code': '1f3ab', 'aliases': []}, + "passenger_ship": {'code': '1f6f3', 'aliases': ['yacht', 'cruise']}, + "passport_control": {'code': '1f6c2', 'aliases': ['immigration']}, + "pause": {'code': '23f8', 'aliases': []}, + "paw_prints": {'code': '1f43e', 'aliases': ['paws']}, + "peace": {'code': '262e', 'aliases': []}, + "peace_sign": {'code': '270c', 'aliases': ['victory']}, + "peach": {'code': '1f351', 'aliases': []}, + "peacock": {'code': '1f99a', 'aliases': ['ostentatious', 'peahen']}, + "peanuts": {'code': '1f95c', 'aliases': []}, + "pear": {'code': '1f350', 'aliases': []}, + "pen": {'code': '1f58a', 'aliases': ['ballpoint_pen']}, + "pencil": {'code': '270f', 'aliases': []}, + "penguin": {'code': '1f427', 'aliases': []}, + "pensive": {'code': '1f614', 'aliases': ['tired']}, + "people_holding_hands": {'code': '1f9d1-200d-1f91d-200d-1f9d1', 'aliases': ['hold', 'holding_hands']}, + "people_hugging": {'code': '1fac2', 'aliases': ['goodbye', 'thanks']}, + "performing_arts": {'code': '1f3ad', 'aliases': ['drama', 'theater']}, + "persevere": {'code': '1f623', 'aliases': ['helpless']}, + "person": {'code': '1f9d1', 'aliases': []}, + "person_bald": {'code': '1f9d1-200d-1f9b2', 'aliases': []}, + "person_beard": {'code': '1f9d4', 'aliases': []}, + "person_blond_hair": {'code': '1f471', 'aliases': ['blond_haired_person']}, + "person_climbing": {'code': '1f9d7', 'aliases': []}, + "person_curly_hair": {'code': '1f9d1-200d-1f9b1', 'aliases': []}, + "person_feeding_baby": {'code': '1f9d1-200d-1f37c', 'aliases': []}, + "person_frowning": {'code': '1f64d', 'aliases': []}, + "person_in_lotus_position": {'code': '1f9d8', 'aliases': []}, + "person_in_manual_wheelchair": {'code': '1f9d1-200d-1f9bd', 'aliases': []}, + "person_in_motorized_wheelchair": {'code': '1f9d1-200d-1f9bc', 'aliases': []}, + "person_in_steamy_room": {'code': '1f9d6', 'aliases': []}, + "person_kneeling": {'code': '1f9ce', 'aliases': ['kneel']}, + "person_pouting": {'code': '1f64e', 'aliases': []}, + "person_red_hair": {'code': '1f9d1-200d-1f9b0', 'aliases': []}, + "person_standing": {'code': '1f9cd', 'aliases': ['stand']}, + "person_white_hair": {'code': '1f9d1-200d-1f9b3', 'aliases': []}, + "person_with_crown": {'code': '1fac5', 'aliases': ['monarch', 'noble', 'regal', 'royalty']}, + "person_with_white_cane": {'code': '1f9d1-200d-1f9af', 'aliases': []}, + "petri_dish": {'code': '1f9eb', 'aliases': ['biology', 'culture']}, + "phone": {'code': '260e', 'aliases': ['telephone']}, + "phone_off": {'code': '1f4f4', 'aliases': []}, + "piano": {'code': '1f3b9', 'aliases': ['musical_keyboard']}, + "pickup_truck": {'code': '1f6fb', 'aliases': ['pick_up', 'pickup']}, + "picture": {'code': '1f5bc', 'aliases': ['framed_picture']}, + "pie": {'code': '1f967', 'aliases': ['filling', 'pastry']}, + "pig": {'code': '1f416', 'aliases': ['oink']}, + "pig_nose": {'code': '1f43d', 'aliases': []}, + "piglet": {'code': '1f437', 'aliases': []}, + "pilot": {'code': '1f9d1-200d-2708', 'aliases': []}, + "pin": {'code': '1f4cd', 'aliases': ['sewing_pin']}, + "pinched_fingers": {'code': '1f90c', 'aliases': ['fingers', 'hand_gesture', 'interrogation', 'pinched', 'sarcastic']}, + "pinching_hand": {'code': '1f90f', 'aliases': ['small_amount']}, + "pineapple": {'code': '1f34d', 'aliases': []}, + "ping_pong": {'code': '1f3d3', 'aliases': ['table_tennis']}, + "pirate_flag": {'code': '1f3f4-200d-2620', 'aliases': ['jolly_roger', 'plunder']}, + "pisces": {'code': '2653', 'aliases': []}, + "pizza": {'code': '1f355', 'aliases': []}, + "piñata": {'code': '1fa85', 'aliases': ['pinata']}, + "placard": {'code': '1faa7', 'aliases': ['demonstration', 'picket', 'protest', 'sign']}, + "place_holder": {'code': '1f4d1', 'aliases': []}, + "place_of_worship": {'code': '1f6d0', 'aliases': []}, + "play": {'code': '25b6', 'aliases': []}, + "play_pause": {'code': '23ef', 'aliases': []}, + "play_reverse": {'code': '25c0', 'aliases': []}, + "playground_slide": {'code': '1f6dd', 'aliases': ['amusement_park']}, + "playing_cards": {'code': '1f3b4', 'aliases': []}, + "pleading_face": {'code': '1f97a', 'aliases': ['begging', 'mercy', 'puppy_eyes']}, + "plunger": {'code': '1faa0', 'aliases': ['force_cup', 'suction']}, + "plus": {'code': '2795', 'aliases': ['add']}, + "point_down": {'code': '1f447', 'aliases': []}, + "point_left": {'code': '1f448', 'aliases': []}, + "point_right": {'code': '1f449', 'aliases': []}, + "point_up": {'code': '1f446', 'aliases': ['this']}, + "polar_bear": {'code': '1f43b-200d-2744', 'aliases': ['arctic']}, + "police": {'code': '1f46e', 'aliases': ['cop']}, + "police_car": {'code': '1f693', 'aliases': []}, + "pony": {'code': '1f434', 'aliases': []}, + "poodle": {'code': '1f429', 'aliases': []}, + "poop": {'code': '1f4a9', 'aliases': ['pile_of_poo']}, + "popcorn": {'code': '1f37f', 'aliases': []}, + "post_office": {'code': '1f3e4', 'aliases': []}, + "potable_water": {'code': '1f6b0', 'aliases': ['tap_water', 'drinking_water']}, + "potato": {'code': '1f954', 'aliases': []}, + "potted_plant": {'code': '1fab4', 'aliases': ['boring', 'grow', 'nurturing', 'useless']}, + "pouch": {'code': '1f45d', 'aliases': []}, + "pound_notes": {'code': '1f4b7', 'aliases': []}, + "pouring_liquid": {'code': '1fad7', 'aliases': ['spill']}, + "pray": {'code': '1f64f', 'aliases': ['welcome', 'thank_you', 'namaste']}, + "prayer_beads": {'code': '1f4ff', 'aliases': []}, + "pregnant": {'code': '1f930', 'aliases': ['expecting']}, + "pregnant_man": {'code': '1fac3', 'aliases': []}, + "pregnant_person": {'code': '1fac4', 'aliases': []}, + "pretzel": {'code': '1f968', 'aliases': ['twisted']}, + "previous_track": {'code': '23ee', 'aliases': ['skip_back']}, + "prince": {'code': '1f934', 'aliases': []}, + "princess": {'code': '1f478', 'aliases': []}, + "printer": {'code': '1f5a8', 'aliases': []}, + "privacy": {'code': '1f50f', 'aliases': ['key_signing', 'digital_security', 'protected']}, + "prohibited": {'code': '1f6ab', 'aliases': ['not_allowed']}, + "projector": {'code': '1f4fd', 'aliases': ['movie']}, + "puppy": {'code': '1f436', 'aliases': []}, + "purple_circle": {'code': '1f7e3', 'aliases': []}, + "purple_heart": {'code': '1f49c', 'aliases': ['bravery']}, + "purple_square": {'code': '1f7ea', 'aliases': []}, + "purse": {'code': '1f45b', 'aliases': []}, + "push_pin": {'code': '1f4cc', 'aliases': ['thumb_tack']}, + "put_litter_in_its_place": {'code': '1f6ae', 'aliases': []}, + "puzzle_piece": {'code': '1f9e9', 'aliases': ['interlocking', 'jigsaw', 'piece', 'puzzle']}, + "question": {'code': '2753', 'aliases': []}, + "rabbit": {'code': '1f407', 'aliases': []}, + "raccoon": {'code': '1f99d', 'aliases': ['curious', 'sly']}, + "racecar": {'code': '1f3ce', 'aliases': []}, + "radio": {'code': '1f4fb', 'aliases': []}, + "radio_button": {'code': '1f518', 'aliases': []}, + "radioactive": {'code': '2622', 'aliases': ['nuclear']}, + "rage": {'code': '1f621', 'aliases': ['mad', 'grumpy', 'very_angry']}, + "railway_car": {'code': '1f683', 'aliases': ['train_car']}, + "railway_track": {'code': '1f6e4', 'aliases': ['train_tracks']}, + "rainbow": {'code': '1f308', 'aliases': ['pride', 'lgbtq']}, + "rainbow_flag": {'code': '1f3f3-200d-1f308', 'aliases': []}, + "rainy": {'code': '1f327', 'aliases': ['soaked', 'drenched']}, + "raised_hands": {'code': '1f64c', 'aliases': ['praise']}, + "raising_hand": {'code': '1f64b', 'aliases': ['pick_me']}, + "ram": {'code': '1f40f', 'aliases': []}, + "ramen": {'code': '1f35c', 'aliases': ['noodles']}, + "rat": {'code': '1f400', 'aliases': []}, + "razor": {'code': '1fa92', 'aliases': ['sharp', 'shave']}, + "receipt": {'code': '1f9fe', 'aliases': ['accounting', 'bookkeeping', 'evidence', 'proof']}, + "record": {'code': '23fa', 'aliases': []}, + "recreational_vehicle": {'code': '1f699', 'aliases': ['jeep']}, + "recycle": {'code': '267b', 'aliases': []}, + "red_book": {'code': '1f4d5', 'aliases': ['closed_book']}, + "red_circle": {'code': '1f534', 'aliases': []}, + "red_envelope": {'code': '1f9e7', 'aliases': ['good_luck', 'hóngbāo', 'lai_see', 'hongbao']}, + "red_square": {'code': '1f7e5', 'aliases': ['red']}, + "red_triangle_down": {'code': '1f53b', 'aliases': []}, + "red_triangle_up": {'code': '1f53a', 'aliases': []}, + "registered": {'code': '00ae', 'aliases': ['r']}, + "relieved": {'code': '1f60c', 'aliases': []}, + "reminder_ribbon": {'code': '1f397', 'aliases': []}, + "repeat": {'code': '1f501', 'aliases': []}, + "repeat_one": {'code': '1f502', 'aliases': []}, + "reply": {'code': '21a9', 'aliases': ['left_hook']}, + "restroom": {'code': '1f6bb', 'aliases': []}, + "revolving_hearts": {'code': '1f49e', 'aliases': []}, + "rewind": {'code': '23ea', 'aliases': ['fast_reverse']}, + "rhinoceros": {'code': '1f98f', 'aliases': []}, + "ribbon": {'code': '1f380', 'aliases': ['decoration']}, + "rice": {'code': '1f35a', 'aliases': []}, + "right": {'code': '27a1', 'aliases': ['east']}, + "right_fist": {'code': '1f91c', 'aliases': []}, + "rightwards_hand": {'code': '1faf1', 'aliases': ['rightward']}, + "ring": {'code': '1f48d', 'aliases': []}, + "ring_buoy": {'code': '1f6df', 'aliases': ['float', 'life_preserver', 'life_saver', 'rescue']}, + "ringed_planet": {'code': '1fa90', 'aliases': ['saturn', 'saturnine']}, + "road": {'code': '1f6e3', 'aliases': ['motorway']}, + "robot": {'code': '1f916', 'aliases': []}, + "rock": {'code': '1faa8', 'aliases': ['boulder', 'heavy', 'solid', 'stone']}, + "rock_carving": {'code': '1f5ff', 'aliases': ['moyai']}, + "rock_on": {'code': '1f918', 'aliases': ['sign_of_the_horns']}, + "rocket": {'code': '1f680', 'aliases': []}, + "roll_of_paper": {'code': '1f9fb', 'aliases': ['paper_towels', 'toilet_paper']}, + "roller_coaster": {'code': '1f3a2', 'aliases': []}, + "roller_skate": {'code': '1f6fc', 'aliases': ['roller', 'skate']}, + "rolling_eyes": {'code': '1f644', 'aliases': []}, + "rolling_on_the_floor_laughing": {'code': '1f923', 'aliases': ['rofl']}, + "rolodex": {'code': '1f4c7', 'aliases': ['card_index']}, + "rooster": {'code': '1f413', 'aliases': ['alarm', 'cock-a-doodle-doo']}, + "rose": {'code': '1f339', 'aliases': []}, + "rosette": {'code': '1f3f5', 'aliases': []}, + "rowboat": {'code': '1f6a3', 'aliases': ['crew', 'sculling', 'rowing']}, + "rugby": {'code': '1f3c9', 'aliases': []}, + "ruler": {'code': '1f4cf', 'aliases': ['straightedge']}, + "running": {'code': '1f3c3', 'aliases': ['runner']}, + "running_shirt": {'code': '1f3bd', 'aliases': []}, + "sad": {'code': '2639', 'aliases': ['big_frown']}, + "safety_pin": {'code': '1f9f7', 'aliases': ['diaper', 'punk_rock']}, + "safety_vest": {'code': '1f9ba', 'aliases': ['emergency', 'vest']}, + "sagittarius": {'code': '2650', 'aliases': []}, + "sake": {'code': '1f376', 'aliases': []}, + "salad": {'code': '1f957', 'aliases': []}, + "salt": {'code': '1f9c2', 'aliases': ['shaker']}, + "saluting_face": {'code': '1fae1', 'aliases': ['salute', 'troops', 'yes']}, + "sandal": {'code': '1f461', 'aliases': ['flip_flops']}, + "sandwich": {'code': '1f96a', 'aliases': []}, + "santa": {'code': '1f385', 'aliases': []}, + "sari": {'code': '1f97b', 'aliases': []}, + "satellite": {'code': '1f6f0', 'aliases': []}, + "satellite_antenna": {'code': '1f4e1', 'aliases': []}, + "sauropod": {'code': '1f995', 'aliases': ['brachiosaurus', 'brontosaurus', 'diplodocus']}, + "saxophone": {'code': '1f3b7', 'aliases': []}, + "scarf": {'code': '1f9e3', 'aliases': ['neck']}, + "school": {'code': '1f3eb', 'aliases': []}, + "science": {'code': '1f52c', 'aliases': ['microscope']}, + "scientist": {'code': '1f9d1-200d-1f52c', 'aliases': []}, + "scissors": {'code': '2702', 'aliases': []}, + "scooter": {'code': '1f6f5', 'aliases': ['motor_bike']}, + "scorpion": {'code': '1f982', 'aliases': []}, + "scorpius": {'code': '264f', 'aliases': []}, + "scream": {'code': '1f631', 'aliases': []}, + "scream_cat": {'code': '1f640', 'aliases': ['weary_cat']}, + "screwdriver": {'code': '1fa9b', 'aliases': []}, + "scroll": {'code': '1f4dc', 'aliases': []}, + "seal": {'code': '1f9ad', 'aliases': ['sea_lion']}, + "search": {'code': '1f50d', 'aliases': ['find', 'magnifying_glass']}, + "seat": {'code': '1f4ba', 'aliases': []}, + "second_place": {'code': '1f948', 'aliases': ['silver']}, + "secret": {'code': '1f5dd', 'aliases': ['dungeon', 'old_key', 'encrypted', 'clue', 'hint']}, + "secure": {'code': '1f510', 'aliases': ['lock_with_key', 'safe', 'commitment', 'loyalty']}, + "see_no_evil": {'code': '1f648', 'aliases': []}, + "seedling": {'code': '1f331', 'aliases': ['sprout']}, + "seeing_stars": {'code': '1f4ab', 'aliases': []}, + "selfie": {'code': '1f933', 'aliases': []}, + "senbei": {'code': '1f358', 'aliases': ['rice_cracker']}, + "service_dog": {'code': '1f415-200d-1f9ba', 'aliases': ['assistance', 'service']}, + "seven": {'code': '0037-20e3', 'aliases': []}, + "sewing_needle": {'code': '1faa1', 'aliases': ['embroidery', 'stitches', 'sutures', 'tailoring']}, + "shamrock": {'code': '2618', 'aliases': ['clover']}, + "shark": {'code': '1f988', 'aliases': []}, + "shaved_ice": {'code': '1f367', 'aliases': []}, + "sheep": {'code': '1f411', 'aliases': ['baa']}, + "shell": {'code': '1f41a', 'aliases': ['seashell', 'conch', 'spiral_shell']}, + "shield": {'code': '1f6e1', 'aliases': []}, + "shinto_shrine": {'code': '26e9', 'aliases': []}, + "ship": {'code': '1f6a2', 'aliases': []}, + "shiro": {'code': '1f3ef', 'aliases': []}, + "shirt": {'code': '1f455', 'aliases': ['tshirt']}, + "shoe": {'code': '1f45e', 'aliases': []}, + "shooting_star": {'code': '1f320', 'aliases': ['wish']}, + "shopping_bags": {'code': '1f6cd', 'aliases': []}, + "shopping_cart": {'code': '1f6d2', 'aliases': ['shopping_trolley']}, + "shorts": {'code': '1fa73', 'aliases': ['pants']}, + "shower": {'code': '1f6bf', 'aliases': []}, + "shrimp": {'code': '1f990', 'aliases': []}, + "shrug": {'code': '1f937', 'aliases': []}, + "shuffle": {'code': '1f500', 'aliases': []}, + "shushing_face": {'code': '1f92b', 'aliases': ['shush']}, + "sick": {'code': '1f912', 'aliases': ['flu', 'face_with_thermometer', 'ill', 'fever']}, + "silence": {'code': '1f910', 'aliases': ['quiet', 'hush', 'zip_it', 'lips_are_sealed']}, + "silhouette": {'code': '1f464', 'aliases': ['shadow']}, + "silhouettes": {'code': '1f465', 'aliases': ['shadows']}, + "singer": {'code': '1f9d1-200d-1f3a4', 'aliases': []}, + "siren": {'code': '1f6a8', 'aliases': ['rotating_light', 'alert']}, + "six": {'code': '0036-20e3', 'aliases': []}, + "skateboard": {'code': '1f6f9', 'aliases': ['board']}, + "ski": {'code': '1f3bf', 'aliases': []}, + "skier": {'code': '26f7', 'aliases': []}, + "skull": {'code': '1f480', 'aliases': []}, + "skull_and_crossbones": {'code': '2620', 'aliases': ['pirate', 'death', 'hazard', 'toxic', 'poison']}, + "skunk": {'code': '1f9a8', 'aliases': ['stink']}, + "sled": {'code': '1f6f7', 'aliases': ['sledge', 'sleigh']}, + "sleeping": {'code': '1f634', 'aliases': []}, + "sleepy": {'code': '1f62a', 'aliases': []}, + "slot_machine": {'code': '1f3b0', 'aliases': []}, + "sloth": {'code': '1f9a5', 'aliases': ['lazy', 'slow']}, + "small_airplane": {'code': '1f6e9', 'aliases': []}, + "small_blue_diamond": {'code': '1f539', 'aliases': []}, + "small_glass": {'code': '1f943', 'aliases': []}, + "small_orange_diamond": {'code': '1f538', 'aliases': []}, + "smile": {'code': '1f642', 'aliases': []}, + "smile_cat": {'code': '1f638', 'aliases': []}, + "smiley": {'code': '1f603', 'aliases': []}, + "smiley_cat": {'code': '1f63a', 'aliases': []}, + "smiling_devil": {'code': '1f608', 'aliases': ['smiling_imp', 'smiling_face_with_horns']}, + "smiling_face": {'code': '263a', 'aliases': ['relaxed']}, + "smiling_face_with_hearts": {'code': '1f970', 'aliases': ['adore', 'crush']}, + "smiling_face_with_tear": {'code': '1f972', 'aliases': ['grateful', 'smiling', 'tear', 'touched']}, + "smirk": {'code': '1f60f', 'aliases': ['smug']}, + "smirk_cat": {'code': '1f63c', 'aliases': ['smug_cat']}, + "smoking": {'code': '1f6ac', 'aliases': []}, + "snail": {'code': '1f40c', 'aliases': []}, + "snake": {'code': '1f40d', 'aliases': ['hiss']}, + "sneezing": {'code': '1f927', 'aliases': []}, + "snowboarder": {'code': '1f3c2', 'aliases': []}, + "snowflake": {'code': '2744', 'aliases': []}, + "snowman": {'code': '2603', 'aliases': []}, + "snowy": {'code': '1f328', 'aliases': ['snowstorm']}, + "snowy_mountain": {'code': '1f3d4', 'aliases': []}, + "soap": {'code': '1f9fc', 'aliases': ['bar', 'bathing', 'lather', 'soapdish']}, + "sob": {'code': '1f62d', 'aliases': []}, + "socks": {'code': '1f9e6', 'aliases': ['stocking']}, + "soft_serve": {'code': '1f366', 'aliases': ['soft_ice_cream']}, + "softball": {'code': '1f94e', 'aliases': ['glove', 'underarm']}, + "softer": {'code': '1f509', 'aliases': []}, + "soon": {'code': '1f51c', 'aliases': []}, + "sort": {'code': '1f5c2', 'aliases': []}, + "sos": {'code': '1f198', 'aliases': []}, + "space_invader": {'code': '1f47e', 'aliases': []}, + "spades": {'code': '2660', 'aliases': []}, + "spaghetti": {'code': '1f35d', 'aliases': []}, + "sparkle": {'code': '2747', 'aliases': []}, + "sparkler": {'code': '1f387', 'aliases': []}, + "sparkles": {'code': '2728', 'aliases': ['glamour']}, + "sparkling_heart": {'code': '1f496', 'aliases': []}, + "speak_no_evil": {'code': '1f64a', 'aliases': []}, + "speaker": {'code': '1f508', 'aliases': []}, + "speaking_head": {'code': '1f5e3', 'aliases': []}, + "speech_bubble": {'code': '1f5e8', 'aliases': []}, + "speechless": {'code': '1f636', 'aliases': ['no_mouth', 'blank', 'poker_face']}, + "speedboat": {'code': '1f6a4', 'aliases': []}, + "spider": {'code': '1f577', 'aliases': []}, + "spiral_calendar": {'code': '1f5d3', 'aliases': ['pad']}, + "spiral_notepad": {'code': '1f5d2', 'aliases': []}, + "spock": {'code': '1f596', 'aliases': ['live_long_and_prosper']}, + "sponge": {'code': '1f9fd', 'aliases': ['absorbing', 'porous']}, + "spoon": {'code': '1f944', 'aliases': []}, + "squared_ok": {'code': '1f197', 'aliases': []}, + "squared_up": {'code': '1f199', 'aliases': []}, + "squid": {'code': '1f991', 'aliases': []}, + "stadium": {'code': '1f3df', 'aliases': []}, + "star": {'code': '2b50', 'aliases': []}, + "star_and_crescent": {'code': '262a', 'aliases': ['islam']}, + "star_of_david": {'code': '2721', 'aliases': ['judaism']}, + "star_struck": {'code': '1f929', 'aliases': []}, + "station": {'code': '1f689', 'aliases': []}, + "statue": {'code': '1f5fd', 'aliases': ['new_york', 'statue_of_liberty']}, + "stethoscope": {'code': '1fa7a', 'aliases': []}, + "stock_market": {'code': '1f4b9', 'aliases': []}, + "stop": {'code': '1f91a', 'aliases': []}, + "stop_button": {'code': '23f9', 'aliases': []}, + "stop_sign": {'code': '1f6d1', 'aliases': ['octagonal_sign']}, + "stopwatch": {'code': '23f1', 'aliases': []}, + "strawberry": {'code': '1f353', 'aliases': []}, + "strike": {'code': '1f3b3', 'aliases': ['bowling']}, + "stuck_out_tongue": {'code': '1f61b', 'aliases': ['mischievous']}, + "stuck_out_tongue_closed_eyes": {'code': '1f61d', 'aliases': []}, + "stuck_out_tongue_wink": {'code': '1f61c', 'aliases': ['joking', 'crazy']}, + "student": {'code': '1f9d1-200d-1f393', 'aliases': []}, + "studio_microphone": {'code': '1f399', 'aliases': []}, + "suburb": {'code': '1f3e1', 'aliases': []}, + "subway": {'code': '1f687', 'aliases': []}, + "sun_face": {'code': '1f31e', 'aliases': []}, + "sunflower": {'code': '1f33b', 'aliases': []}, + "sunglasses": {'code': '1f60e', 'aliases': []}, + "sunny": {'code': '2600', 'aliases': []}, + "sunrise": {'code': '1f305', 'aliases': ['ocean_sunrise']}, + "sunset": {'code': '1f306', 'aliases': []}, + "sunshowers": {'code': '1f326', 'aliases': ['sun_and_rain', 'partly_sunny_with_rain']}, + "superhero": {'code': '1f9b8', 'aliases': []}, + "supervillain": {'code': '1f9b9', 'aliases': []}, + "surf": {'code': '1f3c4', 'aliases': []}, + "sushi": {'code': '1f363', 'aliases': []}, + "suspension_railway": {'code': '1f69f', 'aliases': []}, + "swan": {'code': '1f9a2', 'aliases': ['cygnet', 'ugly_duckling']}, + "sweat": {'code': '1f613', 'aliases': []}, + "sweat_drops": {'code': '1f4a6', 'aliases': []}, + "sweat_smile": {'code': '1f605', 'aliases': []}, + "swim": {'code': '1f3ca', 'aliases': []}, + "symbols": {'code': '1f523', 'aliases': []}, + "synagogue": {'code': '1f54d', 'aliases': []}, + "t_rex": {'code': '1f996', 'aliases': ['tyrannosaurus_rex']}, + "taco": {'code': '1f32e', 'aliases': []}, + "tada": {'code': '1f389', 'aliases': []}, + "take_off": {'code': '1f6eb', 'aliases': ['departure', 'airplane_departure']}, + "takeout_box": {'code': '1f961', 'aliases': ['oyster_pail']}, + "taking_a_picture": {'code': '1f4f8', 'aliases': ['say_cheese']}, + "tamale": {'code': '1fad4', 'aliases': ['mexican', 'wrapped']}, + "taurus": {'code': '2649', 'aliases': []}, + "taxi": {'code': '1f695', 'aliases': ['rideshare']}, + "tea": {'code': '1f375', 'aliases': []}, + "teacher": {'code': '1f9d1-200d-1f3eb', 'aliases': []}, + "teapot": {'code': '1fad6', 'aliases': []}, + "technologist": {'code': '1f9d1-200d-1f4bb', 'aliases': []}, + "teddy_bear": {'code': '1f9f8', 'aliases': ['plaything', 'plush', 'stuffed']}, + "telescope": {'code': '1f52d', 'aliases': []}, + "temperature": {'code': '1f321', 'aliases': ['thermometer', 'warm']}, + "tempura": {'code': '1f364', 'aliases': []}, + "ten": {'code': '1f51f', 'aliases': []}, + "tennis": {'code': '1f3be', 'aliases': []}, + "tent": {'code': '26fa', 'aliases': ['camping']}, + "test_tube": {'code': '1f9ea', 'aliases': ['chemistry']}, + "thinking": {'code': '1f914', 'aliases': []}, + "third_place": {'code': '1f949', 'aliases': ['bronze']}, + "thong_sandal": {'code': '1fa74', 'aliases': ['beach_sandals', 'sandals', 'thong_sandals', 'thongs', 'zōri', 'zori']}, + "thought": {'code': '1f4ad', 'aliases': ['dream']}, + "thread": {'code': '1f9f5', 'aliases': ['spool', 'string']}, + "three": {'code': '0033-20e3', 'aliases': []}, + "thunderstorm": {'code': '26c8', 'aliases': ['thunder_and_rain']}, + "ticket": {'code': '1f39f', 'aliases': []}, + "tie": {'code': '1f454', 'aliases': []}, + "tiger": {'code': '1f405', 'aliases': []}, + "tiger_cub": {'code': '1f42f', 'aliases': []}, + "time": {'code': '1f557', 'aliases': ['clock']}, + "time_ticking": {'code': '23f3', 'aliases': ['hourglass']}, + "timer": {'code': '23f2', 'aliases': []}, + "times_up": {'code': '231b', 'aliases': ['hourglass_done']}, + "tm": {'code': '2122', 'aliases': ['trademark']}, + "toilet": {'code': '1f6bd', 'aliases': []}, + "tomato": {'code': '1f345', 'aliases': []}, + "tongue": {'code': '1f445', 'aliases': []}, + "toolbox": {'code': '1f9f0', 'aliases': ['chest']}, + "tooth": {'code': '1f9b7', 'aliases': ['dentist']}, + "toothbrush": {'code': '1faa5', 'aliases': ['bathroom', 'brush', 'dental', 'hygiene', 'teeth']}, + "top": {'code': '1f51d', 'aliases': []}, + "top_hat": {'code': '1f3a9', 'aliases': []}, + "tornado": {'code': '1f32a', 'aliases': []}, + "tower": {'code': '1f5fc', 'aliases': ['tokyo_tower']}, + "trackball": {'code': '1f5b2', 'aliases': []}, + "tractor": {'code': '1f69c', 'aliases': []}, + "traffic_light": {'code': '1f6a6', 'aliases': ['vertical_traffic_light']}, + "train": {'code': '1f682', 'aliases': ['steam_locomotive']}, + "tram": {'code': '1f68b', 'aliases': ['streetcar']}, + "transgender_flag": {'code': '1f3f3-fe0f-200d-26a7-fe0f', 'aliases': ['light_blue', 'pink']}, + "transgender_symbol": {'code': '26a7', 'aliases': []}, + "tree": {'code': '1f333', 'aliases': ['deciduous_tree']}, + "triangular_flag": {'code': '1f6a9', 'aliases': []}, + "trident": {'code': '1f531', 'aliases': []}, + "triumph": {'code': '1f624', 'aliases': []}, + "troll": {'code': '1f9cc', 'aliases': ['fairy_tale', 'fantasy', 'monster']}, + "trolley": {'code': '1f68e', 'aliases': []}, + "trophy": {'code': '1f3c6', 'aliases': ['winner']}, + "tropical_drink": {'code': '1f379', 'aliases': []}, + "tropical_fish": {'code': '1f420', 'aliases': []}, + "truck": {'code': '1f69b', 'aliases': ['tractor-trailer', 'big_rig', 'semi_truck', 'transport_truck']}, + "trumpet": {'code': '1f3ba', 'aliases': []}, + "tulip": {'code': '1f337', 'aliases': ['flower']}, + "turban": {'code': '1f473', 'aliases': []}, + "turkey": {'code': '1f983', 'aliases': []}, + "turtle": {'code': '1f422', 'aliases': ['tortoise']}, + "tuxedo": {'code': '1f935', 'aliases': []}, + "tv": {'code': '1f4fa', 'aliases': ['television']}, + "two": {'code': '0032-20e3', 'aliases': []}, + "two_hearts": {'code': '1f495', 'aliases': []}, + "two_men_holding_hands": {'code': '1f46c', 'aliases': ['men_couple']}, + "two_women_holding_hands": {'code': '1f46d', 'aliases': ['women_couple']}, + "umbrella": {'code': '2602', 'aliases': []}, + "umbrella_with_rain": {'code': '2614', 'aliases': []}, + "umm": {'code': '1f4ac', 'aliases': ['speech_balloon']}, + "unamused": {'code': '1f612', 'aliases': []}, + "underage": {'code': '1f51e', 'aliases': ['nc17']}, + "unicorn": {'code': '1f984', 'aliases': []}, + "unlocked": {'code': '1f513', 'aliases': []}, + "unread_mail": {'code': '1f4ec', 'aliases': []}, + "up": {'code': '2b06', 'aliases': ['north']}, + "up_down": {'code': '2195', 'aliases': []}, + "upper_left": {'code': '2196', 'aliases': ['north_west']}, + "upper_right": {'code': '2197', 'aliases': ['north_east']}, + "upside_down": {'code': '1f643', 'aliases': ['oops']}, + "upvote": {'code': '1f53c', 'aliases': ['up_button', 'increase']}, + "vampire": {'code': '1f9db', 'aliases': []}, + "vase": {'code': '1f3fa', 'aliases': ['amphora']}, + "vhs": {'code': '1f4fc', 'aliases': ['videocassette']}, + "vibration_mode": {'code': '1f4f3', 'aliases': []}, + "video_camera": {'code': '1f4f9', 'aliases': ['video_recorder']}, + "video_game": {'code': '1f3ae', 'aliases': []}, + "violin": {'code': '1f3bb', 'aliases': []}, + "virgo": {'code': '264d', 'aliases': []}, + "volcano": {'code': '1f30b', 'aliases': []}, + "volleyball": {'code': '1f3d0', 'aliases': []}, + "volume": {'code': '1f39a', 'aliases': ['level_slider']}, + "vs": {'code': '1f19a', 'aliases': []}, + "waffle": {'code': '1f9c7', 'aliases': ['indecisive', 'iron']}, + "wait_one_second": {'code': '261d', 'aliases': ['point_of_information', 'asking_a_question']}, + "walking": {'code': '1f6b6', 'aliases': ['pedestrian']}, + "waning_crescent_moon": {'code': '1f318', 'aliases': []}, + "waning_gibbous_moon": {'code': '1f316', 'aliases': ['gibbous']}, + "warning": {'code': '26a0', 'aliases': ['caution', 'danger']}, + "wastebasket": {'code': '1f5d1', 'aliases': ['trash_can']}, + "watch": {'code': '231a', 'aliases': []}, + "water_buffalo": {'code': '1f403', 'aliases': []}, + "water_polo": {'code': '1f93d', 'aliases': []}, + "watermelon": {'code': '1f349', 'aliases': []}, + "wave": {'code': '1f44b', 'aliases': ['hello', 'hi']}, + "wavy_dash": {'code': '3030', 'aliases': []}, + "waxing_crescent_moon": {'code': '1f312', 'aliases': ['waxing']}, + "waxing_moon": {'code': '1f314', 'aliases': []}, + "wc": {'code': '1f6be', 'aliases': ['water_closet']}, + "weary": {'code': '1f629', 'aliases': ['distraught']}, + "web": {'code': '1f578', 'aliases': ['spider_web']}, + "wedding": {'code': '1f492', 'aliases': []}, + "whale": {'code': '1f433', 'aliases': []}, + "wheel": {'code': '1f6de', 'aliases': ['tire', 'turn']}, + "wheel_of_dharma": {'code': '2638', 'aliases': ['buddhism']}, + "white_and_black_square": {'code': '1f532', 'aliases': []}, + "white_cane": {'code': '1f9af', 'aliases': []}, + "white_circle": {'code': '26aa', 'aliases': []}, + "white_flag": {'code': '1f3f3', 'aliases': ['surrender']}, + "white_flower": {'code': '1f4ae', 'aliases': []}, + "white_heart": {'code': '1f90d', 'aliases': []}, + "white_large_square": {'code': '2b1c', 'aliases': []}, + "white_medium_small_square": {'code': '25fd', 'aliases': []}, + "white_medium_square": {'code': '25fb', 'aliases': []}, + "white_small_square": {'code': '25ab', 'aliases': []}, + "wilted_flower": {'code': '1f940', 'aliases': ['crushed']}, + "wind_chime": {'code': '1f390', 'aliases': []}, + "window": {'code': '1fa9f', 'aliases': ['frame', 'fresh_air', 'opening', 'transparent', 'view']}, + "windy": {'code': '1f32c', 'aliases': ['mother_nature']}, + "wine": {'code': '1f377', 'aliases': []}, + "wink": {'code': '1f609', 'aliases': []}, + "wish_tree": {'code': '1f38b', 'aliases': ['tanabata_tree']}, + "wolf": {'code': '1f43a', 'aliases': []}, + "woman": {'code': '1f469', 'aliases': []}, + "woman_artist": {'code': '1f469-200d-1f3a8', 'aliases': []}, + "woman_astronaut": {'code': '1f469-200d-1f680', 'aliases': []}, + "woman_bald": {'code': '1f469-200d-1f9b2', 'aliases': []}, + "woman_beard": {'code': '1f9d4-200d-2640', 'aliases': []}, + "woman_biking": {'code': '1f6b4-200d-2640', 'aliases': []}, + "woman_blond_hair": {'code': '1f471-200d-2640', 'aliases': ['blond_haired_woman', 'blonde']}, + "woman_bouncing_ball": {'code': '26f9-fe0f-200d-2640-fe0f', 'aliases': []}, + "woman_bowing": {'code': '1f647-200d-2640', 'aliases': []}, + "woman_cartwheeling": {'code': '1f938-200d-2640', 'aliases': []}, + "woman_climbing": {'code': '1f9d7-200d-2640', 'aliases': []}, + "woman_construction_worker": {'code': '1f477-200d-2640', 'aliases': []}, + "woman_cook": {'code': '1f469-200d-1f373', 'aliases': []}, + "woman_curly_hair": {'code': '1f469-200d-1f9b1', 'aliases': []}, + "woman_detective": {'code': '1f575-fe0f-200d-2640-fe0f', 'aliases': []}, + "woman_elf": {'code': '1f9dd-200d-2640', 'aliases': []}, + "woman_facepalming": {'code': '1f926-200d-2640', 'aliases': []}, + "woman_factory_worker": {'code': '1f469-200d-1f3ed', 'aliases': []}, + "woman_fairy": {'code': '1f9da-200d-2640', 'aliases': []}, + "woman_farmer": {'code': '1f469-200d-1f33e', 'aliases': []}, + "woman_feeding_baby": {'code': '1f469-200d-1f37c', 'aliases': []}, + "woman_firefighter": {'code': '1f469-200d-1f692', 'aliases': []}, + "woman_frowning": {'code': '1f64d-200d-2640', 'aliases': []}, + "woman_genie": {'code': '1f9de-200d-2640', 'aliases': []}, + "woman_gesturing_no": {'code': '1f645-200d-2640', 'aliases': []}, + "woman_gesturing_ok": {'code': '1f646-200d-2640', 'aliases': []}, + "woman_getting_haircut": {'code': '1f487-200d-2640', 'aliases': []}, + "woman_getting_massage": {'code': '1f486-200d-2640', 'aliases': []}, + "woman_golfing": {'code': '1f3cc-fe0f-200d-2640-fe0f', 'aliases': []}, + "woman_guard": {'code': '1f482-200d-2640', 'aliases': []}, + "woman_health_worker": {'code': '1f469-200d-2695', 'aliases': []}, + "woman_in_lotus_position": {'code': '1f9d8-200d-2640', 'aliases': []}, + "woman_in_manual_wheelchair": {'code': '1f469-200d-1f9bd', 'aliases': []}, + "woman_in_motorized_wheelchair": {'code': '1f469-200d-1f9bc', 'aliases': []}, + "woman_in_steamy_room": {'code': '1f9d6-200d-2640', 'aliases': []}, + "woman_in_tuxedo": {'code': '1f935-200d-2640', 'aliases': []}, + "woman_judge": {'code': '1f469-200d-2696', 'aliases': []}, + "woman_juggling": {'code': '1f939-200d-2640', 'aliases': []}, + "woman_kneeling": {'code': '1f9ce-200d-2640', 'aliases': []}, + "woman_lifting_weights": {'code': '1f3cb-fe0f-200d-2640-fe0f', 'aliases': []}, + "woman_mage": {'code': '1f9d9-200d-2640', 'aliases': []}, + "woman_mechanic": {'code': '1f469-200d-1f527', 'aliases': []}, + "woman_mountain_biking": {'code': '1f6b5-200d-2640', 'aliases': []}, + "woman_office_worker": {'code': '1f469-200d-1f4bc', 'aliases': []}, + "woman_pilot": {'code': '1f469-200d-2708', 'aliases': []}, + "woman_playing_handball": {'code': '1f93e-200d-2640', 'aliases': []}, + "woman_playing_water_polo": {'code': '1f93d-200d-2640', 'aliases': []}, + "woman_police_officer": {'code': '1f46e-200d-2640', 'aliases': []}, + "woman_pouting": {'code': '1f64e-200d-2640', 'aliases': []}, + "woman_raising_hand": {'code': '1f64b-200d-2640', 'aliases': []}, + "woman_red_hair": {'code': '1f469-200d-1f9b0', 'aliases': []}, + "woman_rowing_boat": {'code': '1f6a3-200d-2640', 'aliases': []}, + "woman_running": {'code': '1f3c3-200d-2640', 'aliases': []}, + "woman_scientist": {'code': '1f469-200d-1f52c', 'aliases': []}, + "woman_shrugging": {'code': '1f937-200d-2640', 'aliases': []}, + "woman_singer": {'code': '1f469-200d-1f3a4', 'aliases': []}, + "woman_standing": {'code': '1f9cd-200d-2640', 'aliases': []}, + "woman_student": {'code': '1f469-200d-1f393', 'aliases': []}, + "woman_superhero": {'code': '1f9b8-200d-2640', 'aliases': []}, + "woman_supervillain": {'code': '1f9b9-200d-2640', 'aliases': []}, + "woman_surfing": {'code': '1f3c4-200d-2640', 'aliases': []}, + "woman_swimming": {'code': '1f3ca-200d-2640', 'aliases': []}, + "woman_teacher": {'code': '1f469-200d-1f3eb', 'aliases': []}, + "woman_technologist": {'code': '1f469-200d-1f4bb', 'aliases': []}, + "woman_tipping_hand": {'code': '1f481-200d-2640', 'aliases': []}, + "woman_vampire": {'code': '1f9db-200d-2640', 'aliases': []}, + "woman_walking": {'code': '1f6b6-200d-2640', 'aliases': []}, + "woman_wearing_turban": {'code': '1f473-200d-2640', 'aliases': []}, + "woman_white_hair": {'code': '1f469-200d-1f9b3', 'aliases': []}, + "woman_with_headscarf": {'code': '1f9d5', 'aliases': ['headscarf', 'hijab', 'mantilla', 'tichel']}, + "woman_with_veil": {'code': '1f470-200d-2640', 'aliases': []}, + "woman_with_white_cane": {'code': '1f469-200d-1f9af', 'aliases': []}, + "woman_zombie": {'code': '1f9df-200d-2640', 'aliases': []}, + "women_with_bunny_ears": {'code': '1f46f-200d-2640', 'aliases': []}, + "women_wrestling": {'code': '1f93c-200d-2640', 'aliases': []}, + "womens": {'code': '1f6ba', 'aliases': []}, + "wood": {'code': '1fab5', 'aliases': ['log', 'timber']}, + "woozy_face": {'code': '1f974', 'aliases': ['intoxicated', 'tipsy', 'uneven_eyes', 'wavy_mouth']}, + "work_in_progress": {'code': '1f6a7', 'aliases': ['construction_zone']}, + "working_on_it": {'code': '1f6e0', 'aliases': ['hammer_and_wrench', 'tools']}, + "worm": {'code': '1fab1', 'aliases': ['annelid', 'earthworm', 'parasite']}, + "worried": {'code': '1f61f', 'aliases': []}, + "wrestling": {'code': '1f93c', 'aliases': []}, + "writing": {'code': '270d', 'aliases': []}, + "www": {'code': '1f310', 'aliases': ['globe']}, + "x": {'code': '274e', 'aliases': []}, + "x_ray": {'code': '1fa7b', 'aliases': ['bones', 'medical']}, + "yam": {'code': '1f360', 'aliases': ['sweet_potato']}, + "yarn": {'code': '1f9f6', 'aliases': ['crochet', 'knit']}, + "yawning_face": {'code': '1f971', 'aliases': ['bored', 'yawn']}, + "yellow_circle": {'code': '1f7e1', 'aliases': ['yellow']}, + "yellow_heart": {'code': '1f49b', 'aliases': ['heart_of_gold']}, + "yellow_large_square": {'code': '1f7e8', 'aliases': []}, + "yen_banknotes": {'code': '1f4b4', 'aliases': []}, + "yin_yang": {'code': '262f', 'aliases': []}, + "yo_yo": {'code': '1fa80', 'aliases': ['fluctuate']}, + "yum": {'code': '1f60b', 'aliases': []}, + "zany_face": {'code': '1f92a', 'aliases': ['goofy', 'small']}, + "zebra": {'code': '1f993', 'aliases': ['stripe']}, + "zero": {'code': '0030-20e3', 'aliases': []}, + "zombie": {'code': '1f9df', 'aliases': []}, + "zzz": {'code': '1f4a4', 'aliases': []}, +} # fmt: on From ad2389f086d6f7b7ab1191dfd87f6955ad237aac Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Thu, 23 Mar 2023 09:22:13 +0000 Subject: [PATCH 013/276] refactor: model: Change OrderedDict to Dict for active_emoji_data. active_emoji_data was returned by model.Model.generate_all_emoji_data as an OrderedDict to preserve its sorted insertion order when used in core.Controller.show_emoji_picker to render an EmojiPickerView. It was also used in model.Model.toggle_message_reaction to determine which emoji reaction to toggle. Since Dict also preserves insertion order from python3.7, we can safely return active_emoji_data as a Dict without any change in behavior. --- zulipterminal/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index b99df970e7..25100fc8e0 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -733,7 +733,7 @@ def generate_all_emoji_data( all_emoji_names.append(emoji_name) all_emoji_names.extend(emoji_data["aliases"]) all_emoji_names = sorted(all_emoji_names) - active_emoji_data = OrderedDict(sorted(all_emoji_data.items())) + active_emoji_data = dict(sorted(all_emoji_data.items())) return active_emoji_data, all_emoji_names def get_messages( From 4ac61b5ca8e392a1470c9272d61bcc01b46747c1 Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Thu, 23 Mar 2023 11:02:48 +0000 Subject: [PATCH 014/276] refactor: keys: Use Dict for KEY_BINDINGS and HELP_CATEGORIES. KEY_BINDINGS was initialized as an OrderedDict to preserve its insertion order when used in lint-hotkeys.read_help_categories to generate hotkeys.md and views to render a HelpView. HELP_CATEGORIES was initialized as an OrderedDict to preserve its insertion order when used in lint-hotkeys.get_hotkeys_file_string to generate hotkeys.md and views to render a HelpView. Since Dict also preserves insertion order from python3.7, we can safely initialize both as a Dict without any change in behavior. --- zulipterminal/config/keys.py | 309 +++++++++++++++++------------------ 1 file changed, 153 insertions(+), 156 deletions(-) diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py index 62482b6c58..453b319053 100644 --- a/zulipterminal/config/keys.py +++ b/zulipterminal/config/keys.py @@ -2,8 +2,7 @@ Keybindings and their helper functions """ -from collections import OrderedDict -from typing import List +from typing import Dict, List from typing_extensions import NotRequired, TypedDict from urwid.command_map import ( @@ -26,389 +25,387 @@ class KeyBinding(TypedDict): # fmt: off -KEY_BINDINGS: 'OrderedDict[str, KeyBinding]' = OrderedDict([ +KEY_BINDINGS: Dict[str, KeyBinding] = { # Key that is displayed in the UI is determined by the method # primary_key_for_command. (Currently the first key in the list) - ('HELP', { + 'HELP': { 'keys': ['?'], 'help_text': 'Show/hide help menu', 'excluded_from_random_tips': True, 'key_category': 'general', - }), - ('MARKDOWN_HELP', { + }, + 'MARKDOWN_HELP': { 'keys': ['meta m'], 'help_text': 'Show/hide markdown help menu', 'key_category': 'general', - }), - ('ABOUT', { + }, + 'ABOUT': { 'keys': ['meta ?'], 'help_text': 'Show/hide about menu', 'key_category': 'general', - }), - ('GO_BACK', { + }, + 'GO_BACK': { 'keys': ['esc'], 'help_text': 'Go Back', 'excluded_from_random_tips': False, 'key_category': 'general', - }), - ('OPEN_DRAFT', { + }, + 'OPEN_DRAFT': { 'keys': ['d'], 'help_text': 'Open draft message saved in this session', 'key_category': 'general', - }), - ('GO_UP', { + }, + 'GO_UP': { 'keys': ['up', 'k'], 'help_text': 'Go up / Previous message', 'key_category': 'navigation', - }), - ('GO_DOWN', { + }, + 'GO_DOWN': { 'keys': ['down', 'j'], 'help_text': 'Go down / Next message', 'key_category': 'navigation', - }), - ('GO_LEFT', { + }, + 'GO_LEFT': { 'keys': ['left', 'h'], 'help_text': 'Go left', 'key_category': 'navigation', - }), - ('GO_RIGHT', { + }, + 'GO_RIGHT': { 'keys': ['right', 'l'], 'help_text': 'Go right', 'key_category': 'navigation', - }), - ('SCROLL_UP', { + }, + 'SCROLL_UP': { 'keys': ['page up', 'K'], 'help_text': 'Scroll up', 'key_category': 'navigation', - }), - ('SCROLL_DOWN', { + }, + 'SCROLL_DOWN': { 'keys': ['page down', 'J'], 'help_text': 'Scroll down', 'key_category': 'navigation', - }), - ('GO_TO_BOTTOM', { + }, + 'GO_TO_BOTTOM': { 'keys': ['end', 'G'], 'help_text': 'Go to bottom / Last message', 'key_category': 'navigation', - }), - ('REPLY_MESSAGE', { + }, + 'REPLY_MESSAGE': { 'keys': ['r', 'enter'], 'help_text': 'Reply to the current message', 'key_category': 'msg_actions', - }), - ('MENTION_REPLY', { + }, + 'MENTION_REPLY': { 'keys': ['@'], 'help_text': 'Reply mentioning the sender of the current message', 'key_category': 'msg_actions', - }), - ('QUOTE_REPLY', { + }, + 'QUOTE_REPLY': { 'keys': ['>'], 'help_text': 'Reply quoting the current message text', 'key_category': 'msg_actions', - }), - ('REPLY_AUTHOR', { + }, + 'REPLY_AUTHOR': { 'keys': ['R'], 'help_text': 'Reply directly to the sender of the current message', 'key_category': 'msg_actions', - }), - ('EDIT_MESSAGE', { + }, + 'EDIT_MESSAGE': { 'keys': ['e'], 'help_text': "Edit message's content or topic", 'key_category': 'msg_actions' - }), - ('STREAM_MESSAGE', { + }, + 'STREAM_MESSAGE': { 'keys': ['c'], 'help_text': 'New message to a stream', 'key_category': 'msg_actions', - }), - ('PRIVATE_MESSAGE', { + }, + 'PRIVATE_MESSAGE': { 'keys': ['x'], 'help_text': 'New message to a person or group of people', 'key_category': 'msg_actions', - }), - ('CYCLE_COMPOSE_FOCUS', { + }, + 'CYCLE_COMPOSE_FOCUS': { 'keys': ['tab'], 'help_text': 'Cycle through recipient and content boxes', 'key_category': 'msg_compose', - }), - ('SEND_MESSAGE', { + }, + 'SEND_MESSAGE': { 'keys': ['ctrl d', 'meta enter'], 'help_text': 'Send a message', 'key_category': 'msg_compose', - }), - ('SAVE_AS_DRAFT', { + }, + 'SAVE_AS_DRAFT': { 'keys': ['meta s'], 'help_text': 'Save current message as a draft', 'key_category': 'msg_compose', - }), - ('AUTOCOMPLETE', { + }, + 'AUTOCOMPLETE': { 'keys': ['ctrl f'], 'help_text': ('Autocomplete @mentions, #stream_names, :emoji:' ' and topics'), 'key_category': 'msg_compose', - }), - ('AUTOCOMPLETE_REVERSE', { + }, + 'AUTOCOMPLETE_REVERSE': { 'keys': ['ctrl r'], 'help_text': 'Cycle through autocomplete suggestions in reverse', 'key_category': 'msg_compose', - }), - ('ADD_REACTION', { + }, + 'ADD_REACTION': { 'keys': [':'], 'help_text': 'Show/hide Emoji picker popup for current message', 'key_category': 'msg_actions', - }), - ('STREAM_NARROW', { + }, + 'STREAM_NARROW': { 'keys': ['s'], 'help_text': 'Narrow to the stream of the current message', 'key_category': 'msg_actions', - }), - ('TOPIC_NARROW', { + }, + 'TOPIC_NARROW': { 'keys': ['S'], 'help_text': 'Narrow to the topic of the current message', 'key_category': 'msg_actions', - }), - ('NARROW_MESSAGE_RECIPIENT', { + }, + 'NARROW_MESSAGE_RECIPIENT': { 'keys': ['meta .'], 'help_text': 'Narrow to compose box message recipient', 'key_category': 'msg_compose', - }), - ('TOGGLE_NARROW', { + }, + 'TOGGLE_NARROW': { 'keys': ['z'], 'help_text': 'Narrow to a topic/direct-chat, or stream/all-direct-messages', 'key_category': 'msg_actions', - }), - ('TOGGLE_TOPIC', { + }, + 'TOGGLE_TOPIC': { 'keys': ['t'], 'help_text': 'Toggle topics in a stream', 'key_category': 'stream_list', - }), - ('ALL_MESSAGES', { + }, + 'ALL_MESSAGES': { 'keys': ['a', 'esc'], 'help_text': 'Narrow to all messages', 'key_category': 'navigation', - }), - ('ALL_PM', { + }, + 'ALL_PM': { 'keys': ['P'], 'help_text': 'Narrow to all direct messages', 'key_category': 'navigation', - }), - ('ALL_STARRED', { + }, + 'ALL_STARRED': { 'keys': ['f'], 'help_text': 'Narrow to all starred messages', 'key_category': 'navigation', - }), - ('ALL_MENTIONS', { + }, + 'ALL_MENTIONS': { 'keys': ['#'], 'help_text': "Narrow to messages in which you're mentioned", 'key_category': 'navigation', - }), - ('NEXT_UNREAD_TOPIC', { + }, + 'NEXT_UNREAD_TOPIC': { 'keys': ['n'], 'help_text': 'Next unread topic', 'key_category': 'navigation', - }), - ('NEXT_UNREAD_PM', { + }, + 'NEXT_UNREAD_PM': { 'keys': ['p'], 'help_text': 'Next unread direct message', 'key_category': 'navigation', - }), - ('SEARCH_PEOPLE', { + }, + 'SEARCH_PEOPLE': { 'keys': ['w'], 'help_text': 'Search Users', 'key_category': 'searching', - }), - ('SEARCH_MESSAGES', { + }, + 'SEARCH_MESSAGES': { 'keys': ['/'], 'help_text': 'Search Messages', 'key_category': 'searching', - }), - ('SEARCH_STREAMS', { + }, + 'SEARCH_STREAMS': { 'keys': ['q'], 'help_text': 'Search Streams', 'key_category': 'searching', - }), - ('SEARCH_TOPICS', { + }, + 'SEARCH_TOPICS': { 'keys': ['q'], 'help_text': 'Search topics in a stream', 'key_category': 'searching', - }), - ('SEARCH_EMOJIS', { + }, + 'SEARCH_EMOJIS': { 'keys': ['p'], 'help_text': 'Search emojis from Emoji-picker popup', 'excluded_from_random_tips': True, 'key_category': 'searching', - }), - ('TOGGLE_MUTE_STREAM', { + }, + 'TOGGLE_MUTE_STREAM': { 'keys': ['m'], 'help_text': 'Mute/unmute Streams', 'key_category': 'stream_list', - }), - ('ENTER', { + }, + 'ENTER': { 'keys': ['enter'], 'help_text': 'Perform current action', 'key_category': 'navigation', - }), - ('THUMBS_UP', { + }, + 'THUMBS_UP': { 'keys': ['+'], 'help_text': 'Add/remove thumbs-up reaction to the current message', 'key_category': 'msg_actions', - }), - ('TOGGLE_STAR_STATUS', { + }, + 'TOGGLE_STAR_STATUS': { 'keys': ['ctrl s', '*'], 'help_text': 'Add/remove star status of the current message', 'key_category': 'msg_actions', - }), - ('MSG_INFO', { + }, + 'MSG_INFO': { 'keys': ['i'], 'help_text': 'Show/hide message information', 'key_category': 'msg_actions', - }), - ('EDIT_HISTORY', { + }, + 'EDIT_HISTORY': { 'keys': ['e'], 'help_text': 'Show/hide edit history (from message information)', 'excluded_from_random_tips': True, 'key_category': 'msg_actions', - }), - ('VIEW_IN_BROWSER', { + }, + 'VIEW_IN_BROWSER': { 'keys': ['v'], 'help_text': 'View current message in browser (from message information)', 'excluded_from_random_tips': True, 'key_category': 'msg_actions', - }), - ('STREAM_INFO', { + }, + 'STREAM_INFO': { 'keys': ['i'], 'help_text': 'Show/hide stream information & modify settings', 'key_category': 'stream_list', - }), - ('STREAM_MEMBERS', { + }, + 'STREAM_MEMBERS': { 'keys': ['m'], 'help_text': 'Show/hide stream members (from stream information)', 'excluded_from_random_tips': True, 'key_category': 'stream_list', - }), - ('COPY_STREAM_EMAIL', { + }, + 'COPY_STREAM_EMAIL': { 'keys': ['c'], 'help_text': 'Copy stream email to clipboard (from stream information)', 'excluded_from_random_tips': True, 'key_category': 'stream_list', - }), - ('REDRAW', { + }, + 'REDRAW': { 'keys': ['ctrl l'], 'help_text': 'Redraw screen', 'key_category': 'general', - }), - ('QUIT', { + }, + 'QUIT': { 'keys': ['ctrl c'], 'help_text': 'Quit', 'key_category': 'general', - }), - ('USER_INFO', { + }, + 'USER_INFO': { 'keys': ['i'], 'help_text': 'View user information (From Users list)', 'key_category': 'general', - }), - ('BEGINNING_OF_LINE', { + }, + 'BEGINNING_OF_LINE': { 'keys': ['ctrl a'], 'help_text': 'Jump to the beginning of line', 'key_category': 'msg_compose', - }), - ('END_OF_LINE', { + }, + 'END_OF_LINE': { 'keys': ['ctrl e'], 'help_text': 'Jump to the end of line', 'key_category': 'msg_compose', - }), - ('ONE_WORD_BACKWARD', { + }, + 'ONE_WORD_BACKWARD': { 'keys': ['meta b'], 'help_text': 'Jump backward one word', 'key_category': 'msg_compose', - }), - ('ONE_WORD_FORWARD', { + }, + 'ONE_WORD_FORWARD': { 'keys': ['meta f'], 'help_text': 'Jump forward one word', 'key_category': 'msg_compose', - }), - ('DELETE_LAST_CHARACTER', { + }, + 'DELETE_LAST_CHARACTER': { 'keys': ['ctrl h'], 'help_text': 'Delete previous character (to left)', 'key_category': 'msg_compose', - }), - ('TRANSPOSE_CHARACTERS', { + }, + 'TRANSPOSE_CHARACTERS': { 'keys': ['ctrl t'], 'help_text': 'Transpose characters', 'key_category': 'msg_compose', - }), - ('CUT_TO_END_OF_LINE', { + }, + 'CUT_TO_END_OF_LINE': { 'keys': ['ctrl k'], 'help_text': 'Cut forwards to the end of the line', 'key_category': 'msg_compose', - }), - ('CUT_TO_START_OF_LINE', { + }, + 'CUT_TO_START_OF_LINE': { 'keys': ['ctrl u'], 'help_text': 'Cut backwards to the start of the line', 'key_category': 'msg_compose', - }), - ('CUT_TO_END_OF_WORD', { + }, + 'CUT_TO_END_OF_WORD': { 'keys': ['meta d'], 'help_text': 'Cut forwards to the end of the current word', 'key_category': 'msg_compose', - }), - ('CUT_TO_START_OF_WORD', { + }, + 'CUT_TO_START_OF_WORD': { 'keys': ['ctrl w'], 'help_text': 'Cut backwards to the start of the current word', 'key_category': 'msg_compose', - }), - ('PASTE_LAST_CUT', { + }, + 'PASTE_LAST_CUT': { 'keys': ['ctrl y'], 'help_text': 'Paste last cut section', 'key_category': 'msg_compose', - }), - ('UNDO_LAST_ACTION', { + }, + 'UNDO_LAST_ACTION': { 'keys': ['ctrl _'], 'help_text': 'Undo last action', 'key_category': 'msg_compose', - }), - ('PREV_LINE', { + }, + 'PREV_LINE': { 'keys': ['up', 'ctrl p'], 'help_text': 'Jump to the previous line', 'key_category': 'msg_compose', - }), - ('NEXT_LINE', { + }, + 'NEXT_LINE': { 'keys': ['down', 'ctrl n'], 'help_text': 'Jump to the next line', 'key_category': 'msg_compose', - }), - ('CLEAR_MESSAGE', { + }, + 'CLEAR_MESSAGE': { 'keys': ['ctrl l'], 'help_text': 'Clear compose box', 'key_category': 'msg_compose', - }), - ('FULL_RENDERED_MESSAGE', { + }, + 'FULL_RENDERED_MESSAGE': { 'keys': ['f'], 'help_text': 'Show/hide full rendered message (from message information)', 'key_category': 'msg_actions', - }), - ('FULL_RAW_MESSAGE', { + }, + 'FULL_RAW_MESSAGE': { 'keys': ['r'], 'help_text': 'Show/hide full raw message (from message information)', 'key_category': 'msg_actions', - }), -]) + }, +} # fmt: on -HELP_CATEGORIES = OrderedDict( - [ - ("general", "General"), - ("navigation", "Navigation"), - ("searching", "Searching"), - ("msg_actions", "Message actions"), - ("stream_list", "Stream list actions"), - ("msg_compose", "Composing a Message"), - ] -) +HELP_CATEGORIES = { + "general": "General", + "navigation": "Navigation", + "searching": "Searching", + "msg_actions": "Message actions", + "stream_list": "Stream list actions", + "msg_compose": "Composing a Message", +} ZT_TO_URWID_CMD_MAPPING = { "GO_UP": CURSOR_UP, From 7a163a7b594b02368cbf236b9b6bb99037284f5f Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Thu, 23 Mar 2023 18:39:25 +0000 Subject: [PATCH 015/276] refactor: core/views: Use Dict over OrderedDict for passing links. topic_links and message_links are passed as OrderedDicts to preserve insertion order to: - Controller.show_msg_info when rendering a MessageLinkButton in a MsgInfoView using MsgInfoView.create_link_buttons - Controller.show_full_rendered_message when rendering a FullRenderedMsgView - Controller.show_full_raw_message when rendering a FullRawMsgView - Controller.show_edit_history when rendering an EditHistoryView Since Dict also preserves insertion order from python3.7, we can safely pass topic_links and message_links as Dicts without any change in behavior. --- zulipterminal/core.py | 17 ++++++++--------- zulipterminal/ui_tools/views.py | 19 +++++++++---------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/zulipterminal/core.py b/zulipterminal/core.py index 656a7c9587..8ee293d8ee 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -8,7 +8,6 @@ import sys import time import webbrowser -from collections import OrderedDict from functools import partial from platform import platform from types import TracebackType @@ -278,8 +277,8 @@ def show_topic_edit_mode(self, button: Any) -> None: def show_msg_info( self, msg: Message, - topic_links: "OrderedDict[str, Tuple[str, int, bool]]", - message_links: "OrderedDict[str, Tuple[str, int, bool]]", + topic_links: Dict[str, Tuple[str, int, bool]], + message_links: Dict[str, Tuple[str, int, bool]], time_mentions: List[Tuple[str, str]], ) -> None: msg_info_view = MsgInfoView( @@ -339,8 +338,8 @@ def show_user_info(self, user_id: int) -> None: def show_full_rendered_message( self, message: Message, - topic_links: "OrderedDict[str, Tuple[str, int, bool]]", - message_links: "OrderedDict[str, Tuple[str, int, bool]]", + topic_links: Dict[str, Tuple[str, int, bool]], + message_links: Dict[str, Tuple[str, int, bool]], time_mentions: List[Tuple[str, str]], ) -> None: self.show_pop_up( @@ -358,8 +357,8 @@ def show_full_rendered_message( def show_full_raw_message( self, message: Message, - topic_links: "OrderedDict[str, Tuple[str, int, bool]]", - message_links: "OrderedDict[str, Tuple[str, int, bool]]", + topic_links: Dict[str, Tuple[str, int, bool]], + message_links: Dict[str, Tuple[str, int, bool]], time_mentions: List[Tuple[str, str]], ) -> None: self.show_pop_up( @@ -377,8 +376,8 @@ def show_full_raw_message( def show_edit_history( self, message: Message, - topic_links: "OrderedDict[str, Tuple[str, int, bool]]", - message_links: "OrderedDict[str, Tuple[str, int, bool]]", + topic_links: Dict[str, Tuple[str, int, bool]], + message_links: Dict[str, Tuple[str, int, bool]], time_mentions: List[Tuple[str, str]], ) -> None: self.show_pop_up( diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 8f6ad15de4..a4b673d6b9 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -3,7 +3,6 @@ """ import threading -from collections import OrderedDict from datetime import datetime from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union @@ -1508,8 +1507,8 @@ def __init__( controller: Any, msg: Message, title: str, - topic_links: "OrderedDict[str, Tuple[str, int, bool]]", - message_links: "OrderedDict[str, Tuple[str, int, bool]]", + topic_links: Dict[str, Tuple[str, int, bool]], + message_links: Dict[str, Tuple[str, int, bool]], time_mentions: List[Tuple[str, str]], ) -> None: self.msg = msg @@ -1626,7 +1625,7 @@ def __init__( @staticmethod def create_link_buttons( - controller: Any, links: "OrderedDict[str, Tuple[str, int, bool]]" + controller: Any, links: Dict[str, Tuple[str, int, bool]] ) -> Tuple[List[MessageLinkButton], int]: link_widgets = [] link_width = 0 @@ -1722,8 +1721,8 @@ def __init__( self, controller: Any, message: Message, - topic_links: "OrderedDict[str, Tuple[str, int, bool]]", - message_links: "OrderedDict[str, Tuple[str, int, bool]]", + topic_links: Dict[str, Tuple[str, int, bool]], + message_links: Dict[str, Tuple[str, int, bool]], time_mentions: List[Tuple[str, str]], title: str, ) -> None: @@ -1840,8 +1839,8 @@ def __init__( self, controller: Any, message: Message, - topic_links: "OrderedDict[str, Tuple[str, int, bool]]", - message_links: "OrderedDict[str, Tuple[str, int, bool]]", + topic_links: Dict[str, Tuple[str, int, bool]], + message_links: Dict[str, Tuple[str, int, bool]], time_mentions: List[Tuple[str, str]], title: str, ) -> None: @@ -1884,8 +1883,8 @@ def __init__( self, controller: Any, message: Message, - topic_links: "OrderedDict[str, Tuple[str, int, bool]]", - message_links: "OrderedDict[str, Tuple[str, int, bool]]", + topic_links: Dict[str, Tuple[str, int, bool]], + message_links: Dict[str, Tuple[str, int, bool]], time_mentions: List[Tuple[str, str]], title: str, ) -> None: From 8374a82a89d656a0227bb15bd6d9a0819d447638 Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Thu, 23 Mar 2023 23:31:33 +0000 Subject: [PATCH 016/276] refactor: helper: Change OrderedDict to Dict for (un)pinned matches. matches is initialized as an OrderedDict to ensure that helper.match_stream returns matched streams that are pinned before matched streams that are unpinned. Since Dict also preserves insertion order from python3.7, we can safely initialize matches as a Dict without any change in behavior. --- zulipterminal/helper.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index 3bb5554fd6..896e813077 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -5,7 +5,7 @@ import os import subprocess import time -from collections import OrderedDict, defaultdict +from collections import defaultdict from contextlib import contextmanager from functools import partial, wraps from itertools import chain, combinations @@ -572,14 +572,10 @@ def match_stream( for datum, stream_name in data ] - matches: "OrderedDict[str, DefaultDict[int, List[Tuple[DataT, str]]]]" = ( - OrderedDict( - [ - ("pinned", defaultdict(list)), - ("unpinned", defaultdict(list)), - ] - ) - ) + matches: Dict[str, DefaultDict[int, List[Tuple[DataT, str]]]] = { + "pinned": defaultdict(list), + "unpinned": defaultdict(list), + } for datum, splits in stream_splits: stream_name = splits[0] From bee9546e62f06890c9bdb56cd3feb07ea817e9bb Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Fri, 24 Mar 2023 12:55:54 +0000 Subject: [PATCH 017/276] refactor: model: Change OrderedDict to Dict for event handlers. event_actions is initialized as an OrderedDict in Model.__init__ to preserve its insertion order when registering event_types with the Zulip Client. Since Dict also preserves insertion order from python3.7, we can safely initialize event_actions as a Dict without any change in behavior. --- zulipterminal/model.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 25100fc8e0..f82556380f 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -4,7 +4,7 @@ import json import time -from collections import OrderedDict, defaultdict +from collections import defaultdict from concurrent.futures import Future, ThreadPoolExecutor, wait from copy import deepcopy from datetime import datetime @@ -141,24 +141,19 @@ def __init__(self, controller: Any) -> None: ] # Events desired with their corresponding callback - self.event_actions: "OrderedDict[str, Callable[[Event], None]]" = OrderedDict( - [ - ("message", self._handle_message_event), - ("update_message", self._handle_update_message_event), - ("reaction", self._handle_reaction_event), - ("subscription", self._handle_subscription_event), - ("typing", self._handle_typing_event), - ("update_message_flags", self._handle_update_message_flags_event), - ( - "update_global_notifications", - self._handle_update_global_notifications_event, - ), - ("update_display_settings", self._handle_update_display_settings_event), - ("user_settings", self._handle_user_settings_event), - ("realm_emoji", self._handle_update_emoji_event), - ("realm_user", self._handle_realm_user_event), - ] - ) + self.event_actions: Dict[str, Callable[[Event], None]] = { + "message": self._handle_message_event, + "update_message": self._handle_update_message_event, + "reaction": self._handle_reaction_event, + "subscription": self._handle_subscription_event, + "typing": self._handle_typing_event, + "update_message_flags": self._handle_update_message_flags_event, + "update_global_notifications": self._handle_update_global_notifications_event, # noqa: E501 + "update_display_settings": self._handle_update_display_settings_event, + "user_settings": self._handle_user_settings_event, + "realm_emoji": self._handle_update_emoji_event, + "realm_user": self._handle_realm_user_event, + } self.initial_data: Dict[str, Any] = {} From 91ed1e7f6e54b83ef9268b38ee4b675be98cbfaa Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Fri, 24 Mar 2023 13:38:27 +0000 Subject: [PATCH 018/276] refactor: boxes: Change OrderedDict to Dict for autocomplete mapping. autocomplete_map is initialized as an OrderedDict to preserve the insertion order of autocomplete prefixes when used in generic_autocomplete to perform autocompletion in a WriteBox. Since Dict also preserves insertion order from python3.7, we can safely initialize autocomplete_map as a Dict without any change in behavior. --- zulipterminal/ui_tools/boxes.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index 7fc848b95c..d4298288ca 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -4,7 +4,7 @@ import re import unicodedata -from collections import Counter, OrderedDict +from collections import Counter from datetime import datetime, timedelta from time import sleep from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple @@ -467,18 +467,16 @@ def _stream_box_autocomplete( return self._process_typeaheads(matched_streams[0], state, matched_streams[1]) def generic_autocomplete(self, text: str, state: Optional[int]) -> Optional[str]: - autocomplete_map = OrderedDict( - [ - ("@_", self.autocomplete_users), - ("@_**", self.autocomplete_users), - ("@", self.autocomplete_mentions), - ("@*", self.autocomplete_groups), - ("@**", self.autocomplete_users), - ("#", self.autocomplete_streams), - ("#**", self.autocomplete_streams), - (":", self.autocomplete_emojis), - ] - ) + autocomplete_map = { + "@_": self.autocomplete_users, + "@_**": self.autocomplete_users, + "@": self.autocomplete_mentions, + "@*": self.autocomplete_groups, + "@**": self.autocomplete_users, + "#": self.autocomplete_streams, + "#**": self.autocomplete_streams, + ":": self.autocomplete_emojis, + } # Look in a reverse order to find the last autocomplete prefix used in # the text. For instance, if text='@#example', use '#' as the prefix. @@ -673,7 +671,7 @@ def autocomplete_stream_and_topic( def validate_and_patch_autocomplete_stream_and_topic( self, text: str, - autocomplete_map: "OrderedDict[str, Callable[..., Any]]", + autocomplete_map: Dict[str, Callable[..., Any]], prefix_indices: Dict[str, int], ) -> str: """ From 7819dab5e7a5edb7e411b180774f004e4a4cd557 Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Fri, 24 Mar 2023 18:57:02 +0000 Subject: [PATCH 019/276] refactor: messages: Change OrderedDict to Dict in MessageBox. message_links and topic_links are declared as Ordered Dicts to preserve their insertion order when used in rendering a MessageBox. transform_content creates an empty OrderedDict and calls soup2markup to populate it with message links. footlinks_view then renders the message links in a MessageBox. Note this occurs after the popup changes, so the ordering should be preserved already; this is input to those views. Since Dict also preserves insertion order from python3.7, we can safely declare message_links and topic_links as Dicts without any change in behavior. Fixes #1330. --- zulipterminal/ui_tools/messages.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index 34f8cffbf7..80d20d2059 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -3,7 +3,7 @@ """ import typing -from collections import OrderedDict, defaultdict +from collections import defaultdict from datetime import date, datetime from time import time from typing import Any, Dict, List, NamedTuple, Optional, Tuple, Union @@ -55,8 +55,8 @@ def __init__(self, message: Message, model: "Model", last_message: Any) -> None: self.topic_name = "" self.email = "" # FIXME: Can we remove this? self.user_id: Optional[int] = None - self.message_links: "OrderedDict[str, Tuple[str, int, bool]]" = OrderedDict() - self.topic_links: "OrderedDict[str, Tuple[str, int, bool]]" = OrderedDict() + self.message_links: Dict[str, Tuple[str, int, bool]] = dict() + self.topic_links: Dict[str, Tuple[str, int, bool]] = dict() self.time_mentions: List[Tuple[str, str]] = list() self.last_message = last_message # if this is the first message @@ -295,11 +295,9 @@ def reactions_view(self, reactions: List[Dict[str, Any]]) -> Any: except Exception: return "" - # Use quotes as a workaround for OrderedDict typing issue. - # See https://github.com/python/mypy/issues/6904. @staticmethod def footlinks_view( - message_links: "OrderedDict[str, Tuple[str, int, bool]]", + message_links: Dict[str, Tuple[str, int, bool]], *, maximum_footlinks: int, padded: bool, @@ -357,9 +355,7 @@ def footlinks_view( @classmethod def soup2markup( cls, soup: Any, metadata: Dict[str, Any], **state: Any - ) -> Tuple[ - List[Any], "OrderedDict[str, Tuple[str, int, bool]]", List[Tuple[str, str]] - ]: + ) -> Tuple[List[Any], Dict[str, Tuple[str, int, bool]], List[Tuple[str, str]]]: # Ensure a string is provided, in case the soup finds none # This could occur if eg. an image is removed or not shown markup: List[Union[str, Tuple[Optional[str], Any]]] = [""] @@ -807,7 +803,7 @@ def transform_content( cls, content: Any, server_url: str ) -> Tuple[ Tuple[None, Any], - "OrderedDict[str, Tuple[str, int, bool]]", + Dict[str, Tuple[str, int, bool]], List[Tuple[str, str]], ]: soup = BeautifulSoup(content, "lxml") @@ -815,7 +811,7 @@ def transform_content( metadata = dict( server_url=server_url, - message_links=OrderedDict(), + message_links=dict(), time_mentions=list(), ) # type: Dict[str, Any] From de73694a6b182d57c805a949980b882b44de1348 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 18 Mar 2023 18:29:55 -0700 Subject: [PATCH 020/276] README: Add Section welcoming users & developers to chat on czo. This expands upon the existing welcoming text to discuss how one can interact with those on chat.zulip.org and the stream, and expectations of participants. In particular, this pulls out some community norms that are more applicable, and relates those to hotkeys in zulip-term. Feature status section adjusted to link to new section. --- README.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f9722224fe..f74c059025 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,9 @@ Current limitations which we expect to only resolve over the long term include s For queries on missing feature support please take a look at the [Frequently Asked Questions (FAQs)](https://github.com/zulip/zulip-terminal/blob/main/docs/FAQ.md), -our open [Issues](https://github.com/zulip/zulip-terminal/issues/), or sign up -on https://chat.zulip.org and chat with users and developers in the -[#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal) -stream! +our open [Issues](https://github.com/zulip/zulip-terminal/issues/), or +[chat with users & developers](#chat-with-fellow-users--developers) online at +the Zulip Community server! ### Supported platforms - Linux @@ -334,6 +333,63 @@ sudo apt-get install xsel No additional package is required to enable copying to clipboard. +## Chat with fellow users & developers! + +While Zulip Terminal is designed to work with any Zulip server, the main +contributors are present on the Zulip Community server at +https://chat.zulip.org, with most conversation in the +[**#zulip-terminal** stream](https://chat.zulip.org/#narrow/stream/206-zulip-terminal). + +You are welcome to view conversations in that stream using the link above, or +sign up for an account and chat with us - whether you are a user or developer! + +We aim to keep the Zulip community friendly, welcoming and productive, so if +participating, please respect our +[Community norms](https://zulip.com/development-community/#community-norms). + +### Notes more relevant to the **#zulip-terminal** stream + +These are a subset of the **Community norms** linked above, which are more +relevant to users of Zulip Terminal: those more likely to be in a text +environment, limited in character rows/columns, and present in this one smaller +stream. + +* **Prefer text in [code blocks](https://zulip.com/help/code-blocks), instead of screenshots** + + Zulip Terminal supports downloading images, but there is no guarantee that + users will be able to view them. + + *Try Meta+m to see example content formatting, including code blocks* + +* **Prefer [silent mentions](https://zulip.com/help/mention-a-user-or-group#silently-mention-a-user) + over regular mentions - or avoid mentions entirely** + + With Zulip's topics, the intended recipient can often already be clear. + Experienced members will be present as their time permits - responding + to messages when they return - and others may be able to assist before then. + + (Save [regular mentions](https://zulip.com/help/mention-a-user-or-group#mention-a-user-or-group_1) + for those who you do not expect to be present on a regular basis) + + *Try Ctrl+f/b to cycle through autocompletion in message content, after typing `@_` to specify a silent mention* + +* **Prefer trimming [quote and reply](https://zulip.com/help/quote-and-reply) + text to only the relevant parts of longer messages - or avoid quoting entirely** + + Zulip's topics often make it clear which message you're replying to. Long + messages can be more difficult to read with limited rows and columns of text, + but this is worsened if quoting an entire long message with extra content. + + *Try > to quote a selected message, deleting text as normal when composing a message* + +* **Prefer a [quick emoji reaction](https://zulip.com/help/emoji-reactions) +to show agreement instead of simple short messages** + + Reactions take up less space, including in Zulip Terminal, particularly + when multiple users wish to respond with the same sentiment. + + *Try + to toggle thumbs-up (+1) on a message, or use : to search for other reactions* + ## Contributor Guidelines Zulip Terminal is being built by the awesome [Zulip](https://zulip.com/team) community. From 99507e1fd280d906d2c5cc4e2c8289cd2b32b19d Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 10 Apr 2023 18:13:21 -0700 Subject: [PATCH 021/276] README: Update Zulip badge and link to new section. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f74c059025..1d0f5fde4b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [Recent changes](https://github.com/zulip/zulip-terminal/blob/main/CHANGELOG.md) | [Configuration](#Configuration) | [Hot Keys](https://github.com/zulip/zulip-terminal/blob/main/docs/hotkeys.md) | [FAQs](https://github.com/zulip/zulip-terminal/blob/main/docs/FAQ.md) | [Development](#contributor-guidelines) | [Tutorial](https://github.com/zulip/zulip-terminal/blob/main/docs/getting-started.md) -[![Zulip chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://chat.zulip.org/#narrow/stream/206-zulip-terminal) +[![Chat with us!](https://img.shields.io/badge/Zulip-chat_with_us!-brightgreen.svg)](https://github.com/zulip/zulip-terminal/blob/main/README.md#chat-with-fellow-users--developers) [![PyPI](https://img.shields.io/pypi/v/zulip-term.svg)](https://pypi.python.org/pypi/zulip-term) [![Python Versions](https://img.shields.io/pypi/pyversions/zulip-term.svg)](https://pypi.python.org/pypi/zulip-term) From 4edf5290fe28ccbc155227ff3373be8d174c7c15 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 4 Apr 2023 21:46:37 -0700 Subject: [PATCH 022/276] README: Add sub-headings and adjust ordering in Feature status section. This breaks up the large block of prose, grouping each under titles. --- README.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1d0f5fde4b..56048486d0 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,18 @@ Learn how to use Zulip Terminal with our We consider the client to already provide a fairly stable moderately-featureful everyday-user experience. +The current development focus is on improving aspects of everyday usage which +are more commonly used - to reduce the need for users to temporarily switch to +another client for a particular feature. + +Current limitations which we expect to only resolve over the long term include support for: +* All operations performed by users with extra privileges (owners/admins) +* Accessing and updating all settings +* Using a mouse/pointer to achieve all actions +* An internationalized UI + +#### Intentional differences + The terminal client currently has a number of intentional differences to the Zulip web client: - Additional and occasionally *different* [Hot keys](https://github.com/zulip/zulip-terminal/blob/main/docs/hotkeys.md) @@ -58,15 +70,7 @@ The terminal client currently has a number of intentional differences to the Zul - Content previewable in the web client, such as images, are also stored as footlinks -The current development focus is on improving aspects of everyday usage which -are more commonly used - to reduce the need for users to temporarily switch to -another client for a particular feature. - -Current limitations which we expect to only resolve over the long term include support for: -* All operations performed by users with extra privileges (owners/admins) -* Accessing and updating all settings -* Using a mouse/pointer to achieve all actions -* An internationalized UI +#### Feature queries? For queries on missing feature support please take a look at the [Frequently Asked Questions (FAQs)](https://github.com/zulip/zulip-terminal/blob/main/docs/FAQ.md), From 10e8c06056f01e572b5e51c7aa36730c41e7a381 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 4 Apr 2023 21:59:26 -0700 Subject: [PATCH 023/276] README/FAQ: Add Python implementation badge linking to new FAQ section. --- README.md | 1 + docs/FAQ.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/README.md b/README.md index 56048486d0..a5d4cf1762 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ [![Chat with us!](https://img.shields.io/badge/Zulip-chat_with_us!-brightgreen.svg)](https://github.com/zulip/zulip-terminal/blob/main/README.md#chat-with-fellow-users--developers) [![PyPI](https://img.shields.io/pypi/v/zulip-term.svg)](https://pypi.python.org/pypi/zulip-term) [![Python Versions](https://img.shields.io/pypi/pyversions/zulip-term.svg)](https://pypi.python.org/pypi/zulip-term) +[![Python Implementations](https://img.shields.io/pypi/implementation/zulip-term.svg)](https://github.com/zulip/zulip-terminal/blob/main/docs/FAQ.md#what-python-implementations-are-supported) [![GitHub Actions - Linting & tests](https://github.com/zulip/zulip-terminal/workflows/Linting%20%26%20tests/badge.svg?branch=main)](https://github.com/zulip/zulip-terminal/actions?query=workflow%3A%22Linting+%26+tests%22+branch%3Amain) [![Coverage status](https://img.shields.io/codecov/c/github/zulip/zulip-terminal/main.svg)](https://app.codecov.io/gh/zulip/zulip-terminal/branch/main) diff --git a/docs/FAQ.md b/docs/FAQ.md index b141e0ae26..3bac13ca67 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,5 +1,17 @@ # Frequently Asked Questions (FAQ) +## What Python implementations are supported? + +Users and developers run regularly with the "traditional" implementation of +Python (CPython). + +We also expect running with [PyPy](https://www.pypy.org) to be smooth based on +our automated testing, though it is worth noting we do not explicitly run the +application in these tests. + +Feedback on using these or any other implementations are welcome, such as those +[listed at python.org](https://www.python.org/download/alternatives/). + ## Colors appear mismatched, don't change with theme, or look strange Some terminal emulators support specifying custom colors, or custom color schemes. If you do this then this can override the colors that Zulip Terminal attempts to use. From e084e42080603235ff7bccb8c2bca4ca30f56357 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 4 Apr 2023 22:29:29 -0700 Subject: [PATCH 024/276] README/FAQ: Move Platform section to FAQ & link from new OS badge. Expand section to indicate: - Limited platform testing - Some features are not supported or need configuration per-OS - Windows not being supported natively (with issue ref) - Reference to Dockerfile documentation (and no builds distributed) --- README.md | 6 +----- docs/FAQ.md | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a5d4cf1762..14c314a7b0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![PyPI](https://img.shields.io/pypi/v/zulip-term.svg)](https://pypi.python.org/pypi/zulip-term) [![Python Versions](https://img.shields.io/pypi/pyversions/zulip-term.svg)](https://pypi.python.org/pypi/zulip-term) [![Python Implementations](https://img.shields.io/pypi/implementation/zulip-term.svg)](https://github.com/zulip/zulip-terminal/blob/main/docs/FAQ.md#what-python-implementations-are-supported) +[![OS Platforms](https://img.shields.io/static/v1?label=OS&message=Linux%20%7C%20WSL%20%7C%20macOS%20%7C%20Docker&color=blueviolet)](https://github.com/zulip/zulip-terminal/blob/main/docs/FAQ.md#what-operating-systems-are-supported) [![GitHub Actions - Linting & tests](https://github.com/zulip/zulip-terminal/workflows/Linting%20%26%20tests/badge.svg?branch=main)](https://github.com/zulip/zulip-terminal/actions?query=workflow%3A%22Linting+%26+tests%22+branch%3Amain) [![Coverage status](https://img.shields.io/codecov/c/github/zulip/zulip-terminal/main.svg)](https://app.codecov.io/gh/zulip/zulip-terminal/branch/main) @@ -79,11 +80,6 @@ our open [Issues](https://github.com/zulip/zulip-terminal/issues/), or [chat with users & developers](#chat-with-fellow-users--developers) online at the Zulip Community server! -### Supported platforms -- Linux -- OSX -- WSL (On Windows) - ### Supported Server Versions The minimum server version that Zulip Terminal supports is diff --git a/docs/FAQ.md b/docs/FAQ.md index 3bac13ca67..57fe2cc7dc 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -12,6 +12,28 @@ application in these tests. Feedback on using these or any other implementations are welcome, such as those [listed at python.org](https://www.python.org/download/alternatives/). +## What operating systems are supported? + +We expect everything to work smoothly on the following operating systems: +* Linux +* macOS +* WSL (Windows Subsystem for Linux) + +These are covered in our automatic test suite, though it is assumed that Python +insulates us from excessive variations between distributions and versions, +including when using WSL. + +Note that some features are not supported or require extra configuration, +depending on the platform - see +[Configuration](https://github.com/zulip/zulip-terminal/blob/main/README.md#configuration). + +> NOTE: Windows is **not** natively supported right now, see +> [#357](https://github.com/zulip/zulip-terminal/issues/357). + +`Dockerfile`s have also been contributed, though we don't currently distribute +pre-built versions of these to install - see the [Docker +documentation](https://github.com/zulip/zulip-terminal/blob/main/docker/). + ## Colors appear mismatched, don't change with theme, or look strange Some terminal emulators support specifying custom colors, or custom color schemes. If you do this then this can override the colors that Zulip Terminal attempts to use. From ddb7d44d735cab4ecff938d40a2834da4bdf2763 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 7 Apr 2023 15:12:47 -0700 Subject: [PATCH 025/276] README/FAQ: Move Python versions section into FAQ & link from badge. The badge previously linked to PyPI, but this is already linked from the PyPI badge itself. --- README.md | 22 +--------------------- docs/FAQ.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 14c314a7b0..17e19196de 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Chat with us!](https://img.shields.io/badge/Zulip-chat_with_us!-brightgreen.svg)](https://github.com/zulip/zulip-terminal/blob/main/README.md#chat-with-fellow-users--developers) [![PyPI](https://img.shields.io/pypi/v/zulip-term.svg)](https://pypi.python.org/pypi/zulip-term) -[![Python Versions](https://img.shields.io/pypi/pyversions/zulip-term.svg)](https://pypi.python.org/pypi/zulip-term) +[![Python Versions](https://img.shields.io/pypi/pyversions/zulip-term.svg)](https://github.com/zulip/zulip-terminal/blob/main/docs/FAQ.md#what-python-versions-are-supported) [![Python Implementations](https://img.shields.io/pypi/implementation/zulip-term.svg)](https://github.com/zulip/zulip-terminal/blob/main/docs/FAQ.md#what-python-implementations-are-supported) [![OS Platforms](https://img.shields.io/static/v1?label=OS&message=Linux%20%7C%20WSL%20%7C%20macOS%20%7C%20Docker&color=blueviolet)](https://github.com/zulip/zulip-terminal/blob/main/docs/FAQ.md#what-operating-systems-are-supported) @@ -86,26 +86,6 @@ The minimum server version that Zulip Terminal supports is [`2.1.0`](https://zulip.readthedocs.io/en/latest/overview/changelog.html#zulip-2-1-x-series). It may still work with earlier versions. -### Supported Python Versions - -Version 0.7.0 was the last release with support for Python 3.6. - -Version 0.6.0 was the last release with support for Python 3.5. - -Later releases and the main development branch are currently tested (on Ubuntu) -with: -- CPython 3.7-3.11 -- PyPy 3.7-3.9 - -Since our automated testing does not cover interactive testing of the UI, there -may be issues with some Python versions, though generally we have not found -this to be the case. - -Please note that generally we limit each release to between a lower and upper -Python version, so it is possible that for example if you have a newer version -of Python installed, then some releases (or `main`) may not install correctly. -In some cases this can give rise to the symptoms in issue #1145. - ## Installation We recommend installing in a dedicated python virtual environment (see below) diff --git a/docs/FAQ.md b/docs/FAQ.md index 57fe2cc7dc..70641045e7 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -12,6 +12,26 @@ application in these tests. Feedback on using these or any other implementations are welcome, such as those [listed at python.org](https://www.python.org/download/alternatives/). +## What Python versions are supported? + +Current practice is to pin minimum and maximum python version support. + +As a result: +- the current `main` branch in git supports Python 3.7 to 3.11 +- Release 0.7.0 was the last release to support Python 3.6 (maximum 3.10) +- Release 0.6.0 was the last release to support Python 3.5 (maximum 3.9) + +Note the minimum versions of Python are close to or in the unsupported range at +this time. + +The next release will include support for Python 3.11; before then we suggest +installing the +[latest (git) version](https://github.com/zulip/zulip-terminal/blob/main/README.md#installation). + +> NOTE: If you attempt to install on a system using a version of Python which +> pip cannot match, it may install a very old version - see +> [issue 1145](https://github.com/zulip/zulip-terminal/issues/1145). + ## What operating systems are supported? We expect everything to work smoothly on the following operating systems: From a8d34b19aa5564be0bbac2075bf22d57bb9ebcdf Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 7 Apr 2023 15:53:49 -0700 Subject: [PATCH 026/276] README/FAQ: Move supported Zulip server versions section to FAQ. Text is slightly extended to indicate newer features may be supported, and how the server version may be determined from within zulip terminal. --- README.md | 6 ------ docs/FAQ.md | 12 ++++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 17e19196de..4fe7a1d1eb 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,6 @@ our open [Issues](https://github.com/zulip/zulip-terminal/issues/), or [chat with users & developers](#chat-with-fellow-users--developers) online at the Zulip Community server! -### Supported Server Versions - -The minimum server version that Zulip Terminal supports is -[`2.1.0`](https://zulip.readthedocs.io/en/latest/overview/changelog.html#zulip-2-1-x-series). -It may still work with earlier versions. - ## Installation We recommend installing in a dedicated python virtual environment (see below) diff --git a/docs/FAQ.md b/docs/FAQ.md index 70641045e7..aba479b6fe 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -54,6 +54,18 @@ depending on the platform - see pre-built versions of these to install - see the [Docker documentation](https://github.com/zulip/zulip-terminal/blob/main/docker/). +## What versions of Zulip are supported? + +For the features that we support, we expect Zulip server versions as far +back as 2.1.0 to be usable. + +> NOTE: You can check your server version by pressing +> meta? in the application. + +Note that a subset of features in more recent Zulip versions are supported, and +could in some cases be present when using this client, particularly if the +feature relies upon a client-side implementation. + ## Colors appear mismatched, don't change with theme, or look strange Some terminal emulators support specifying custom colors, or custom color schemes. If you do this then this can override the colors that Zulip Terminal attempts to use. From 27a00efb95ed02b19890276126a809122186211a Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 12 Apr 2023 13:33:36 -0700 Subject: [PATCH 027/276] ui_mappings: Add UserStatus Literal for internal status strings. This enables the STATE_ICON lookup to be typed more cleanly. --- zulipterminal/config/ui_mappings.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zulipterminal/config/ui_mappings.py b/zulipterminal/config/ui_mappings.py index 22d6092dca..39d63d471d 100644 --- a/zulipterminal/config/ui_mappings.py +++ b/zulipterminal/config/ui_mappings.py @@ -25,8 +25,10 @@ } +UserStatus = Literal["active", "idle", "offline", "inactive"] + # Mapping that binds user activity status to corresponding markers. -STATE_ICON = { +STATE_ICON: Dict[UserStatus, str] = { "active": STATUS_ACTIVE, "idle": STATUS_IDLE, "offline": STATUS_OFFLINE, From aa68fd690ab16d702d1d11d9223ccbf8a7849466 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 9 Apr 2023 22:41:05 -0700 Subject: [PATCH 028/276] refactor: model/ui_mappings: Simplify construction of user_list. This significantly reduces code duplication. Note added in ui_mappings that the order is relevant in the UI. --- zulipterminal/config/ui_mappings.py | 1 + zulipterminal/model.py | 50 +++++++++++++---------------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/zulipterminal/config/ui_mappings.py b/zulipterminal/config/ui_mappings.py index 39d63d471d..512eb3a115 100644 --- a/zulipterminal/config/ui_mappings.py +++ b/zulipterminal/config/ui_mappings.py @@ -28,6 +28,7 @@ UserStatus = Literal["active", "idle", "offline", "inactive"] # Mapping that binds user activity status to corresponding markers. +# NOTE: Ordering of keys affects display order STATE_ICON: Dict[UserStatus, str] = { "active": STATUS_ACTIVE, "idle": STATUS_IDLE, diff --git a/zulipterminal/model.py b/zulipterminal/model.py index f82556380f..f78ef4d00b 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -2,6 +2,7 @@ Defines the `Model`, fetching and storing data retrieved from the Zulip server """ +import itertools import json import time from collections import defaultdict @@ -46,6 +47,7 @@ from zulipterminal.config.ui_mappings import ( EDIT_TOPIC_POLICY, ROLE_BY_ID, + STATE_ICON, StreamAccessType, ) from zulipterminal.helper import ( @@ -1100,35 +1102,27 @@ def get_all_users(self) -> List[Dict[str, Any]]: self._all_users_by_id[bot["user_id"]] = bot self.user_id_email_dict[bot["user_id"]] = email - # Generate filtered lists for active & idle users - active = [ - properties - for properties in self.user_dict.values() - if properties["status"] == "active" - ] - idle = [ - properties - for properties in self.user_dict.values() - if properties["status"] == "idle" - ] - offline = [ - properties - for properties in self.user_dict.values() - if properties["status"] == "offline" - ] - inactive = [ - properties - for properties in self.user_dict.values() - if properties["status"] == "inactive" - ] + # Generate filtered lists for each status + ordered_statuses = list(STATE_ICON.keys()) + presences_by_status = { + status: sorted( + [ + properties + for properties in self.user_dict.values() + if properties["status"] == status + ], + key=lambda user: user["full_name"].casefold(), + ) + for status in ordered_statuses + } + user_list = list( + itertools.chain.from_iterable( + presences_by_status[status] for status in ordered_statuses + ) + ) + user_list.insert(0, current_user) # Add current user to the top of the list - # Construct user_list from sorted components of each list - user_list = sorted(active, key=lambda u: u["full_name"].casefold()) - user_list += sorted(idle, key=lambda u: u["full_name"].casefold()) - user_list += sorted(offline, key=lambda u: u["full_name"].casefold()) - user_list += sorted(inactive, key=lambda u: u["full_name"].casefold()) - # Add current user to the top of the list - user_list.insert(0, current_user) + # NOTE: Do this after generating user_list to avoid current_user duplication self.user_dict[current_user["email"]] = current_user self.user_id_email_dict[self.user_id] = current_user["email"] From 85bf4a131d7b1e48249d64f1f5d0089c4deb1391 Mon Sep 17 00:00:00 2001 From: Progyan Date: Tue, 5 Apr 2022 23:52:06 +0530 Subject: [PATCH 029/276] symbols/ui_mappings: Add bot icon and map to bot status. Bots do not have a dynamic status, so give them a static one. --- zulipterminal/config/symbols.py | 1 + zulipterminal/config/ui_mappings.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/zulipterminal/config/symbols.py b/zulipterminal/config/symbols.py index 4870d9ca7a..2b49e8e429 100644 --- a/zulipterminal/config/symbols.py +++ b/zulipterminal/config/symbols.py @@ -23,5 +23,6 @@ STATUS_IDLE = "◒" STATUS_OFFLINE = "○" STATUS_INACTIVE = "•" +BOT_MARKER = "♟" AUTOHIDE_TAB_LEFT_ARROW = "❰" AUTOHIDE_TAB_RIGHT_ARROW = "❱" diff --git a/zulipterminal/config/ui_mappings.py b/zulipterminal/config/ui_mappings.py index 512eb3a115..8f94c5f3d6 100644 --- a/zulipterminal/config/ui_mappings.py +++ b/zulipterminal/config/ui_mappings.py @@ -8,6 +8,7 @@ from zulipterminal.api_types import EditPropagateMode from zulipterminal.config.symbols import ( + BOT_MARKER, STATUS_ACTIVE, STATUS_IDLE, STATUS_INACTIVE, @@ -25,7 +26,7 @@ } -UserStatus = Literal["active", "idle", "offline", "inactive"] +UserStatus = Literal["active", "idle", "offline", "inactive", "bot"] # Mapping that binds user activity status to corresponding markers. # NOTE: Ordering of keys affects display order @@ -34,6 +35,7 @@ "idle": STATUS_IDLE, "offline": STATUS_OFFLINE, "inactive": STATUS_INACTIVE, + "bot": BOT_MARKER, } From 09fe2b7bbd9d05aa89db1e5b3cad07ade6c4522d Mon Sep 17 00:00:00 2001 From: Progyan Date: Sun, 7 Aug 2022 00:19:46 +0530 Subject: [PATCH 030/276] themes: Add user_bot styles for bot 'status' in UI. These are constructed dynamically based on the user status, so if a user may have a "bot" status, a "user_bot" style is now necessary. --- zulipterminal/config/themes.py | 1 + zulipterminal/themes/gruvbox_dark.py | 1 + zulipterminal/themes/gruvbox_light.py | 1 + zulipterminal/themes/zt_blue.py | 1 + zulipterminal/themes/zt_dark.py | 1 + zulipterminal/themes/zt_light.py | 1 + 6 files changed, 6 insertions(+) diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py index 5872572adb..3d497294ca 100644 --- a/zulipterminal/config/themes.py +++ b/zulipterminal/config/themes.py @@ -35,6 +35,7 @@ 'user_idle' : '', 'user_offline' : '', 'user_inactive' : '', + 'user_bot' : '', 'title' : 'bold', 'column_title' : 'bold', 'time' : '', diff --git a/zulipterminal/themes/gruvbox_dark.py b/zulipterminal/themes/gruvbox_dark.py index 5e0c232521..1abd77906e 100644 --- a/zulipterminal/themes/gruvbox_dark.py +++ b/zulipterminal/themes/gruvbox_dark.py @@ -30,6 +30,7 @@ 'user_idle' : (Color.NEUTRAL_YELLOW, Color.DARK0_HARD), 'user_offline' : (Color.LIGHT2, Color.DARK0_HARD), 'user_inactive' : (Color.LIGHT2, Color.DARK0_HARD), + 'user_bot' : (Color.LIGHT2, Color.DARK0_HARD), 'title' : (Color.LIGHT2__BOLD, Color.DARK0_HARD), 'column_title' : (Color.LIGHT2__BOLD, Color.DARK0_HARD), 'time' : (Color.BRIGHT_BLUE, Color.DARK0_HARD), diff --git a/zulipterminal/themes/gruvbox_light.py b/zulipterminal/themes/gruvbox_light.py index 7c536de97a..e477a9f086 100644 --- a/zulipterminal/themes/gruvbox_light.py +++ b/zulipterminal/themes/gruvbox_light.py @@ -29,6 +29,7 @@ 'user_idle' : (Color.NEUTRAL_YELLOW, Color.LIGHT0_HARD), 'user_offline' : (Color.DARK2, Color.LIGHT0_HARD), 'user_inactive' : (Color.DARK2, Color.LIGHT0_HARD), + 'user_bot' : (Color.DARK2, Color.LIGHT0_HARD), 'title' : (Color.DARK2__BOLD, Color.LIGHT0_HARD), 'column_title' : (Color.DARK2__BOLD, Color.LIGHT0_HARD), 'time' : (Color.FADED_BLUE, Color.LIGHT0_HARD), diff --git a/zulipterminal/themes/zt_blue.py b/zulipterminal/themes/zt_blue.py index d6fed8b0cc..eb99c8dc0a 100644 --- a/zulipterminal/themes/zt_blue.py +++ b/zulipterminal/themes/zt_blue.py @@ -24,6 +24,7 @@ 'user_idle' : (Color.DARK_GRAY, Color.LIGHT_BLUE), 'user_offline' : (Color.BLACK, Color.LIGHT_BLUE), 'user_inactive' : (Color.BLACK, Color.LIGHT_BLUE), + 'user_bot' : (Color.BLACK, Color.LIGHT_BLUE), 'title' : (Color.WHITE__BOLD, Color.DARK_BLUE), 'column_title' : (Color.BLACK__BOLD, Color.LIGHT_BLUE), 'time' : (Color.DARK_BLUE, Color.LIGHT_BLUE), diff --git a/zulipterminal/themes/zt_dark.py b/zulipterminal/themes/zt_dark.py index 36791644ee..69a5f4ad75 100644 --- a/zulipterminal/themes/zt_dark.py +++ b/zulipterminal/themes/zt_dark.py @@ -24,6 +24,7 @@ 'user_idle' : (Color.YELLOW, Color.BLACK), 'user_offline' : (Color.WHITE, Color.BLACK), 'user_inactive' : (Color.WHITE, Color.BLACK), + 'user_bot' : (Color.WHITE, Color.BLACK), 'title' : (Color.WHITE__BOLD, Color.BLACK), 'column_title' : (Color.WHITE__BOLD, Color.BLACK), 'time' : (Color.LIGHT_BLUE, Color.BLACK), diff --git a/zulipterminal/themes/zt_light.py b/zulipterminal/themes/zt_light.py index 1ca6b94547..6b0ee5709a 100644 --- a/zulipterminal/themes/zt_light.py +++ b/zulipterminal/themes/zt_light.py @@ -24,6 +24,7 @@ 'user_idle' : (Color.DARK_BLUE, Color.WHITE), 'user_offline' : (Color.BLACK, Color.WHITE), 'user_inactive' : (Color.BLACK, Color.WHITE), + 'user_bot' : (Color.BLACK, Color.WHITE), 'title' : (Color.WHITE__BOLD, Color.DARK_GRAY), 'column_title' : (Color.BLACK__BOLD, Color.WHITE), 'time' : (Color.DARK_BLUE, Color.WHITE), From 74bcb11f49a5088af464cc2ec14a77d573c7e314 Mon Sep 17 00:00:00 2001 From: Progyan Date: Tue, 5 Apr 2022 23:53:06 +0530 Subject: [PATCH 031/276] model: Set status of bots to be a static "bot" value. This will be picked up using earlier commits, to show it differently in the UI. With a different status, bots are now explicitly placed at the end of the user list. Tests updated. Original logic by Progyan, simplified using logic suggested in review by neiljp in #1187. Co-authored-by: neiljp (Neil Pilgrim) --- tests/conftest.py | 26 +++++++++++++------------- zulipterminal/model.py | 8 ++++++-- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c4dc1c6532..07e8313da2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1021,25 +1021,25 @@ def user_dict(logged_on_user: Dict[str, Any]) -> Dict[str, Dict[str, Any]]: "emailgateway@zulip.com": { "email": "emailgateway@zulip.com", "full_name": "Email Gateway", - "status": "inactive", + "status": "bot", "user_id": 6, }, "feedback@zulip.com": { "email": "feedback@zulip.com", "full_name": "Zulip Feedback Bot", - "status": "inactive", + "status": "bot", "user_id": 1, }, "notification-bot@zulip.com": { "email": "notification-bot@zulip.com", "full_name": "Notification Bot", - "status": "inactive", + "status": "bot", "user_id": 5, }, "welcome-bot@zulip.com": { "email": "welcome-bot@zulip.com", "full_name": "Welcome Bot", - "status": "inactive", + "status": "bot", "user_id": 4, }, } @@ -1075,12 +1075,6 @@ def user_list(logged_on_user: Dict[str, Any]) -> List[Dict[str, Any]]: "status": "active", "user_id": logged_on_user["user_id"], }, - { - "email": "emailgateway@zulip.com", - "full_name": "Email Gateway", - "status": "inactive", - "user_id": 6, - }, { "full_name": "Human 1", "email": "person1@example.com", @@ -1105,22 +1099,28 @@ def user_list(logged_on_user: Dict[str, Any]) -> List[Dict[str, Any]]: "user_id": 14, "status": "inactive", }, + { + "email": "emailgateway@zulip.com", + "full_name": "Email Gateway", + "status": "bot", + "user_id": 6, + }, { "email": "notification-bot@zulip.com", "full_name": "Notification Bot", - "status": "inactive", + "status": "bot", "user_id": 5, }, { "email": "welcome-bot@zulip.com", "full_name": "Welcome Bot", - "status": "inactive", + "status": "bot", "user_id": 4, }, { "email": "feedback@zulip.com", "full_name": "Zulip Feedback Bot", - "status": "inactive", + "status": "bot", "user_id": 1, }, ] diff --git a/zulipterminal/model.py b/zulipterminal/model.py index f78ef4d00b..999be50cc4 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -1035,7 +1035,10 @@ def get_all_users(self) -> List[Dict[str, Any]]: } continue email = user["email"] - if email in presences: # presences currently subset of all users + if user["is_bot"]: + # Bot has no dynamic status, so avoid presence lookup + status = "bot" + elif email in presences: # presences currently subset of all users """ * Aggregate our information on a user's presence across their * clients. @@ -1086,6 +1089,7 @@ def get_all_users(self) -> List[Dict[str, Any]]: "user_id": user["user_id"], "status": status, } + self._all_users_by_id[user["user_id"]] = user self.user_id_email_dict[user["user_id"]] = email @@ -1096,7 +1100,7 @@ def get_all_users(self) -> List[Dict[str, Any]]: "full_name": bot["full_name"], "email": email, "user_id": bot["user_id"], - "status": "inactive", + "status": "bot", } self._cross_realm_bots_by_id[bot["user_id"]] = bot self._all_users_by_id[bot["user_id"]] = bot From efb18d314fd17817b545712c2435d71eb5cdabca Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 9 Apr 2023 17:20:21 -0700 Subject: [PATCH 032/276] refactor: README: Reformat Contributor section; split lines & sentences. This does not modify content, only shortens lines to make diffs easier to read. Visually compared to previous version as rendered on GitHub to ensure no changes. --- README.md | 185 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 139 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 4fe7a1d1eb..fefb59e6db 100644 --- a/README.md +++ b/README.md @@ -369,52 +369,82 @@ to show agreement instead of simple short messages** Zulip Terminal is being built by the awesome [Zulip](https://zulip.com/team) community. -To be a part of it and to contribute to the code, feel free to work on any [issue](https://github.com/zulip/zulip-terminal/issues) or propose your idea on +To be a part of it and to contribute to the code, feel free to work on any +[issue](https://github.com/zulip/zulip-terminal/issues) or propose your idea on [#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal). -For commit structure and style, please review the [Commit Style](#commit-style) section below. +For commit structure and style, please review the [Commit Style](#commit-style) +section below. -If you are new to `git` (or not!), you may benefit from the [Zulip git guide](http://zulip.readthedocs.io/en/latest/git/index.html). -When contributing, it's important to note that we use a **rebase-oriented workflow**. +If you are new to `git` (or not!), you may benefit from the +[Zulip git guide](http://zulip.readthedocs.io/en/latest/git/index.html). +When contributing, it's important to note that we use a **rebase-oriented +workflow**. -A simple [tutorial](https://github.com/zulip/zulip-terminal/blob/main/docs/developer-feature-tutorial.md) is available for implementing the `typing` indicator. +A simple +[tutorial](https://github.com/zulip/zulip-terminal/blob/main/docs/developer-feature-tutorial.md) +is available for implementing the `typing` indicator. Follow it to understand how to implement a new feature for zulip-terminal. -You can of course browse the source on GitHub & in the source tree you download, and check the [source file overview](https://github.com/zulip/zulip-terminal/docs/developer-file-overview.md) for ideas of how files are currently arranged. +You can of course browse the source on GitHub & in the source tree you +download, and check the +[source file overview](https://github.com/zulip/zulip-terminal/docs/developer-file-overview.md) +for ideas of how files are currently arranged. ### Urwid -Zulip Terminal uses [urwid](http://urwid.org/) to render the UI components in terminal. Urwid is an awesome library through which you can render a decent terminal UI just using python. [Urwid's Tutorial](http://urwid.org/tutorial/index.html) is a great place to start for new contributors. +Zulip Terminal uses [urwid](http://urwid.org/) to render the UI components in +terminal. +Urwid is an awesome library through which you can render a decent terminal UI +just using python. +[Urwid's Tutorial](http://urwid.org/tutorial/index.html) is a great place to +start for new contributors. ### Getting Zulip Terminal code and connecting it to upstream -First, fork the `zulip/zulip-terminal` repository on GitHub ([see how](https://docs.github.com/en/get-started/quickstart/fork-a-repo)) and then clone your forked repository locally, replacing **YOUR_USERNAME** with your GitHub username: +First, fork the `zulip/zulip-terminal` repository on GitHub +([see how](https://docs.github.com/en/get-started/quickstart/fork-a-repo)) +and then clone your forked repository locally, replacing **YOUR_USERNAME** with +your GitHub username: + ``` $ git clone --config pull.rebase git@github.com:YOUR_USERNAME/zulip-terminal.git ``` -This should create a new directory for the repository in the current directory, so enter the repository directory with `cd zulip-terminal` and configure and fetch the upstream remote repository for your cloned fork of Zulip Terminal: +This should create a new directory for the repository in the current directory, +so enter the repository directory with `cd zulip-terminal` and configure and +fetch the upstream remote repository for your cloned fork of Zulip Terminal: + ``` $ git remote add -f upstream https://github.com/zulip/zulip-terminal.git ``` -For detailed explanation on the commands used for cloning and setting upstream, refer to Step 1 of the [Get Zulip Code](https://zulip.readthedocs.io/en/latest/git/cloning.html) section of Zulip's Git guide. +For detailed explanation on the commands used for cloning and setting upstream, +refer to Step 1 of the +[Get Zulip Code](https://zulip.readthedocs.io/en/latest/git/cloning.html) +section of Zulip's Git guide. ### Setting up a development environment -Various options are available; we are exploring the benefits of each and would appreciate feedback on which you use or feel works best. +Various options are available; we are exploring the benefits of each and would +appreciate feedback on which you use or feel works best. -Note that the tools used in each case are typically the same, but are called in different ways. +Note that the tools used in each case are typically the same, but are called in +different ways. -The following commands should be run in the repository directory, created by a process similar to that in the previous section. +The following commands should be run in the repository directory, created by a +process similar to that in the previous section. #### Pipenv -1. Install pipenv (see the [recommended installation notes](https://pipenv.readthedocs.io/en/latest/install/#pragmatic-installation-of-pipenv); pipenv can be installed in a virtual environment, if you wish) +1. Install pipenv + (see the [recommended installation notes](https://pipenv.readthedocs.io/en/latest/install/#pragmatic-installation-of-pipenv); + pipenv can be installed in a virtual environment, if you wish) ``` $ pip3 install --user pipenv ``` -2. Initialize the pipenv virtual environment for zulip-term (using the default python 3; use eg. `--python 3.6` to be more specific) +2. Initialize the pipenv virtual environment for zulip-term (using the default + python 3; use eg. `--python 3.6` to be more specific) ``` $ pipenv --three @@ -429,7 +459,8 @@ $ pipenv run pip3 install -e '.[dev]' #### Pip -1. Manually create & activate a virtual environment; any method should work, such as that used in the above simple installation +1. Manually create & activate a virtual environment; any method should work, + such as that used in the above simple installation 1. `python3 -m venv zt_venv` (creates a venv named `zt_venv` in the current directory) 2. `source zt_venv/bin/activate` (activates the venv; this assumes a bash-like shell) @@ -459,7 +490,9 @@ Once you have a development environment set up, you might find the following use | Run all tests | `pytest` | `pipenv run pytest` | | Build test coverage report | `pytest --cov-report html:cov_html --cov=./` | `pipenv run pytest --cov-report html:cov_html --cov=./` | -If using make with pip, running `make` will ensure the development environment is up to date with the specified dependencies, useful after fetching from git and rebasing. +If using make with pip, running `make` will ensure the development environment +is up to date with the specified dependencies, useful after fetching from git +and rebasing. #### Passing linters and automated tests @@ -506,7 +539,8 @@ Currently we have The project uses `black` and `isort` for code-style and import sorting respectively. -These tools can be run as linters locally , but can also *automatically* format your code for you. +These tools can be run as linters locally , but can also *automatically* format +your code for you. If you're using a `make`-based setup, running `make fix` will run both (and a few other tools) and reformat the current state of your code - so you'll want @@ -589,9 +623,11 @@ respect reviewers' time. #### Commit Message Style -We aim to follow a standard commit style to keep the `git log` consistent and easy to read. +We aim to follow a standard commit style to keep the `git log` consistent and +easy to read. -Much like working with code, we suggest you refer to recent commits in the git log, for examples of the style we're actively using. +Much like working with code, we suggest you refer to recent commits in the git +log, for examples of the style we're actively using. Our overall style for commit messages broadly follows the general guidelines given for @@ -618,64 +654,121 @@ Some example commit titles: (ideally more descriptive in practice!) * `requirements: Upgrade some-dependency from ==9.2 to ==9.3.` - upgrade a dependency from version ==9.2 to version ==9.3, in the central dependencies file (*not* some file requirements.*) -To aid in satisfying some of these rules you can use `GitLint`, as described in the following section. +To aid in satisfying some of these rules you can use `GitLint`, as described in +the following section. -**However**, please check your commits manually versus these style rules, since GitLint cannot check everything - including language or grammar! +**However**, please check your commits manually versus these style rules, since +GitLint cannot check everything - including language or grammar! #### GitLint -If you plan to submit git commits in pull-requests (PRs), then we highly suggest installing the `gitlint` commit-message hook by running `gitlint install-hook` (or `pipenv run gitlint install-hook` with pipenv setups). While the content still depends upon your writing skills, this ensures a more consistent formatting structure between commits, including by different authors. +If you plan to submit git commits in pull-requests (PRs), then we highly +suggest installing the `gitlint` commit-message hook by running `gitlint +install-hook` (or `pipenv run gitlint install-hook` with pipenv setups). +While the content still depends upon your writing skills, this ensures a more +consistent formatting structure between commits, including by different +authors. -If the hook is installed as described above, then after completing the text for a commit, it will be checked by gitlint against the style we have set up, and will offer advice if there are any issues it notices. If gitlint finds any, it will ask if you wish to commit with the message as it is (`y` for 'yes'), stop the commit process (`n` for 'no'), or edit the commit message (`e` for 'edit'). +If the hook is installed as described above, then after completing the text for +a commit, it will be checked by gitlint against the style we have set up, and +will offer advice if there are any issues it notices. +If gitlint finds any, it will ask if you wish to commit with the message as it +is (`y` for 'yes'), stop the commit process (`n` for 'no'), or edit the commit +message (`e` for 'edit'). -Other gitlint options are available; for example it is possible to apply it to a range of commits with the `--commits` option, eg. `gitlint --commits HEAD~2..HEAD` would apply it to the last few commits. +Other gitlint options are available; for example it is possible to apply it to +a range of commits with the `--commits` option, eg. `gitlint --commits +HEAD~2..HEAD` would apply it to the last few commits. ### Tips for working with tests (pytest) -Tests for zulip-terminal are written using [pytest](https://pytest.org/). You can read the tests in the `/tests` folder to learn about writing tests for a new class/function. If you are new to pytest, reading its documentation is definitely recommended. - -We currently have thousands of tests which get checked upon running `pytest`. While it is dependent on your system capability, this should typically take less than one minute to run. However, during debugging you may still wish to limit the scope of your tests, to improve the turnaround time: -* If lots of tests are failing in a very verbose way, you might try the `-x` option (eg. `pytest -x`) to stop tests after the first failure; due to parametrization of tests and test fixtures, many apparent errors/failures can be resolved with just one fix! (try eg. `pytest --maxfail 3` for a less-strict version of this) -* To avoid running all the successful tests each time, along with the failures, you can run with `--lf` (eg. `pytest --lf`), short for `--last-failed` (similar useful options may be `--failed-first` and `--new-first`, which may work well with `-x`) -* Since pytest 3.10 there is `--sw` (`--stepwise`), which works through known failures in the same way as `--lf` and `-x` can be used, which can be combined with `--stepwise-skip` to control which test is the current focus -* If you know the names of tests which are failing and/or in a specific location, you might limit tests to a particular location (eg. `pytest tests/model`) or use a selected keyword (eg. `pytest -k __handle`) - -When only a subset of tests are running it becomes more practical and useful to use the `-v` option (`--verbose`); instead of showing a `.` (or `F`, `E`, `x`, etc) for each test result, it gives the name (with parameters) of each test being run (eg. `pytest -v -k __handle`). This option also shows more detail in tests and can be given multiple times (eg. `pytest -vv`). - -For additional help with pytest options see `pytest -h`, or check out the [full pytest documentation](https://docs.pytest.org/en/latest/). +Tests for zulip-terminal are written using [pytest](https://pytest.org/). +You can read the tests in the `/tests` folder to learn about writing tests for +a new class/function. +If you are new to pytest, reading its documentation is definitely recommended. + +We currently have thousands of tests which get checked upon running `pytest`. +While it is dependent on your system capability, this should typically take +less than one minute to run. +However, during debugging you may still wish to limit the scope of your tests, +to improve the turnaround time: + +* If lots of tests are failing in a very verbose way, you might try the `-x` + option (eg. `pytest -x`) to stop tests after the first failure; due to +parametrization of tests and test fixtures, many apparent errors/failures can +be resolved with just one fix! (try eg. `pytest --maxfail 3` for a less-strict +version of this) + +* To avoid running all the successful tests each time, along with the failures, + you can run with `--lf` (eg. `pytest --lf`), short for `--last-failed` +(similar useful options may be `--failed-first` and `--new-first`, which may +work well with `-x`) + +* Since pytest 3.10 there is `--sw` (`--stepwise`), which works through known + failures in the same way as `--lf` and `-x` can be used, which can be +combined with `--stepwise-skip` to control which test is the current focus + +* If you know the names of tests which are failing and/or in a specific + location, you might limit tests to a particular location (eg. `pytest +tests/model`) or use a selected keyword (eg. `pytest -k __handle`) + +When only a subset of tests are running it becomes more practical and useful to +use the `-v` option (`--verbose`); instead of showing a `.` (or `F`, `E`, `x`, +etc) for each test result, it gives the name (with parameters) of each test +being run (eg. `pytest -v -k __handle`). +This option also shows more detail in tests and can be given multiple times +(eg. `pytest -vv`). + +For additional help with pytest options see `pytest -h`, or check out the [full +pytest documentation](https://docs.pytest.org/en/latest/). ### Debugging Tips #### Output using `print` -The stdout (standard output) for zulip-terminal is redirected to `./debug.log` if debugging is enabled at run-time using `-d` or `--debug`. +The stdout (standard output) for zulip-terminal is redirected to `./debug.log` +if debugging is enabled at run-time using `-d` or `--debug`. -This means that if you want to check the value of a variable, or perhaps indicate reaching a certain point in the code, you can simply use `print()`, eg. +This means that if you want to check the value of a variable, or perhaps +indicate reaching a certain point in the code, you can simply use `print()`, +eg. ```python3 print(f"Just about to do something with {variable}") ``` -and when running with a debugging option, the string will be printed to `./debug.log`. +and when running with a debugging option, the string will be printed to +`./debug.log`. -With a bash-like terminal, you can run something like `tail -f debug.log` in another terminal, to see the output from `print` as it happens. +With a bash-like terminal, you can run something like `tail -f debug.log` in +another terminal, to see the output from `print` as it happens. #### Interactive debugging using pudb & telnet -If you want to debug zulip-terminal while it is running, or in a specific state, you can insert +If you want to debug zulip-terminal while it is running, or in a specific +state, you can insert ```python3 from pudb.remote import set_trace set_trace() ``` -in the part of the code you want to debug. This will start a telnet connection for you. You can find the IP address and +in the part of the code you want to debug. +This will start a telnet connection for you. You can find the IP address and port of the telnet connection in `./debug.log`. Then simply run ``` $ telnet 127.0.0.1 6899 ``` -in another terminal, where `127.0.0.1` is the IP address and `6899` is port you find in `./debug.log`. +in another terminal, where `127.0.0.1` is the IP address and `6899` is port you +find in `./debug.log`. #### There's no effect in Zulip Terminal after making local changes! -This likely means that you have installed both normal and development versions of zulip-terminal. +This likely means that you have installed both normal and development versions +of zulip-terminal. To ensure you run the development version: -* If using pipenv, call `pipenv run zulip-term` from the cloned/downloaded `zulip-terminal` directory; -* If using pip (pip3), ensure you have activated the correct virtual environment (venv); depending on how your shell is configured, the name of the venv may appear in the command prompt. Note that not including the `-e` in the pip3 command will also cause this problem. +* If using pipenv, call `pipenv run zulip-term` from the cloned/downloaded + `zulip-terminal` directory; + +* If using pip (pip3), ensure you have activated the correct virtual + environment (venv); depending on how your shell is configured, the name of +the venv may appear in the command prompt. +Note that not including the `-e` in the pip3 command will also cause this +problem. From 59d70b01dab369417a257f14736d27c9878029ab Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 9 Apr 2023 17:47:03 -0700 Subject: [PATCH 033/276] README: Fix link to developer-file-overview.md in Contributing section. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fefb59e6db..bb9ae4bf8f 100644 --- a/README.md +++ b/README.md @@ -388,7 +388,7 @@ Follow it to understand how to implement a new feature for zulip-terminal. You can of course browse the source on GitHub & in the source tree you download, and check the -[source file overview](https://github.com/zulip/zulip-terminal/docs/developer-file-overview.md) +[source file overview](https://github.com/zulip/zulip-terminal/blob/main/docs/developer-file-overview.md) for ideas of how files are currently arranged. ### Urwid From bba2e48364afb02ba5bd4d815dc7b7b652e7fed4 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 13 Apr 2023 14:23:00 -0700 Subject: [PATCH 034/276] README: Fix link to install pipenv in Contributing section. Thanks to Vishwesh Pillai for discovering this. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bb9ae4bf8f..b6bcf14965 100644 --- a/README.md +++ b/README.md @@ -438,7 +438,7 @@ process similar to that in the previous section. #### Pipenv 1. Install pipenv - (see the [recommended installation notes](https://pipenv.readthedocs.io/en/latest/install/#pragmatic-installation-of-pipenv); + (see the [recommended installation notes](https://pipenv.readthedocs.io/en/latest/installation); pipenv can be installed in a virtual environment, if you wish) ``` $ pip3 install --user pipenv From 71af9d7478e77d970138c225e97be9fd4ea495e4 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 9 Apr 2023 17:32:17 -0700 Subject: [PATCH 035/276] minor: README: Contributor section spacing adjustments. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b6bcf14965..2556752e6b 100644 --- a/README.md +++ b/README.md @@ -539,7 +539,7 @@ Currently we have The project uses `black` and `isort` for code-style and import sorting respectively. -These tools can be run as linters locally , but can also *automatically* format +These tools can be run as linters locally, but can also *automatically* format your code for you. If you're using a `make`-based setup, running `make fix` will run both (and a @@ -665,6 +665,7 @@ GitLint cannot check everything - including language or grammar! If you plan to submit git commits in pull-requests (PRs), then we highly suggest installing the `gitlint` commit-message hook by running `gitlint install-hook` (or `pipenv run gitlint install-hook` with pipenv setups). + While the content still depends upon your writing skills, this ensures a more consistent formatting structure between commits, including by different authors. From 1bc123694255826a8ecde2c93ab1be05caf4ec1b Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 13 Apr 2023 14:09:09 -0700 Subject: [PATCH 036/276] FAQ: Update Python version section regarding very recent versions. The note relating to #1145 is retained, but testing in CI indicates a more specific error is given if there is no matching python for a user's environment - now that versions 0.2.0 and 0.2.1 have been yanked from PyPI. Instead of a broken install of a very old version, a recent pip should error with a message along the lines of `ERROR: Package 'zulip-term' requires a different Python: 3.11.3 not in <3.11,>=3.7'` --- docs/FAQ.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index aba479b6fe..e2988f9cf0 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -28,8 +28,11 @@ The next release will include support for Python 3.11; before then we suggest installing the [latest (git) version](https://github.com/zulip/zulip-terminal/blob/main/README.md#installation). -> NOTE: If you attempt to install on a system using a version of Python which -> pip cannot match, it may install a very old version - see +> Until Mid-April 2023 it is possible that installing with a very recent +> version of Python could lead to an apparently broken installation with a very +> old version of zulip-terminal. +> This should no longer be the case, now that these older versions have been +> yanked from PyPI, but could give symptoms resembling > [issue 1145](https://github.com/zulip/zulip-terminal/issues/1145). ## What operating systems are supported? From f420528f13ce37b4dc5b5939b10f9f11d1a39c10 Mon Sep 17 00:00:00 2001 From: Subhasish-Behera Date: Fri, 24 Mar 2023 08:17:13 +0530 Subject: [PATCH 037/276] refactor: model/helper/messages: Migrate pm_with to pm-with in narrows. The latter form has officially been the narrow format, but the server was previously looser in also accepting the underscore version - though that was subsequently reverted. For details see (on chat.zulip.org): - #zulip-terminal > Cannot narrow to `pm_with` (#T1352) - #api design > direct message search operators Tests adapted. Commit text updated and commits squashed together by neiljp (all changes are necessary together to keep ZT functioning) --- tests/core/test_core.py | 6 ++--- tests/helper/test_helper.py | 10 ++++----- tests/model/test_model.py | 36 +++++++++++++++--------------- tests/ui_tools/test_messages.py | 6 ++--- zulipterminal/helper.py | 4 ++-- zulipterminal/model.py | 10 ++++----- zulipterminal/ui_tools/messages.py | 4 ++-- 7 files changed, 38 insertions(+), 38 deletions(-) diff --git a/tests/core/test_core.py b/tests/core/test_core.py index 93f005c905..17517d77f7 100644 --- a/tests/core/test_core.py +++ b/tests/core/test_core.py @@ -228,7 +228,7 @@ def test_narrow_to_user( controller.narrow_to_user(recipient_emails=emails) - assert controller.model.narrow == [["pm_with", user_email]] + assert controller.model.narrow == [["pm-with", user_email]] controller.view.message_view.log.clear.assert_called_once_with() recipients = frozenset([controller.model.user_id, user_id]) assert controller.model.recipients == recipients @@ -474,8 +474,8 @@ def test_stream_muting_confirmation_popup( ([["search", "BOO"]], [["search", "FOO"]]), ([["stream", "PTEST"]], [["stream", "PTEST"], ["search", "FOO"]]), ( - [["pm_with", "foo@zulip.com"], ["search", "BOO"]], - [["pm_with", "foo@zulip.com"], ["search", "FOO"]], + [["pm-with", "foo@zulip.com"], ["search", "BOO"]], + [["pm-with", "foo@zulip.com"], ["search", "FOO"]], ), ( [["stream", "PTEST"], ["topic", "RDS"]], diff --git a/tests/helper/test_helper.py b/tests/helper/test_helper.py index e32b9963fb..3c23343d6d 100644 --- a/tests/helper/test_helper.py +++ b/tests/helper/test_helper.py @@ -79,7 +79,7 @@ def test_index_messages_narrow_user( messages = messages_successful_response["messages"] model = mocker.patch(MODEL + ".__init__", return_value=None) model.index = initial_index - model.narrow = [["pm_with", "boo@zulip.com"]] + model.narrow = [["pm-with", "boo@zulip.com"]] model.is_search_narrow.return_value = False model.user_id = 5140 model.user_dict = { @@ -99,7 +99,7 @@ def test_index_messages_narrow_user_multiple( messages = messages_successful_response["messages"] model = mocker.patch(MODEL + ".__init__", return_value=None) model.index = initial_index - model.narrow = [["pm_with", "boo@zulip.com, bar@zulip.com"]] + model.narrow = [["pm-with", "boo@zulip.com, bar@zulip.com"]] model.is_search_narrow.return_value = False model.user_id = 5140 model.user_dict = { @@ -348,13 +348,13 @@ def test_display_error_if_present( ), case( {"type": "private", "to": [4, 5], "content": "Hi"}, - [["pm_with", "welcome-bot@zulip.com, notification-bot@zulip.com"]], + [["pm-with", "welcome-bot@zulip.com, notification-bot@zulip.com"]], False, id="group_private_conv__same_group_pm__not_notified", ), case( {"type": "private", "to": [4, 5], "content": "Hi"}, - [["pm_with", "welcome-bot@zulip.com"]], + [["pm-with", "welcome-bot@zulip.com"]], True, id="private_conv__other_pm__notified", ), @@ -362,7 +362,7 @@ def test_display_error_if_present( {"type": "private", "to": [4], "content": ":party_parrot:"}, [ [ - "pm_with", + "pm-with", "person1@example.com, person2@example.com, " "welcome-bot@zulip.com", ] diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 04a553afcd..7d858b8bea 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -337,8 +337,8 @@ def test_normalize_and_cache_message_retention_text( [], [["stream", "hello world"]], [["stream", "hello world"], ["topic", "what's it all about?"]], - [["pm_with", "FOO@zulip.com"]], - [["pm_with", "Foo@zulip.com, Bar@zulip.com"]], + [["pm-with", "FOO@zulip.com"]], + [["pm-with", "Foo@zulip.com, Bar@zulip.com"]], [["is", "private"]], [["is", "starred"]], ], @@ -355,8 +355,8 @@ def test_get_focus_in_current_narrow_individually(self, model, msg_id, narrow): [], [["stream", "hello world"]], [["stream", "hello world"], ["topic", "what's it all about?"]], - [["pm_with", "FOO@zulip.com"]], - [["pm_with", "Foo@zulip.com, Bar@zulip.com"]], + [["pm-with", "FOO@zulip.com"]], + [["pm-with", "Foo@zulip.com, Bar@zulip.com"]], [["is", "private"]], [["is", "starred"]], ], @@ -414,7 +414,7 @@ def test_set_narrow_bad_input(self, model, bad_args): ([["is", "starred"]], dict(starred=True)), ([["is", "mentioned"]], dict(mentioned=True)), ([["is", "private"]], dict(pms=True)), - ([["pm_with", "FOO@zulip.com"]], dict(pm_with="FOO@zulip.com")), + ([["pm-with", "FOO@zulip.com"]], dict(pm_with="FOO@zulip.com")), ], ) def test_set_narrow_already_set(self, model, narrow, good_args): @@ -435,7 +435,7 @@ def test_set_narrow_already_set(self, model, narrow, good_args): ([], [["is", "starred"]], dict(starred=True)), ([], [["is", "mentioned"]], dict(mentioned=True)), ([], [["is", "private"]], dict(pms=True)), - ([], [["pm_with", "FOOBOO@gmail.com"]], dict(pm_with="FOOBOO@gmail.com")), + ([], [["pm-with", "FOOBOO@gmail.com"]], dict(pm_with="FOOBOO@gmail.com")), ], ) def test_set_narrow_not_already_set( @@ -466,12 +466,12 @@ def test_set_narrow_not_already_set( ), ([["is", "private"]], {"private_msg_ids": {0, 1}}, {0, 1}), ( - [["pm_with", "FOO@zulip.com"]], + [["pm-with", "FOO@zulip.com"]], {"private_msg_ids_by_user_ids": {frozenset({1, 2}): {0, 1}}}, {0, 1}, ), ( - [["pm_with", "FOO@zulip.com"]], + [["pm-with", "FOO@zulip.com"]], { # Covers recipient empty-set case "private_msg_ids_by_user_ids": { frozenset({1, 3}): {0, 1} # NOTE {1,3} not {1,2} @@ -1848,7 +1848,7 @@ def test__handle_message_event_with_flags(self, mocker, model, message_fixture): "id": 1, "display_recipient": [{"id": 5827}, {"id": 5}], }, - [["pm_with", "notification-bot@zulip.com"]], + [["pm-with", "notification-bot@zulip.com"]], frozenset({5827, 5}), ["msg_w"], id="user_pm_x_appears_in_narrow_with_x", @@ -1866,7 +1866,7 @@ def test__handle_message_event_with_flags(self, mocker, model, message_fixture): "id": 1, "display_recipient": [{"id": 5827}, {"id": 3212}], }, - [["pm_with", "notification-bot@zulip.com"]], + [["pm-with", "notification-bot@zulip.com"]], frozenset({5827, 5}), [], id="user_pm_x_does_not_appear_in_narrow_without_x", @@ -2904,7 +2904,7 @@ def test_toggle_stream_visual_notifications( id="not_in_pm_narrow", ), case( - [["pm_with", "othello@zulip.com"]], + [["pm-with", "othello@zulip.com"]], { "op": "start", "sender": {"user_id": 4, "email": "hamlet@zulip.com"}, @@ -2920,7 +2920,7 @@ def test_toggle_stream_visual_notifications( id="not_in_pm_narrow_with_sender", ), case( - [["pm_with", "hamlet@zulip.com"]], + [["pm-with", "hamlet@zulip.com"]], { "op": "start", "sender": {"user_id": 4, "email": "hamlet@zulip.com"}, @@ -2936,7 +2936,7 @@ def test_toggle_stream_visual_notifications( id="in_pm_narrow_with_sender_typing:start", ), case( - [["pm_with", "hamlet@zulip.com"]], + [["pm-with", "hamlet@zulip.com"]], { "op": "start", "sender": {"user_id": 4, "email": "hamlet@zulip.com"}, @@ -2952,7 +2952,7 @@ def test_toggle_stream_visual_notifications( id="in_pm_narrow_with_sender_typing:start_while_animation_in_progress", ), case( - [["pm_with", "hamlet@zulip.com"]], + [["pm-with", "hamlet@zulip.com"]], { "op": "stop", "sender": {"user_id": 4, "email": "hamlet@zulip.com"}, @@ -2968,7 +2968,7 @@ def test_toggle_stream_visual_notifications( id="in_pm_narrow_with_sender_typing:stop", ), case( - [["pm_with", "hamlet@zulip.com"]], + [["pm-with", "hamlet@zulip.com"]], { "op": "start", "sender": {"user_id": 5, "email": "iago@zulip.com"}, @@ -2984,7 +2984,7 @@ def test_toggle_stream_visual_notifications( id="in_pm_narrow_with_other_myself_typing:start", ), case( - [["pm_with", "hamlet@zulip.com"]], + [["pm-with", "hamlet@zulip.com"]], { "op": "stop", "sender": {"user_id": 5, "email": "iago@zulip.com"}, @@ -3000,7 +3000,7 @@ def test_toggle_stream_visual_notifications( id="in_pm_narrow_with_other_myself_typing:stop", ), case( - [["pm_with", "iago@zulip.com"]], + [["pm-with", "iago@zulip.com"]], { "op": "start", "sender": {"user_id": 5, "email": "iago@zulip.com"}, @@ -3013,7 +3013,7 @@ def test_toggle_stream_visual_notifications( id="in_pm_narrow_with_oneself:start", ), case( - [["pm_with", "iago@zulip.com"]], + [["pm-with", "iago@zulip.com"]], { "op": "stop", "sender": {"user_id": 5, "email": "iago@zulip.com"}, diff --git a/tests/ui_tools/test_messages.py b/tests/ui_tools/test_messages.py index e0bbe18d33..78d9d60fb0 100644 --- a/tests/ui_tools/test_messages.py +++ b/tests/ui_tools/test_messages.py @@ -911,13 +911,13 @@ def test_main_view_generates_PM_header( ([["is", "private"]], 1, "You and ", "All direct messages"), ([["is", "private"]], 2, "You and ", "All direct messages"), ( - [["pm_with", "boo@zulip.com"]], + [["pm-with", "boo@zulip.com"]], 1, "You and ", "Direct message conversation", ), ( - [["pm_with", "boo@zulip.com, bar@zulip.com"]], + [["pm-with", "boo@zulip.com, bar@zulip.com"]], 2, "You and ", "Group direct message conversation", @@ -1129,7 +1129,7 @@ def test_update_message_author_status( ([["is", "starred"]], False), ([["is", "mentioned"]], False), ([["is", "private"]], False), - ([["pm_with", "notification-bot@zulip.com"]], False), + ([["pm-with", "notification-bot@zulip.com"]], False), ], ids=[ "all_messages_narrow", diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index 896e813077..c9817fc701 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -423,7 +423,7 @@ def index_messages(messages: List[Message], model: Any, index: Index) -> Index: {recipient["id"] for recipient in msg["display_recipient"]} ) - if narrow[0][0] == "pm_with": + if narrow[0][0] == "pm-with": narrow_emails = [ model.user_dict[email]["user_id"] for email in narrow[0][1].split(", ") @@ -678,7 +678,7 @@ def notify_if_message_sent_outside_narrow( recipient_emails = [ controller.model.user_id_email_dict[user_id] for user_id in message["to"] ] - pm_with_narrow = [["pm_with", ", ".join(recipient_emails)]] + pm_with_narrow = [["pm-with", ", ".join(recipient_emails)]] check_narrow_and_notify(pm_narrow, pm_with_narrow, controller) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 999be50cc4..dfc144d745 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -300,7 +300,7 @@ def set_narrow( frozenset(["stream"]): [["stream", stream]], frozenset(["stream", "topic"]): [["stream", stream], ["topic", topic]], frozenset(["pms"]): [["is", "private"]], - frozenset(["pm_with"]): [["pm_with", pm_with]], + frozenset(["pm_with"]): [["pm-with", pm_with]], frozenset(["starred"]): [["is", "starred"]], frozenset(["mentioned"]): [["is", "mentioned"]], } @@ -314,7 +314,7 @@ def set_narrow( if new_narrow != self.narrow: self.narrow = new_narrow - if pm_with is not None and new_narrow[0][0] == "pm_with": + if pm_with is not None and new_narrow[0][0] == "pm-with": users = pm_with.split(", ") self.recipients = frozenset( [self.user_dict[user]["user_id"] for user in users] + [self.user_id] @@ -360,7 +360,7 @@ def get_message_ids_in_current_narrow(self) -> Set[int]: ids = index["topic_msg_ids"][stream_id].get(topic, set()) elif narrow[0][1] == "private": ids = index["private_msg_ids"] - elif narrow[0][0] == "pm_with": + elif narrow[0][0] == "pm-with": recipients = self.recipients ids = index["private_msg_ids_by_user_ids"].get(recipients, set()) elif narrow[0][1] == "starred": @@ -400,7 +400,7 @@ def current_narrow_contains_message(self, message: Message) -> bool: ) # PM-with or ( - self.narrow[0][0] == "pm_with" + self.narrow[0][0] == "pm-with" and message["type"] == "private" and len(self.narrow) == 1 and self.recipients @@ -1380,7 +1380,7 @@ def _handle_typing_event(self, event: Event) -> None: # and the person typing isn't the user themselves if ( len(narrow) == 1 - and narrow[0][0] == "pm_with" + and narrow[0][0] == "pm-with" and sender_email in narrow[0][1].split(",") and sender_id != self.user_id ): diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index 80d20d2059..1911dc5dd6 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -111,7 +111,7 @@ def __init__(self, message: Message, model: "Model", last_message: Any) -> None: def need_recipient_header(self) -> bool: # Prevent redundant information in recipient bar - if len(self.model.narrow) == 1 and self.model.narrow[0][0] == "pm_with": + if len(self.model.narrow) == 1 and self.model.narrow[0][0] == "pm-with": return False if len(self.model.narrow) == 2 and self.model.narrow[1][0] == "topic": return False @@ -923,7 +923,7 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: elif is_command_key("TOGGLE_NARROW", key): self.model.unset_search_narrow() if self.message["type"] == "private": - if len(self.model.narrow) == 1 and self.model.narrow[0][0] == "pm_with": + if len(self.model.narrow) == 1 and self.model.narrow[0][0] == "pm-with": self.model.controller.narrow_to_all_pm( contextual_message_id=self.message["id"], ) From aba94e326fde4b6249cde24640add790642cb423 Mon Sep 17 00:00:00 2001 From: AmeliaTaihui Date: Fri, 7 Apr 2023 20:36:48 -0400 Subject: [PATCH 038/276] helper: Match topic search text against start of all words in topic. This extends the existing behavior which only matched against the start of the topic, and applies in entering the topic in the compose box as well as linking to streams and topics in message content. A similar approach is taken as with streams, splitting on spaces as well as a set of additional delimiters (-_/). The topics fixture is extended to include text split by delimiters. Test cases updated and extended. Fixes #1358. --- tests/conftest.py | 9 ++++++- tests/ui_tools/test_boxes.py | 51 ++++++++++++++++++++++++++++++------ zulipterminal/helper.py | 17 +++++++++--- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 07e8313da2..ffafd3580e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -587,7 +587,14 @@ def message_history(request: Any) -> List[Dict[str, Any]]: @pytest.fixture def topics() -> List[str]: - return ["Topic 1", "This is a topic", "Hello there!"] + return [ + "Topic 1", + "This is a topic", + "Hello there!", + "He-llo there!", + "Hello t/here!", + "Hello from out-er_space!", + ] @pytest.fixture( diff --git a/tests/ui_tools/test_boxes.py b/tests/ui_tools/test_boxes.py index d9fb8b10a2..975b14c416 100644 --- a/tests/ui_tools/test_boxes.py +++ b/tests/ui_tools/test_boxes.py @@ -96,22 +96,39 @@ def test_not_calling_typing_method_without_recipients( [ ("#**Stream 1>T", 0, True, "#**Stream 1>Topic 1**"), ("#**Stream 1>T", 1, True, "#**Stream 1>This is a topic**"), - ("#**Stream 1>T", 2, True, None), - ("#**Stream 1>T", -1, True, "#**Stream 1>This is a topic**"), - ("#**Stream 1>T", -2, True, "#**Stream 1>Topic 1**"), - ("#**Stream 1>T", -3, True, None), + ("#**Stream 1>T", 2, True, "#**Stream 1>Hello there!**"), + ("#**Stream 1>T", 3, True, "#**Stream 1>He-llo there!**"), + ("#**Stream 1>T", 4, True, "#**Stream 1>Hello t/here!**"), + ("#**Stream 1>T", 5, True, None), + ("#**Stream 1>T", -1, True, "#**Stream 1>Hello t/here!**"), + ("#**Stream 1>T", -2, True, "#**Stream 1>He-llo there!**"), + ("#**Stream 1>T", -3, True, "#**Stream 1>Hello there!**"), + ("#**Stream 1>T", -4, True, "#**Stream 1>This is a topic**"), + ("#**Stream 1>T", -5, True, "#**Stream 1>Topic 1**"), + ("#**Stream 1>T", -6, True, None), ("#**Stream 1>To", 0, True, "#**Stream 1>Topic 1**"), ("#**Stream 1>H", 0, True, "#**Stream 1>Hello there!**"), ("#**Stream 1>Hello ", 0, True, "#**Stream 1>Hello there!**"), ("#**Stream 1>", 0, True, "#**Stream 1>Topic 1**"), ("#**Stream 1>", 1, True, "#**Stream 1>This is a topic**"), - ("#**Stream 1>", -1, True, "#**Stream 1>Hello there!**"), - ("#**Stream 1>", -2, True, "#**Stream 1>This is a topic**"), + ("#**Stream 1>", 2, True, "#**Stream 1>Hello there!**"), + ("#**Stream 1>", 3, True, "#**Stream 1>He-llo there!**"), + ("#**Stream 1>", 4, True, "#**Stream 1>Hello t/here!**"), + ("#**Stream 1>", 5, True, "#**Stream 1>Hello from out-er_space!**"), + ("#**Stream 1>", 6, True, None), + ("#**Stream 1>", -1, True, "#**Stream 1>Hello from out-er_space!**"), + ("#**Stream 1>", -2, True, "#**Stream 1>Hello t/here!**"), + ("#**Stream 1>", -3, True, "#**Stream 1>He-llo there!**"), + ("#**Stream 1>", -4, True, "#**Stream 1>Hello there!**"), + ("#**Stream 1>", -5, True, "#**Stream 1>This is a topic**"), + ("#**Stream 1>", -6, True, "#**Stream 1>Topic 1**"), + ("#**Stream 1>", -7, True, None), # Fenced prefix ("#**Stream 1**>T", 0, True, "#**Stream 1>Topic 1**"), # Unfenced prefix ("#Stream 1>T", 0, True, "#**Stream 1>Topic 1**"), ("#Stream 1>T", 1, True, "#**Stream 1>This is a topic**"), + ("#Stream 1>T", 2, True, "#**Stream 1>Hello there!**"), # Invalid stream ("#**invalid stream>", 0, False, None), ("#**invalid stream**>", 0, False, None), @@ -1306,12 +1323,30 @@ def test__stream_box_autocomplete_with_spaces( @pytest.mark.parametrize( "text, matching_topics", [ - ("", ["Topic 1", "This is a topic", "Hello there!"]), - ("Th", ["This is a topic"]), + ( + "", + [ + "Topic 1", + "This is a topic", + "Hello there!", + "He-llo there!", + "Hello t/here!", + "Hello from out-er_space!", + ], + ), + ("Th", ["This is a topic", "Hello there!", "He-llo there!"]), + ("ll", ["He-llo there!"]), + ("her", ["Hello t/here!"]), + ("er", ["Hello from out-er_space!"]), + ("spa", ["Hello from out-er_space!"]), ], ids=[ "no_search_text", "single_word_search_text", + "split_in_first_word", + "split_in_second_word", + "first_split_in_third_word", + "second_split_in_third_word", ], ) def test__topic_box_autocomplete( diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index c9817fc701..12f402b5e6 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -535,9 +535,20 @@ def match_emoji(emoji: str, text: str) -> bool: def match_topics(topic_names: List[str], search_text: str) -> List[str]: - return [ - name for name in topic_names if name.lower().startswith(search_text.lower()) - ] + matching_topics = [] + delimiters = "-_/" + trans = str.maketrans(delimiters, len(delimiters) * " ") + for full_topic_name in topic_names: + # "abc def-gh" --> ["abc def gh", "def", "gh"] + words_to_be_matched = [full_topic_name] + full_topic_name.translate( + trans + ).split()[1:] + + for word in words_to_be_matched: + if word.lower().startswith(search_text.lower()): + matching_topics.append(full_topic_name) + break + return matching_topics DataT = TypeVar("DataT") From a89cb8adcb9d023f7ee24b148672537293dba02d Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 8 Apr 2023 19:26:57 -0700 Subject: [PATCH 039/276] refactor: api_types: Set message types as only being private or stream. This field could previously be an arbitrary `str`, though in practice `Message`s come from the server and `Composition`s are sent from ZT, so only the latter directly benefits from matching strings to these limited values. DirectMessageString is used here, though other naming has yet to migrate towards "direct" rather than "private". Fixture updated to use MessageType instead of str. --- tests/conftest.py | 4 ++-- zulipterminal/api_types.py | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ffafd3580e..8ab80ac94e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ from pytest_mock import MockerFixture from urwid import Widget -from zulipterminal.api_types import Message +from zulipterminal.api_types import Message, MessageType from zulipterminal.config.keys import ( ZT_TO_URWID_CMD_MAPPING, keys_for_command, @@ -416,7 +416,7 @@ def display_recipient_factory( def msg_template_factory( msg_id: int, - msg_type: str, + msg_type: MessageType, timestamp: int, *, subject: str = "", diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 4fdc812845..a52f51896d 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -22,19 +22,28 @@ TYPING_STARTED_WAIT_PERIOD = 10 TYPING_STOPPED_WAIT_PERIOD = 5 +############################################################################### +# Core message types (used in Composition and Message below) + +DirectMessageString = Literal["private"] +StreamMessageString = Literal["stream"] + +MessageType = Union[DirectMessageString, StreamMessageString] + + ############################################################################### # Parameter to pass in request to: # https://zulip.com/api/send-message class PrivateComposition(TypedDict): - type: Literal["private"] + type: DirectMessageString content: str to: List[int] # User ids class StreamComposition(TypedDict): - type: Literal["stream"] + type: StreamMessageString content: str to: str # stream name # TODO: Migrate to using int (stream id) subject: str # TODO: Migrate to using topic @@ -80,6 +89,8 @@ class StreamMessageUpdateRequest(TypedDict): # https://zulip.com/api/get-events#message # https://zulip.com/api/get-message (unused) +## TODO: Improve this typing to split private and stream message data + class Message(TypedDict, total=False): id: int @@ -101,7 +112,7 @@ class Message(TypedDict, total=False): sender_email: str sender_realm_str: str display_recipient: Any - type: str + type: MessageType stream_id: int # Only for stream msgs. avatar_url: str content_type: str From 99157c2219603dea46eedff229883a86c5d6bd88 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 20 Apr 2023 13:23:58 -0700 Subject: [PATCH 040/276] refactor: api_types/model: Move pre-ZFL53 lengths to api_types. Comment adjusted; Final applied to new constants. --- zulipterminal/api_types.py | 13 ++++++++++++- zulipterminal/model.py | 11 +++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index a52f51896d..1d730a181d 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional, Union -from typing_extensions import Literal, NotRequired, TypedDict +from typing_extensions import Final, Literal, NotRequired, TypedDict # These are documented in the zulip package (python-zulip-api repo) from zulip import EditPropagateMode # one/all/later @@ -22,6 +22,17 @@ TYPING_STARTED_WAIT_PERIOD = 10 TYPING_STOPPED_WAIT_PERIOD = 5 + +############################################################################### +# These values are in the register response from ZFL 53 +# Before this feature level, they had the listed default (fixed) values +# (strictly, the stream value was available, under a different name) + +MAX_STREAM_NAME_LENGTH: Final = 60 +MAX_TOPIC_NAME_LENGTH: Final = 60 +MAX_MESSAGE_LENGTH: Final = 10000 + + ############################################################################### # Core message types (used in Composition and Message below) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index dfc144d745..84eae6757f 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -31,6 +31,9 @@ from zulipterminal import unicode_emojis from zulipterminal.api_types import ( + MAX_MESSAGE_LENGTH, + MAX_STREAM_NAME_LENGTH, + MAX_TOPIC_NAME_LENGTH, Composition, EditPropagateMode, Event, @@ -70,14 +73,6 @@ OFFLINE_THRESHOLD_SECS = 140 -# Adapted from zerver/models.py -# These fields have migrated to the API inside the Realm object -# in ZFL 53. To allow backporting to earlier server versions, we -# define these hard-coded parameters. -MAX_STREAM_NAME_LENGTH = 60 -MAX_TOPIC_NAME_LENGTH = 60 -MAX_MESSAGE_LENGTH = 10000 - class ServerConnectionFailure(Exception): pass From 10e02d0f49a77c41baaf4e15b1bbc1c0ae38e1a6 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 20 Apr 2023 13:59:57 -0700 Subject: [PATCH 041/276] refactor: api_types/model: Improve types for typing notifications. With the addition of stream typing notifications, and soon the migration to "direct" in place of "private", message type can now be useful to include. However, note that we don't yet explicitly specify the type for the direct/private message form, since this was added only in ZFL 58, Zulip 4.0. --- zulipterminal/api_types.py | 37 +++++++++++++++++++++++++++++++------ zulipterminal/model.py | 8 +++++--- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 1d730a181d..4e26e5388d 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -17,12 +17,6 @@ RESOLVED_TOPIC_PREFIX = "✔ " -# Refer to https://zulip.com/api/set-typing-status for the protocol -# on typing notifications sent by clients. -TYPING_STARTED_WAIT_PERIOD = 10 -TYPING_STOPPED_WAIT_PERIOD = 5 - - ############################################################################### # These values are in the register response from ZFL 53 # Before this feature level, they had the listed default (fixed) values @@ -42,6 +36,37 @@ MessageType = Union[DirectMessageString, StreamMessageString] +############################################################################### +# Parameters to pass in request to: +# https://zulip.com/api/set-typing-status +# Refer to the top of that page for the expected protocol clients should observe +# +# NOTE: `to` field could be email until ZFL 11/3.0; ids were possible from 2.0+ + +# Timing parameters for when notifications should occur +TYPING_STARTED_WAIT_PERIOD: Final = 10 +TYPING_STOPPED_WAIT_PERIOD: Final = 5 +TYPING_STARTED_EXPIRY_PERIOD: Final = 15 # TODO: Needs implementation in ZT + +TypingStatusChange = Literal["start", "stop"] + + +class DirectTypingNotification(TypedDict): + # The type field was added in ZFL 58, Zulip 4.0, so don't require it yet + ## type: DirectMessageString + op: TypingStatusChange + to: List[int] + + +# NOTE: Not yet implemented in ZT +# New in ZFL 58, Zulip 4.0 +class StreamTypingNotification(TypedDict): + type: StreamMessageString + op: TypingStatusChange + to: List[int] # NOTE: Length 1, stream id + topic: str + + ############################################################################### # Parameter to pass in request to: # https://zulip.com/api/send-message diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 84eae6757f..9166e304a9 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -27,7 +27,7 @@ import zulip from bs4 import BeautifulSoup -from typing_extensions import Literal, TypedDict +from typing_extensions import TypedDict from zulipterminal import unicode_emojis from zulipterminal.api_types import ( @@ -35,6 +35,7 @@ MAX_STREAM_NAME_LENGTH, MAX_TOPIC_NAME_LENGTH, Composition, + DirectTypingNotification, EditPropagateMode, Event, PrivateComposition, @@ -44,6 +45,7 @@ StreamComposition, StreamMessageUpdateRequest, Subscription, + TypingStatusChange, ) from zulipterminal.config.keys import primary_key_for_command from zulipterminal.config.symbols import STREAM_TOPIC_SEPARATOR @@ -513,12 +515,12 @@ def mark_message_ids_as_read(self, id_list: List[int]) -> None: @asynch def send_typing_status_by_user_ids( - self, recipient_user_ids: List[int], *, status: Literal["start", "stop"] + self, recipient_user_ids: List[int], *, status: TypingStatusChange ) -> None: if not self.user_settings()["send_private_typing_notifications"]: return if recipient_user_ids: - request = {"to": recipient_user_ids, "op": status} + request: DirectTypingNotification = {"to": recipient_user_ids, "op": status} response = self.client.set_typing_status(request) display_error_if_present(response, self.controller) else: From db866cb59d2d15f619459400f01da544063e75eb Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 23 Apr 2023 23:43:52 -0700 Subject: [PATCH 042/276] README/editorconfig: Add .editorconfig to improve editing consistency. Add small section in README to document the benefits. --- .editorconfig | 26 ++++++++++++++++++++++++++ README.md | 9 +++++++++ 2 files changed, 35 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..da76e2dc08 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +# See https://editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +indent_size = 4 +indent_style = space +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{py,pyi}] +max_line_length = 88 + +[*.{md,yaml,yml}] +indent_size = 2 + +[Makefile] +indent_style = tab + +[*.diff] +trim_trailing_whitespace = false + +[.git/*] +trim_trailing_whitespace = false diff --git a/README.md b/README.md index 2556752e6b..15f56be29e 100644 --- a/README.md +++ b/README.md @@ -494,6 +494,15 @@ If using make with pip, running `make` will ensure the development environment is up to date with the specified dependencies, useful after fetching from git and rebasing. +#### Editing the source + +Pick your favorite text editor or development environment! + +The source includes an `.editorconfig` file which enables many editors to +automatically configure themselves to produce files which meet the minimum +requirements for the project. See https://editorconfig.org for editor support; +note that some may require plugins if you wish to use this feature. + #### Passing linters and automated tests The linters and automated tests (pytest) are run in CI (GitHub Actions) when From 8723e7c6e5c1f78ce3ecb1196b6f72f5ba1e4afd Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 30 Apr 2023 18:36:44 -0700 Subject: [PATCH 043/276] README: Rewrite gitlint section to enhance use of the tool. Prior to this commit, while the tool is installed in every development environment, installing the hook was simply 'highly suggested'. This rephrases the text to improve the motivation for using gitlint, including running it manually, and then showing how automating it is therefore useful. This section still follows the general commit message style section; it may be necessary to refer one to another, or otherwise combine them to improve motivation towards using this tool. Alternatively, gitlint could be added to CI. --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 15f56be29e..4d07d88318 100644 --- a/README.md +++ b/README.md @@ -671,13 +671,14 @@ GitLint cannot check everything - including language or grammar! #### GitLint -If you plan to submit git commits in pull-requests (PRs), then we highly -suggest installing the `gitlint` commit-message hook by running `gitlint -install-hook` (or `pipenv run gitlint install-hook` with pipenv setups). +The `gitlint` tool is installed by default in the development environment, and +can help ensure that your commits meet the expected standard. -While the content still depends upon your writing skills, this ensures a more -consistent formatting structure between commits, including by different -authors. +The tool can check specific commits manually, eg. `gitlint` for the latest +commit, or `gitlint --commits main..` for commits leading from `main`. +However, we highly recommend running `gitlint install-hook` to install the +`gitlint` commit-message hook +(or `pipenv run gitlint install-hook` with pipenv setups). If the hook is installed as described above, then after completing the text for a commit, it will be checked by gitlint against the style we have set up, and @@ -686,9 +687,9 @@ If gitlint finds any, it will ask if you wish to commit with the message as it is (`y` for 'yes'), stop the commit process (`n` for 'no'), or edit the commit message (`e` for 'edit'). -Other gitlint options are available; for example it is possible to apply it to -a range of commits with the `--commits` option, eg. `gitlint --commits -HEAD~2..HEAD` would apply it to the last few commits. +While the content still depends upon your writing skills, this ensures a more +consistent formatting structure between commits, including by different +authors. ### Tips for working with tests (pytest) From 27611dc997d7388846211a48003edeef23812bb1 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 30 Apr 2023 19:18:43 -0700 Subject: [PATCH 044/276] README: Improve structure of linting/testing section. Various notes were previously added to this documentation. This commit reorganizes text in these sections to improve readability, including raising the section level of the Editing, Linting/testing, and Commit sections. In particular, this should more clearly explain, in order: - when checks are run on GitHub - that they can be run locally and why that's useful - the expectation of passing all linting and tests for a PR to be merged - ideas of what to do if linting/tests don't pass --- README.md | 52 +++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 4d07d88318..c59e35103d 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ systems: (creates a virtual environment named `zt_venv` in the current directory) 2. `source zt_venv/bin/activate` (activates the virtual environment; this assumes a bash-like shell) -3. Run one of the install commands above, +3. Run one of the install commands above. If you open a different terminal window (or log-off/restart your computer), you'll need to run **step 2** of the above list again before running @@ -494,7 +494,7 @@ If using make with pip, running `make` will ensure the development environment is up to date with the specified dependencies, useful after fetching from git and rebasing. -#### Editing the source +### Editing the source Pick your favorite text editor or development environment! @@ -503,48 +503,54 @@ automatically configure themselves to produce files which meet the minimum requirements for the project. See https://editorconfig.org for editor support; note that some may require plugins if you wish to use this feature. -#### Passing linters and automated tests +### Running linters and automated tests -The linters and automated tests (pytest) are run in CI (GitHub Actions) when -you submit a pull request (PR), and we expect them to pass before code is -merged. -> **NOTE:** Mergeable PRs with multiple commits are expected to pass linting -> and tests at **each commit**, not simply overall +The linters and automated tests (pytest) are automatically run in CI (GitHub +Actions) when you submit a pull request (PR), or push changes to an existing +pull request. -Running these tools locally can speed your development and avoid the need -to repeatedly push your code to GitHub simply to run these checks. -> If you have troubles understanding why the linters or pytest are failing, -> please do push your code to a branch/PR and we can discuss the problems in -> the PR or on chat.zulip.org. - -All linters and tests can be run using the commands in the table above. -Individual linters may also be run via scripts in `tools/`. +However, running these checks on your computer can speed up your development by +avoiding the need to repeatedly push your code to GitHub. +Commands to achieve this are listed in the table of development tasks above +(individual linters may also be run via scripts in `tools/`). In addition, if using a `make`-based system: - `make lint` and `make test` run all of each group of tasks - `make check` runs all checks, which is useful before pushing a PR (or an update) +- `tools/check-branch` will run `make check` on each commit in your branch + +> **NOTE: It is highly unlikely that a pull request will be merged, until *all* +linters and tests are passing, including on a per-commit basis.** + +#### The linters and tests aren't passing on my branch/PR - what do I do? Correcting some linting errors requires manual intervention, such as from `mypy` for type-checking. + +For tips on testing, please review the section further below regarding pytest. + However, other linting errors may be fixed automatically, as detailed below - **this can save a lot of time manually adjusting your code to pass the linters!** -#### Updating hotkeys & file docstrings, vs related documentation +> If you have troubles understanding why the linters or pytest are failing, +> please do push your code to a branch/PR and we can discuss the problems in +> the PR or on chat.zulip.org. + +#### Automatically updating hotkeys & file docstrings, vs related documentation If you update these, note that you do not need to update the text in both places manually to pass linting. The source of truth is in the source code, so simply update the python file and -run the relevant tool, as detailed below. - -Currently we have +run the relevant tool. Currently we have: * `tools/lint-hotkeys --fix` to regenerate docs/hotkeys.md from config/keys.py * `tools/lint-docstring --fix` to regenerate docs/developer-file-overview.md from file docstrings -(these tools are also used for the linting process to ensure that these files are synchronzed) +(these tools are also used for the linting process, to ensure that these files +are synchronized) -#### Auto-formatting code +#### Automatically formatting code The project uses `black` and `isort` for code-style and import sorting respectively. @@ -559,7 +565,7 @@ the changes. You can also use the tools individually on a file or directory, eg. `black zulipterminal` or `isort tests/model/test_model.py` -#### Structuring Commits - speeding up reviews, merging & development +### Structuring Commits - speeding up reviews, merging & development As you work locally, investigating changes to make, it's common to make a series of small commits to store your progress. From fdbf38a7195c50778d19b2fb3afc182de276dbc9 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 1 May 2023 10:24:22 -0700 Subject: [PATCH 045/276] refactor: messages/views: Remove extra parameter to top_header_bar(). This parameter was previously simply passed the value of the object, so just use self instead. It was also erroneously named message_view, whereas the relevant object was a message_box. Tests updated, including one test case previously not being run correctly. --- tests/ui/test_ui_tools.py | 3 +-- tests/ui_tools/test_messages.py | 2 +- zulipterminal/ui_tools/messages.py | 6 +++--- zulipterminal/ui_tools/views.py | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index bb713403a1..481598c884 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -386,8 +386,7 @@ def test_message_calls_search_and_header_bar(self, mocker, msg_view): msg_w = mocker.MagicMock() msg_w.original_widget.message = {"id": 1} msg_view.update_search_box_narrow(msg_w.original_widget) - msg_w.original_widget.top_header_bar.assert_called_once_with - (msg_w.original_widget) + msg_w.original_widget.top_header_bar.assert_called_once_with() msg_w.original_widget.top_search_bar.assert_called_once_with() def test_read_message_no_msgw(self, mocker, msg_view): diff --git a/tests/ui_tools/test_messages.py b/tests/ui_tools/test_messages.py index 78d9d60fb0..7f3b78b98b 100644 --- a/tests/ui_tools/test_messages.py +++ b/tests/ui_tools/test_messages.py @@ -962,7 +962,7 @@ def test_msg_generates_search_and_header_bar( current_message = messages[msg_type] msg_box = MessageBox(current_message, self.model, messages[0]) search_bar = msg_box.top_search_bar() - header_bar = msg_box.top_header_bar(msg_box) + header_bar = msg_box.top_header_bar() assert header_bar[0].text.startswith(assert_header_bar) assert search_bar.text_to_fill == assert_search_bar diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index 1911dc5dd6..f9ab74ab3e 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -186,11 +186,11 @@ def private_header(self) -> Any: header.markup = title_markup return header - def top_header_bar(self, message_view: Any) -> Any: + def top_header_bar(self) -> Any: if self.message["type"] == "stream": - return message_view.stream_header() + return self.stream_header() else: - return message_view.private_header() + return self.private_header() def top_search_bar(self) -> Any: curr_narrow = self.model.narrow diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index a4b673d6b9..cf388e9cd3 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -241,7 +241,7 @@ def update_search_box_narrow(self, message_view: Any) -> None: return # if view is ready display current narrow # at the bottom of the view. - recipient_bar = message_view.top_header_bar(message_view) + recipient_bar = message_view.top_header_bar() top_header = message_view.top_search_bar() self.model.controller.view.search_box.conversation_focus.set_text( top_header.markup From 5505093f38ddd754193ef4fe0b17b297a6fa3b4f Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 4 May 2023 10:24:38 -0700 Subject: [PATCH 046/276] refactor: messages/views: Rename top_header_bar to recipient_header. This improves the naming at call sites, as well as enabling clear additional usage and reduction of duplicated code in main_view(). Tests updated. --- tests/ui/test_ui_tools.py | 2 +- tests/ui_tools/test_messages.py | 2 +- zulipterminal/ui_tools/messages.py | 7 ++----- zulipterminal/ui_tools/views.py | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index 481598c884..2cc93d4941 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -386,7 +386,7 @@ def test_message_calls_search_and_header_bar(self, mocker, msg_view): msg_w = mocker.MagicMock() msg_w.original_widget.message = {"id": 1} msg_view.update_search_box_narrow(msg_w.original_widget) - msg_w.original_widget.top_header_bar.assert_called_once_with() + msg_w.original_widget.recipient_header.assert_called_once_with() msg_w.original_widget.top_search_bar.assert_called_once_with() def test_read_message_no_msgw(self, mocker, msg_view): diff --git a/tests/ui_tools/test_messages.py b/tests/ui_tools/test_messages.py index 7f3b78b98b..db22da3d2b 100644 --- a/tests/ui_tools/test_messages.py +++ b/tests/ui_tools/test_messages.py @@ -962,7 +962,7 @@ def test_msg_generates_search_and_header_bar( current_message = messages[msg_type] msg_box = MessageBox(current_message, self.model, messages[0]) search_bar = msg_box.top_search_bar() - header_bar = msg_box.top_header_bar() + header_bar = msg_box.recipient_header() assert header_bar[0].text.startswith(assert_header_bar) assert search_bar.text_to_fill == assert_search_bar diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index f9ab74ab3e..583b9c5883 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -186,7 +186,7 @@ def private_header(self) -> Any: header.markup = title_markup return header - def top_header_bar(self) -> Any: + def recipient_header(self) -> Any: if self.message["type"] == "stream": return self.stream_header() else: @@ -622,10 +622,7 @@ def soup2markup( def main_view(self) -> List[Any]: # Recipient Header if self.need_recipient_header(): - if self.message["type"] == "stream": - recipient_header = self.stream_header() - else: - recipient_header = self.private_header() + recipient_header = self.recipient_header() else: recipient_header = None diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index cf388e9cd3..b8828cfdec 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -241,7 +241,7 @@ def update_search_box_narrow(self, message_view: Any) -> None: return # if view is ready display current narrow # at the bottom of the view. - recipient_bar = message_view.top_header_bar() + recipient_bar = message_view.recipient_header() top_header = message_view.top_search_bar() self.model.controller.view.search_box.conversation_focus.set_text( top_header.markup From ab5de216699548a5f13bf15cbdc72c5e650aea0f Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 1 May 2023 08:26:36 -0700 Subject: [PATCH 047/276] refactor: views: Explicitly define __init__ for ModListWalker. Fixture updated. --- tests/ui/test_ui_tools.py | 2 +- zulipterminal/ui_tools/views.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index 2cc93d4941..2c5358fa9a 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -30,7 +30,7 @@ class TestModListWalker: @pytest.fixture def mod_walker(self): - return ModListWalker([list(range(1))]) + return ModListWalker(contents=[list(range(1))]) @pytest.mark.parametrize( "num_items, focus_position", diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index b8828cfdec..8eaae7b262 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -64,6 +64,9 @@ class ModListWalker(urwid.SimpleFocusListWalker): + def __init__(self, *, contents: List[Any]) -> None: + super().__init__(contents) + def set_focus(self, position: int) -> None: # When setting focus via set_focus method. self.focus = position @@ -106,7 +109,7 @@ def __init__(self, model: Any, view: Any) -> None: self.view = view # Initialize for reference self.focus_msg = 0 - self.log = ModListWalker(self.main_view()) + self.log = ModListWalker(contents=self.main_view()) self.log.read_message = self.read_message super().__init__(self.log) From 6ad4f9837503d5518dc5d794f4b8013d3b4db030 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 1 May 2023 08:30:13 -0700 Subject: [PATCH 048/276] refactor: views: Set read_message in ModListWalker via __init__. This avoids explicitly setting it from the caller after initialization, and the need for hasattr checks where it is used. Tests updated. --- tests/ui/test_ui_tools.py | 7 +++---- zulipterminal/ui_tools/views.py | 14 +++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index 2c5358fa9a..53ba2f2f30 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -29,8 +29,9 @@ class TestModListWalker: @pytest.fixture - def mod_walker(self): - return ModListWalker(contents=[list(range(1))]) + def mod_walker(self, mocker): + read_message = mocker.Mock(spec=lambda: None) + return ModListWalker(contents=[list(range(1))], action=read_message) @pytest.mark.parametrize( "num_items, focus_position", @@ -46,12 +47,10 @@ def test_extend(self, num_items, focus_position, mod_walker, mocker): mod_walker._set_focus.assert_called_once_with(focus_position) def test__set_focus(self, mod_walker, mocker): - mod_walker.read_message = mocker.Mock() mod_walker._set_focus(0) mod_walker.read_message.assert_called_once_with() def test_set_focus(self, mod_walker, mocker): - mod_walker.read_message = mocker.Mock() mod_walker.set_focus(0) mod_walker.read_message.assert_called_once_with() diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 8eaae7b262..2c9cf5430a 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -64,15 +64,16 @@ class ModListWalker(urwid.SimpleFocusListWalker): - def __init__(self, *, contents: List[Any]) -> None: + def __init__(self, *, contents: List[Any], action: Callable[[], None]) -> None: + self.read_message = action super().__init__(contents) def set_focus(self, position: int) -> None: # When setting focus via set_focus method. self.focus = position self._modified() - if hasattr(self, "read_message"): - self.read_message() + + self.read_message() def _set_focus(self, index: int) -> None: # This method is called when directly setting focus via @@ -88,8 +89,8 @@ def _set_focus(self, index: int) -> None: if index != self._focus: self._focus_changed(index) self._focus = index - if hasattr(self, "read_message"): - self.read_message() + + self.read_message() def extend(self, items: List[Any], focus_position: Optional[int] = None) -> int: if focus_position is None: @@ -109,8 +110,7 @@ def __init__(self, model: Any, view: Any) -> None: self.view = view # Initialize for reference self.focus_msg = 0 - self.log = ModListWalker(contents=self.main_view()) - self.log.read_message = self.read_message + self.log = ModListWalker(contents=self.main_view(), action=self.read_message) super().__init__(self.log) self.set_focus(self.focus_msg) From b3c70e7e41613dcf1c9f5fcef124dcb5dc8b102c Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 1 May 2023 08:57:16 -0700 Subject: [PATCH 049/276] refactor: views: Rename ModListWalker.read_message to ._action. While this is currently only used in this project, there is no need to make it so specific. Tests updated. --- tests/ui/test_ui_tools.py | 6 ++++-- zulipterminal/ui_tools/views.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index 53ba2f2f30..3c4edf64cd 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -47,12 +47,14 @@ def test_extend(self, num_items, focus_position, mod_walker, mocker): mod_walker._set_focus.assert_called_once_with(focus_position) def test__set_focus(self, mod_walker, mocker): + mod_walker._action.assert_not_called() mod_walker._set_focus(0) - mod_walker.read_message.assert_called_once_with() + mod_walker._action.assert_called_once_with() def test_set_focus(self, mod_walker, mocker): + mod_walker._action.assert_not_called() mod_walker.set_focus(0) - mod_walker.read_message.assert_called_once_with() + mod_walker._action.assert_called_once_with() class TestMessageView: diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 2c9cf5430a..b59533daf3 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -65,7 +65,7 @@ class ModListWalker(urwid.SimpleFocusListWalker): def __init__(self, *, contents: List[Any], action: Callable[[], None]) -> None: - self.read_message = action + self._action = action super().__init__(contents) def set_focus(self, position: int) -> None: @@ -73,7 +73,7 @@ def set_focus(self, position: int) -> None: self.focus = position self._modified() - self.read_message() + self._action() def _set_focus(self, index: int) -> None: # This method is called when directly setting focus via @@ -90,7 +90,7 @@ def _set_focus(self, index: int) -> None: self._focus_changed(index) self._focus = index - self.read_message() + self._action() def extend(self, items: List[Any], focus_position: Optional[int] = None) -> int: if focus_position is None: From b81f30f86a2bbb00672bcea2746d799981cb5e5f Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 1 May 2023 13:25:16 -0700 Subject: [PATCH 050/276] refactor: core/model/helper: Simplify pointer type to Optional[int]. None is much simpler to check, compared to Set[None]. Tests and fixtures updated. --- tests/conftest.py | 2 +- tests/model/test_model.py | 6 ++---- zulipterminal/core.py | 4 ++-- zulipterminal/helper.py | 5 ++--- zulipterminal/model.py | 6 +++--- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8ab80ac94e..3e77d51f33 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -820,7 +820,7 @@ def empty_index( ) -> Index: return deepcopy( Index( - pointer=defaultdict(set, {}), + pointer=dict(), all_msg_ids=set(), starred_msg_ids=set(), mentioned_msg_ids=set(), diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 7d858b8bea..b1cbc1d5b3 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -330,7 +330,7 @@ def test_normalize_and_cache_message_retention_text( == expect_msg_retention_text[stream_id] ) - @pytest.mark.parametrize("msg_id", [1, 5, set()]) + @pytest.mark.parametrize("msg_id", [1, 5, None]) @pytest.mark.parametrize( "narrow", [ @@ -362,9 +362,7 @@ def test_get_focus_in_current_narrow_individually(self, model, msg_id, narrow): ], ) def test_set_focus_in_current_narrow(self, mocker, model, narrow, msg_id): - from collections import defaultdict - - model.index = dict(pointer=defaultdict(set)) + model.index = dict(pointer=dict()) model.narrow = narrow model.set_focus_in_current_narrow(msg_id) assert model.index["pointer"][str(narrow)] == msg_id diff --git a/zulipterminal/core.py b/zulipterminal/core.py index 8ee293d8ee..f1f32e41c9 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -571,9 +571,9 @@ def _narrow_to(self, anchor: Optional[int], **narrow: Any) -> None: w_list = create_msg_box_list(self.model, msg_id_list, focus_msg_id=anchor) focus_position = self.model.get_focus_in_current_narrow() - if focus_position == set(): # No available focus; set to end + if focus_position is None: # No available focus; set to end focus_position = len(w_list) - 1 - assert not isinstance(focus_position, set) + assert focus_position is not None self.view.message_view.log.clear() if 0 <= focus_position < len(w_list): diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index 12f402b5e6..0b7f59b328 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -25,7 +25,6 @@ Set, Tuple, TypeVar, - Union, ) from urllib.parse import unquote @@ -79,7 +78,7 @@ class TidiedUserInfo(TypedDict): class Index(TypedDict): - pointer: Dict[str, Union[int, Set[None]]] # narrow_str, message_id + pointer: Dict[str, Optional[int]] # narrow_str, message_id (or no data) # Various sets of downloaded message ids (all, starred, ...) all_msg_ids: Set[int] starred_msg_ids: Set[int] @@ -97,7 +96,7 @@ class Index(TypedDict): initial_index = Index( - pointer=defaultdict(set), + pointer=dict(), all_msg_ids=set(), starred_msg_ids=set(), mentioned_msg_ids=set(), diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 9166e304a9..68b7850829 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -264,12 +264,12 @@ def normalize_and_cache_message_retention_text(self) -> None: ) self.cached_retention_text[stream["stream_id"]] = message_retention_response - def get_focus_in_current_narrow(self) -> Union[int, Set[None]]: + def get_focus_in_current_narrow(self) -> Optional[int]: """ Returns the focus in the current narrow. - For no existing focus this returns {}, otherwise the message ID. + For no existing focus this returns None, otherwise the message ID. """ - return self.index["pointer"][repr(self.narrow)] + return self.index["pointer"].get(repr(self.narrow), None) def set_focus_in_current_narrow(self, focus_message: int) -> None: self.index["pointer"][repr(self.narrow)] = focus_message From e7cdb0612167afe09d512b251ca17252eb664bb2 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 7 May 2023 21:45:53 -0700 Subject: [PATCH 051/276] bugfix: views: Add missing part of previous focus refactor from #1393. Test updated and slightly reworded. --- tests/ui/test_ui_tools.py | 8 +++++--- zulipterminal/ui_tools/views.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index 3c4edf64cd..0081823a6c 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -80,8 +80,10 @@ def test_init(self, mocker, msg_view, msg_box): assert msg_view.old_loading is False assert msg_view.new_loading is False - @pytest.mark.parametrize("narrow_focus_pos, focus_msg", [(set(), 1), (0, 0)]) - def test_main_view(self, mocker, narrow_focus_pos, focus_msg): + @pytest.mark.parametrize( + "narrow_focus_pos, expected_focus_msg", [(None, 1), (0, 0)] + ) + def test_main_view(self, mocker, narrow_focus_pos, expected_focus_msg): mocker.patch(MESSAGEVIEW + ".read_message") self.urwid.SimpleFocusListWalker.return_value = mocker.Mock() mocker.patch(MESSAGEVIEW + ".set_focus") @@ -91,7 +93,7 @@ def test_main_view(self, mocker, narrow_focus_pos, focus_msg): msg_view = MessageView(self.model, self.view) - assert msg_view.focus_msg == focus_msg + assert msg_view.focus_msg == expected_focus_msg @pytest.mark.parametrize( "messages_fetched", diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index b59533daf3..ffac925590 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -121,7 +121,7 @@ def __init__(self, model: Any, view: Any) -> None: def main_view(self) -> List[Any]: msg_btn_list = create_msg_box_list(self.model) focus_msg = self.model.get_focus_in_current_narrow() - if focus_msg == set(): + if focus_msg is None: focus_msg = len(msg_btn_list) - 1 self.focus_msg = focus_msg return msg_btn_list From ce9deccc5c411bda370125445f9565abfc74fe52 Mon Sep 17 00:00:00 2001 From: Wladimir Ramos Date: Sat, 6 May 2023 16:00:50 -0300 Subject: [PATCH 052/276] refactor: model/views: Migrate get_next_unread_pm & tests to model. --- tests/model/test_model.py | 20 ++++++++++++++++++++ tests/ui/test_ui_tools.py | 28 +++++----------------------- zulipterminal/model.py | 16 ++++++++++++++++ zulipterminal/ui_tools/views.py | 18 +----------------- 4 files changed, 42 insertions(+), 40 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index b1cbc1d5b3..e48f672177 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -72,6 +72,7 @@ def test_init( assert model.recipients == frozenset() assert model.index == initial_index assert model._last_unread_topic is None + assert model.last_unread_pm is None model.get_messages.assert_called_once_with( num_before=30, num_after=10, anchor=None ) @@ -3743,6 +3744,25 @@ def test_get_next_unread_topic( assert unread_topic == next_unread_topic + def test_get_next_unread_pm(self, model): + model.unread_counts = {"unread_pms": {1: 1, 2: 1}} + return_value = model.get_next_unread_pm() + assert return_value == 1 + assert model.last_unread_pm == 1 + + def test_get_next_unread_pm_again(self, model): + model.unread_counts = {"unread_pms": {1: 1, 2: 1}} + model.last_unread_pm = 1 + return_value = model.get_next_unread_pm() + assert return_value == 2 + assert model.last_unread_pm == 2 + + def test_get_next_unread_pm_no_unread(self, model): + model.unread_counts = {"unread_pms": {}} + return_value = model.get_next_unread_pm() + assert return_value is None + assert model.last_unread_pm is None + @pytest.mark.parametrize( "stream_id, expected_response", [ diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index 0081823a6c..03d56f7554 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -806,32 +806,12 @@ def mid_col_view(self): def test_init(self, mid_col_view): assert mid_col_view.model == self.model assert mid_col_view.controller == self.model.controller - assert mid_col_view.last_unread_pm is None assert mid_col_view.search_box == self.search_box assert self.view.message_view == "MSG_LIST" self.super.assert_called_once_with( "MSG_LIST", header=self.search_box, footer=self.write_box ) - def test_get_next_unread_pm(self, mid_col_view): - mid_col_view.model.unread_counts = {"unread_pms": {1: 1, 2: 1}} - return_value = mid_col_view.get_next_unread_pm() - assert return_value == 1 - assert mid_col_view.last_unread_pm == 1 - - def test_get_next_unread_pm_again(self, mid_col_view): - mid_col_view.model.unread_counts = {"unread_pms": {1: 1, 2: 1}} - mid_col_view.last_unread_pm = 1 - return_value = mid_col_view.get_next_unread_pm() - assert return_value == 2 - assert mid_col_view.last_unread_pm == 2 - - def test_get_next_unread_pm_no_unread(self, mid_col_view): - mid_col_view.model.unread_counts = {"unread_pms": {}} - return_value = mid_col_view.get_next_unread_pm() - assert return_value is None - assert mid_col_view.last_unread_pm is None - @pytest.mark.parametrize("key", keys_for_command("SEARCH_MESSAGES")) def test_keypress_focus_header(self, mid_col_view, mocker, key, widget_size): size = widget_size(mid_col_view) @@ -932,8 +912,9 @@ def test_keypress_NEXT_UNREAD_PM_stream( ): size = widget_size(mid_col_view) mocker.patch(MIDCOLVIEW + ".focus_position") - mocker.patch(MIDCOLVIEW + ".get_next_unread_pm", return_value=1) + mid_col_view.model.user_id_email_dict = {1: "EMAIL"} + mid_col_view.model.get_next_unread_pm.return_value = 1 mid_col_view.keypress(size, key) @@ -948,16 +929,17 @@ def test_keypress_NEXT_UNREAD_PM_no_pm( ): size = widget_size(mid_col_view) mocker.patch(MIDCOLVIEW + ".focus_position") - mocker.patch(MIDCOLVIEW + ".get_next_unread_pm", return_value=None) + mid_col_view.model.get_next_unread_pm.return_value = None return_value = mid_col_view.keypress(size, key) + assert return_value == key @pytest.mark.parametrize("key", keys_for_command("PRIVATE_MESSAGE")) def test_keypress_PRIVATE_MESSAGE(self, mid_col_view, mocker, key, widget_size): size = widget_size(mid_col_view) mocker.patch(MIDCOLVIEW + ".focus_position") - mocker.patch(MIDCOLVIEW + ".get_next_unread_pm", return_value=None) + mid_col_view.model.get_next_unread_pm.return_value = None mid_col_view.footer = mocker.Mock() return_value = mid_col_view.keypress(size, key) mid_col_view.footer.private_box_view.assert_called_once_with() diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 68b7850829..9e92d058cd 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -108,6 +108,7 @@ def __init__(self, controller: Any) -> None: self.recipients: FrozenSet[Any] = frozenset() self.index = initial_index self._last_unread_topic = None + self.last_unread_pm = None self.user_id = -1 self.user_email = "" @@ -902,6 +903,21 @@ def get_next_unread_topic(self) -> Optional[Tuple[int, str]]: next_topic = True return None + def get_next_unread_pm(self) -> Optional[int]: + pms = list(self.unread_counts["unread_pms"].keys()) + next_pm = False + for pm in pms: + if next_pm is True: + self.last_unread_pm = pm + return pm + if pm == self.last_unread_pm: + next_pm = True + if len(pms) > 0: + pm = pms[0] + self.last_unread_pm = pm + return pm + return None + def _fetch_initial_data(self) -> None: # Thread Processes to reduce start time. # NOTE: Exceptions do not work well with threads diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index ffac925590..2c2f3834fb 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -549,26 +549,10 @@ def __init__(self, view: Any, model: Any, write_box: Any, search_box: Any) -> No self.model = model self.controller = model.controller self.view = view - self.last_unread_pm = None self.search_box = search_box view.message_view = message_view super().__init__(message_view, header=search_box, footer=write_box) - def get_next_unread_pm(self) -> Optional[int]: - pms = list(self.model.unread_counts["unread_pms"].keys()) - next_pm = False - for pm in pms: - if next_pm is True: - self.last_unread_pm = pm - return pm - if pm == self.last_unread_pm: - next_pm = True - if len(pms) > 0: - pm = pms[0] - self.last_unread_pm = pm - return pm - return None - def update_message_list_status_markers(self) -> None: for message_w in self.body.log: message_box = message_w.original_widget @@ -624,7 +608,7 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: return key elif is_command_key("NEXT_UNREAD_PM", key): # narrow to next unread pm - pm = self.get_next_unread_pm() + pm = self.model.get_next_unread_pm() if pm is None: return key email = self.model.user_id_email_dict[pm] From 2566a1c2341013cb7d1a07c33173d84dde8727dd Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 8 May 2023 21:42:35 -0700 Subject: [PATCH 053/276] refactor: symbols: Add unicode details & further comments. This makes it easier to look up symbols elsewhere, though it is also useful to continue having the symbols directly in the strings, to allow them to be viewed easily in the source. --- zulipterminal/config/symbols.py | 59 ++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/zulipterminal/config/symbols.py b/zulipterminal/config/symbols.py index 2b49e8e429..41aab179d5 100644 --- a/zulipterminal/config/symbols.py +++ b/zulipterminal/config/symbols.py @@ -2,27 +2,48 @@ Terminal characters used to mark particular elements of the user interface """ -INVALID_MARKER = "✗" +# Unless otherwise noted, all symbols are in: +# - Basic Multilingual Plane (BMP) +# - Unicode v1.1 +# Suffix comments indicate: unicode name, codepoint (unicode block, version if not v1.1) + +INVALID_MARKER = "✗" # BALLOT X, U+2717 (Dingbats) STREAM_MARKER_PRIVATE = "P" STREAM_MARKER_PUBLIC = "#" -STREAM_MARKER_WEB_PUBLIC = "⊚" -STREAM_TOPIC_SEPARATOR = "▶" -# Used as a separator between messages and 'EDITED' -MESSAGE_CONTENT_MARKER = "▒" # Options are '█', '▓', '▒', '░' -QUOTED_TEXT_MARKER = "░" -MESSAGE_HEADER_DIVIDER = "━" -CHECK_MARK = "✓" -APPLICATION_TITLE_BAR_LINE = "═" -PINNED_STREAMS_DIVIDER = "-" -COLUMN_TITLE_BAR_LINE = "━" +STREAM_MARKER_WEB_PUBLIC = "⊚" # CIRCLED RING OPERATOR, U+229A (Mathematical operators) +STREAM_TOPIC_SEPARATOR = "▶" # BLACK RIGHT-POINTING TRIANGLE, U+25B6 (Geometric shapes) + +# Range of block options for consideration: '█', '▓', '▒', '░' +# FULL BLOCK U+2588, DARK SHADE U+2593, MEDIUM SHADE U+2592, LIGHT SHADE U+2591 + +# Separator between messages and 'EDITED' +MESSAGE_CONTENT_MARKER = "▒" # MEDIUM SHADE, U+2592 (Block elements) + +QUOTED_TEXT_MARKER = "░" # LIGHT SHADE, U+2591 (Block elements) + +# Extends from end of recipient details (above messages where recipients differ above) +MESSAGE_HEADER_DIVIDER = "━" # BOX DRAWINGS HEAVY HORIZONTAL, U+2501 (Box drawing) + +# NOTE: CHECK_MARK is not used for resolved topics (that is an API detail) +CHECK_MARK = "✓" # CHECK MARK, U+2713 (Dingbats) + +APPLICATION_TITLE_BAR_LINE = "═" # BOX DRAWINGS DOUBLE HORIZONTAL, U+2550 (Box drawing) +PINNED_STREAMS_DIVIDER = "-" # HYPHEN-MINUS, U+002D (Basic latin) +COLUMN_TITLE_BAR_LINE = "━" # BOX DRAWINGS HEAVY HORIZONTAL, U+2501 (Box drawing) + # NOTE: The '⏱' emoji needs an extra space while rendering. Otherwise, it # appears to overlap its subsequent text. -TIME_MENTION_MARKER = "⏱ " # Other tested options are: '⧗' and '⧖'. +# Other tested options are: '⧗' and '⧖'. +# TODO: Try 25F7, WHITE CIRCLE WITH UPPER RIGHT QUADRANT? +TIME_MENTION_MARKER = "⏱ " # STOPWATCH, U+23F1 (Misc Technical, Unicode 6.0) + MUTE_MARKER = "M" -STATUS_ACTIVE = "●" -STATUS_IDLE = "◒" -STATUS_OFFLINE = "○" -STATUS_INACTIVE = "•" -BOT_MARKER = "♟" -AUTOHIDE_TAB_LEFT_ARROW = "❰" -AUTOHIDE_TAB_RIGHT_ARROW = "❱" +STATUS_ACTIVE = "●" # BLACK CIRCLE, U+25CF (Geometric shapes) +STATUS_IDLE = "◒" # CIRCLE WITH LOWER HALF BLACK, U+25D2 (Geometric shapes) +STATUS_OFFLINE = "○" # WHITE CIRCLE, U+25CB (Geometric shapes) +STATUS_INACTIVE = "•" # BULLET, U+2022 (General punctuation) +BOT_MARKER = "♟" # BLACK CHESS PAWN, U+265F (Misc symbols) + +# Unicode 3.2: +AUTOHIDE_TAB_LEFT_ARROW = "❰" # HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT, U+2770 +AUTOHIDE_TAB_RIGHT_ARROW = "❱" # HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT, U+2771 From 369e3638be74686a5688018a5d2497d68465ca04 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 9 May 2023 18:17:07 -0700 Subject: [PATCH 054/276] render_symbols: Add support for nested symbols (dicts). Previously all symbols were assumed to be simple variables only. This change enables expanding entries stored in a dict. For example: NAME = dict(A=x, B=y) results in entries in the UI listed as: NAME__A=x NAME__B=y. --- zulipterminal/scripts/render_symbols.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/zulipterminal/scripts/render_symbols.py b/zulipterminal/scripts/render_symbols.py index e0e80b3324..f4a3850da8 100755 --- a/zulipterminal/scripts/render_symbols.py +++ b/zulipterminal/scripts/render_symbols.py @@ -13,11 +13,24 @@ ("footer", "black", "white"), ] -symbol_dict = { +initial_symbol_dict = { name: symbol for name, symbol in vars(symbols).items() if not name.startswith("__") and not name.endswith("__") } +nested_dict = { + f"{name}__{subname}": subsymbol + for name, symbol in initial_symbol_dict.items() + if isinstance(symbol, dict) + for subname, subsymbol in symbol.items() +} +symbol_dict = { + name: symbol + for name, symbol in initial_symbol_dict.items() + if not isinstance(symbol, dict) +} +symbol_dict.update(nested_dict) + max_symbol_name_length = max([len(name) for name in symbol_dict]) symbol_names_list = [urwid.Text(name, align="center") for name in symbol_dict] From 54054c089fe6fa5c617133ee7a01554b66bb946f Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 8 May 2023 21:46:59 -0700 Subject: [PATCH 055/276] refactor: symbols/core: Extract POPUP_CONTENT_BORDER into symbols. --- zulipterminal/config/symbols.py | 12 ++++++++++++ zulipterminal/core.py | 13 ++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/zulipterminal/config/symbols.py b/zulipterminal/config/symbols.py index 41aab179d5..96c0fa7bb3 100644 --- a/zulipterminal/config/symbols.py +++ b/zulipterminal/config/symbols.py @@ -47,3 +47,15 @@ # Unicode 3.2: AUTOHIDE_TAB_LEFT_ARROW = "❰" # HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT, U+2770 AUTOHIDE_TAB_RIGHT_ARROW = "❱" # HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT, U+2771 + +# All in Block elements: +POPUP_CONTENT_BORDER = dict( + tlcorner="▛", # QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT, U+259B (v3.2) + tline="▀", # UPPER HALF BLOCK, U+2580 + trcorner="▜", # QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT, U+259C (v3.2) + rline="▐", # RIGHT HALF BLOCK, U+2590 + lline="▌", # LEFT HALF BLOCK, U+258C + blcorner="▙", # QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT, U+2599 (v3.2) + bline="▄", # LOWER HALF BLOCK, U+2584 + brcorner="▟", # QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT, U+259F (v3.2) +) diff --git a/zulipterminal/core.py b/zulipterminal/core.py index f1f32e41c9..4aa5ea2839 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -19,6 +19,7 @@ from typing_extensions import Literal from zulipterminal.api_types import Composition, Message +from zulipterminal.config.symbols import POPUP_CONTENT_BORDER from zulipterminal.config.themes import ThemeSpec from zulipterminal.config.ui_sizes import ( MAX_LINEAR_SCALING_WIDTH, @@ -220,16 +221,6 @@ def clamp(n: int, minn: int, maxn: int) -> int: return max_popup_cols, max_popup_rows def show_pop_up(self, to_show: Any, style: str) -> None: - border_lines = dict( - tlcorner="▛", - tline="▀", - trcorner="▜", - rline="▐", - lline="▌", - blcorner="▙", - bline="▄", - brcorner="▟", - ) text = urwid.Text(to_show.title, align="center") title_map = urwid.AttrMap(urwid.Filler(text), style) title_box_adapter = urwid.BoxAdapter(title_map, height=1) @@ -245,7 +236,7 @@ def show_pop_up(self, to_show: Any, style: str) -> None: brcorner="", ) title = urwid.AttrMap(title_box, "popup_border") - content = urwid.LineBox(to_show, **border_lines) + content = urwid.LineBox(to_show, **POPUP_CONTENT_BORDER) self.loop.widget = urwid.Overlay( urwid.AttrMap(urwid.Frame(header=title, body=content), "popup_border"), self.view, From 2621596e66e40447577fc2cb215deff825e8cf22 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 8 May 2023 21:57:21 -0700 Subject: [PATCH 056/276] refactor: core: Simplify top of popups from LineBox to Divider. --- zulipterminal/core.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/zulipterminal/core.py b/zulipterminal/core.py index 4aa5ea2839..4667bc99da 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -224,19 +224,11 @@ def show_pop_up(self, to_show: Any, style: str) -> None: text = urwid.Text(to_show.title, align="center") title_map = urwid.AttrMap(urwid.Filler(text), style) title_box_adapter = urwid.BoxAdapter(title_map, height=1) - title_box = urwid.LineBox( - title_box_adapter, - tlcorner="▄", - tline="▄", - trcorner="▄", - rline="", - lline="", - blcorner="", - bline="", - brcorner="", - ) - title = urwid.AttrMap(title_box, "popup_border") + title_top = urwid.AttrMap(urwid.Divider("▄"), "popup_border") + title = urwid.Pile([title_top, title_box_adapter]) + content = urwid.LineBox(to_show, **POPUP_CONTENT_BORDER) + self.loop.widget = urwid.Overlay( urwid.AttrMap(urwid.Frame(header=title, body=content), "popup_border"), self.view, From cca516d6591eefaf9de29cec92f9a42f28a790cd Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 8 May 2023 21:59:19 -0700 Subject: [PATCH 057/276] refactor: symbols/core: Extract POPUP_TOP_LINE into symbols. --- zulipterminal/config/symbols.py | 1 + zulipterminal/core.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/zulipterminal/config/symbols.py b/zulipterminal/config/symbols.py index 96c0fa7bb3..32822adaea 100644 --- a/zulipterminal/config/symbols.py +++ b/zulipterminal/config/symbols.py @@ -49,6 +49,7 @@ AUTOHIDE_TAB_RIGHT_ARROW = "❱" # HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT, U+2771 # All in Block elements: +POPUP_TOP_LINE = "▄" # LOWER HALF BLOCK, U+2584 POPUP_CONTENT_BORDER = dict( tlcorner="▛", # QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT, U+259B (v3.2) tline="▀", # UPPER HALF BLOCK, U+2580 diff --git a/zulipterminal/core.py b/zulipterminal/core.py index 4667bc99da..572efd10f2 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -19,7 +19,7 @@ from typing_extensions import Literal from zulipterminal.api_types import Composition, Message -from zulipterminal.config.symbols import POPUP_CONTENT_BORDER +from zulipterminal.config.symbols import POPUP_CONTENT_BORDER, POPUP_TOP_LINE from zulipterminal.config.themes import ThemeSpec from zulipterminal.config.ui_sizes import ( MAX_LINEAR_SCALING_WIDTH, @@ -224,7 +224,7 @@ def show_pop_up(self, to_show: Any, style: str) -> None: text = urwid.Text(to_show.title, align="center") title_map = urwid.AttrMap(urwid.Filler(text), style) title_box_adapter = urwid.BoxAdapter(title_map, height=1) - title_top = urwid.AttrMap(urwid.Divider("▄"), "popup_border") + title_top = urwid.AttrMap(urwid.Divider(POPUP_TOP_LINE), "popup_border") title = urwid.Pile([title_top, title_box_adapter]) content = urwid.LineBox(to_show, **POPUP_CONTENT_BORDER) From 4d42616618c6e6aad5378a7a463f50a234b4668e Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 10 May 2023 16:47:04 -0700 Subject: [PATCH 058/276] refactor: boxes: Rewrite LineBox as Pile with Dividers. This reduces duplication of line characters. --- zulipterminal/ui_tools/boxes.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index d4298288ca..0c5bf35cda 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -218,16 +218,12 @@ def private_box_view( self.msg_write_box.set_completer_delims(DELIMS_MESSAGE_COMPOSE) self.header_write_box = urwid.Columns([self.to_write_box]) - header_line_box = urwid.LineBox( - self.header_write_box, - tlcorner="━", - tline="━", - trcorner="━", - lline="", - blcorner="─", - bline="─", - brcorner="─", - rline="", + header_line_box = urwid.Pile( + [ + urwid.Divider("━"), + self.header_write_box, + urwid.Divider("─"), + ] ) self.contents = [ (header_line_box, self.options()), @@ -354,16 +350,12 @@ def _setup_common_stream_compose( ], dividechars=1, ) - header_line_box = urwid.LineBox( - self.header_write_box, - tlcorner="━", - tline="━", - trcorner="━", - lline="", - blcorner="─", - bline="─", - brcorner="─", - rline="", + header_line_box = urwid.Pile( + [ + urwid.Divider("━"), + self.header_write_box, + urwid.Divider("─"), + ] ) write_box = [ (header_line_box, self.options()), From f05c6b15e81eb013396e54c3d0983d7c8478abc9 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 8 May 2023 22:10:03 -0700 Subject: [PATCH 059/276] refactor: symbols/boxes: Extract COMPOSE_HEADER_TOP/BOTTOM into symbols. --- zulipterminal/config/symbols.py | 3 +++ zulipterminal/ui_tools/boxes.py | 15 ++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/zulipterminal/config/symbols.py b/zulipterminal/config/symbols.py index 32822adaea..9cbb8bec7a 100644 --- a/zulipterminal/config/symbols.py +++ b/zulipterminal/config/symbols.py @@ -60,3 +60,6 @@ bline="▄", # LOWER HALF BLOCK, U+2584 brcorner="▟", # QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT, U+259F (v3.2) ) + +COMPOSE_HEADER_TOP = "━" # BOX DRAWINGS HEAVY HORIZONTAL, U+2501 (Box drawing) +COMPOSE_HEADER_BOTTOM = "─" # BOX DRAWINGS LIGHT HORIZONTAL, U+2500 (Box drawing) diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index 0c5bf35cda..f86c4dd74a 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -32,7 +32,12 @@ REGEX_STREAM_AND_TOPIC_FENCED_HALF, REGEX_STREAM_AND_TOPIC_UNFENCED, ) -from zulipterminal.config.symbols import INVALID_MARKER, STREAM_TOPIC_SEPARATOR +from zulipterminal.config.symbols import ( + COMPOSE_HEADER_BOTTOM, + COMPOSE_HEADER_TOP, + INVALID_MARKER, + STREAM_TOPIC_SEPARATOR, +) from zulipterminal.config.ui_mappings import STREAM_ACCESS_TYPE from zulipterminal.helper import ( asynch, @@ -220,9 +225,9 @@ def private_box_view( self.header_write_box = urwid.Columns([self.to_write_box]) header_line_box = urwid.Pile( [ - urwid.Divider("━"), + urwid.Divider(COMPOSE_HEADER_TOP), self.header_write_box, - urwid.Divider("─"), + urwid.Divider(COMPOSE_HEADER_BOTTOM), ] ) self.contents = [ @@ -352,9 +357,9 @@ def _setup_common_stream_compose( ) header_line_box = urwid.Pile( [ - urwid.Divider("━"), + urwid.Divider(COMPOSE_HEADER_TOP), self.header_write_box, - urwid.Divider("─"), + urwid.Divider(COMPOSE_HEADER_BOTTOM), ] ) write_box = [ From f5c98616a538fa1ed504373d77f8fd01a92b4011 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 8 May 2023 22:36:43 -0700 Subject: [PATCH 060/276] refactor: symbols/boxes: Extract MESSAGE_RECIPIENTS_BORDER into symbols. --- zulipterminal/config/symbols.py | 13 +++++++++++++ zulipterminal/ui_tools/boxes.py | 10 ++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/zulipterminal/config/symbols.py b/zulipterminal/config/symbols.py index 9cbb8bec7a..82ac331dcf 100644 --- a/zulipterminal/config/symbols.py +++ b/zulipterminal/config/symbols.py @@ -63,3 +63,16 @@ COMPOSE_HEADER_TOP = "━" # BOX DRAWINGS HEAVY HORIZONTAL, U+2501 (Box drawing) COMPOSE_HEADER_BOTTOM = "─" # BOX DRAWINGS LIGHT HORIZONTAL, U+2500 (Box drawing) + +_MESSAGE_RECIPIENTS_TOP = "─" # BOX DRAWINGS LIGHT HORIZONTAL, U+2500 (Box drawing) +_MESSAGE_RECIPIENTS_BOTTOM = _MESSAGE_RECIPIENTS_TOP +MESSAGE_RECIPIENTS_BORDER = dict( + tline=_MESSAGE_RECIPIENTS_TOP, + lline="", + trcorner=_MESSAGE_RECIPIENTS_TOP, + tlcorner=_MESSAGE_RECIPIENTS_TOP, + blcorner=_MESSAGE_RECIPIENTS_BOTTOM, + rline="", + bline=_MESSAGE_RECIPIENTS_BOTTOM, + brcorner=_MESSAGE_RECIPIENTS_BOTTOM, +) diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index f86c4dd74a..36128f444a 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -36,6 +36,7 @@ COMPOSE_HEADER_BOTTOM, COMPOSE_HEADER_TOP, INVALID_MARKER, + MESSAGE_RECIPIENTS_BORDER, STREAM_TOPIC_SEPARATOR, ) from zulipterminal.config.ui_mappings import STREAM_ACCESS_TYPE @@ -935,14 +936,7 @@ def main_view(self) -> Any: self.recipient_bar = urwid.LineBox( self.msg_narrow, title="Current message recipients", - tline="─", - lline="", - trcorner="─", - tlcorner="─", - blcorner="─", - rline="", - bline="─", - brcorner="─", + **MESSAGE_RECIPIENTS_BORDER, ) return [self.search_bar, self.recipient_bar] From 6933b1efb11d11e8cdc4d6628a49d445ac027ad9 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 8 May 2023 22:41:32 -0700 Subject: [PATCH 061/276] refactor: ui/symbols: Extract COLUMN_DIVIDER_LINE into symbols. --- zulipterminal/config/symbols.py | 1 + zulipterminal/ui.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/zulipterminal/config/symbols.py b/zulipterminal/config/symbols.py index 82ac331dcf..e12aba46dc 100644 --- a/zulipterminal/config/symbols.py +++ b/zulipterminal/config/symbols.py @@ -30,6 +30,7 @@ APPLICATION_TITLE_BAR_LINE = "═" # BOX DRAWINGS DOUBLE HORIZONTAL, U+2550 (Box drawing) PINNED_STREAMS_DIVIDER = "-" # HYPHEN-MINUS, U+002D (Basic latin) COLUMN_TITLE_BAR_LINE = "━" # BOX DRAWINGS HEAVY HORIZONTAL, U+2501 (Box drawing) +COLUMN_DIVIDER_LINE = "│" # BOX DRAWINGS LIGHT VERTICAL, U+2502 (Box drawing) # NOTE: The '⏱' emoji needs an extra space while rendering. Otherwise, it # appears to overlap its subsequent text. diff --git a/zulipterminal/ui.py b/zulipterminal/ui.py index fa140634ce..5ab4193b79 100644 --- a/zulipterminal/ui.py +++ b/zulipterminal/ui.py @@ -14,6 +14,7 @@ APPLICATION_TITLE_BAR_LINE, AUTOHIDE_TAB_LEFT_ARROW, AUTOHIDE_TAB_RIGHT_ARROW, + COLUMN_DIVIDER_LINE, COLUMN_TITLE_BAR_LINE, ) from zulipterminal.config.ui_sizes import LEFT_WIDTH, RIGHT_WIDTH, TAB_WIDTH @@ -66,8 +67,8 @@ def middle_column_view(self) -> Any: title_attr="column_title", tline=COLUMN_TITLE_BAR_LINE, bline="", - trcorner="│", - tlcorner="│", + trcorner=COLUMN_DIVIDER_LINE, + tlcorner=COLUMN_DIVIDER_LINE, ) def right_column_view(self) -> Any: From 055b2bfd1abbb68b3f5cd5f0a012a500f2fbd99a Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 8 May 2023 23:12:39 -0700 Subject: [PATCH 062/276] refactor: ui/views: Remove unnecessary LineBox symbols. --- zulipterminal/ui.py | 2 +- zulipterminal/ui_tools/views.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/zulipterminal/ui.py b/zulipterminal/ui.py index 5ab4193b79..a8ffdf3165 100644 --- a/zulipterminal/ui.py +++ b/zulipterminal/ui.py @@ -82,7 +82,7 @@ def right_column_view(self) -> Any: tline=COLUMN_TITLE_BAR_LINE, trcorner=COLUMN_TITLE_BAR_LINE, lline="", - blcorner="─", + blcorner="", rline="", bline="", brcorner="", diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 2c2f3834fb..53cf3778f1 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -321,10 +321,10 @@ def __init__(self, streams_btn_list: List[Any], view: Any) -> None: list_box, header=urwid.LineBox( self.stream_search_box, - tlcorner="─", + tlcorner="", tline="", lline="", - trcorner="─", + trcorner="", blcorner="─", rline="", bline="─", @@ -426,10 +426,10 @@ def __init__( self.list_box, header=urwid.LineBox( self.header_list, - tlcorner="─", + tlcorner="", tline="", lline="", - trcorner="─", + trcorner="", blcorner="─", rline="", bline="─", @@ -640,10 +640,10 @@ def __init__(self, view: Any) -> None: self.view.user_search = self.user_search search_box = urwid.LineBox( self.user_search, - tlcorner="─", + tlcorner="", tline="", lline="", - trcorner="─", + trcorner="", blcorner="─", rline="", bline="─", @@ -842,7 +842,7 @@ def streams_view(self) -> Any: rline="", lline="", bline="", - brcorner="─", + brcorner="", ) return w @@ -874,7 +874,7 @@ def topics_view(self, stream_button: Any) -> Any: rline="", lline="", bline="", - brcorner="─", + brcorner="", ) return w @@ -1941,10 +1941,10 @@ def __init__( ) search_box = urwid.LineBox( self.emoji_search, - tlcorner="─", + tlcorner="", tline="", lline="", - trcorner="─", + trcorner="", blcorner="─", rline="", bline="─", From 20eebe0e559c2f3bb2abffcf974a50fce87f2610 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 8 May 2023 23:16:52 -0700 Subject: [PATCH 063/276] refactor: symbols/views: Extract SECTION_DIVIDER_LINE into symbols. --- zulipterminal/config/symbols.py | 1 + zulipterminal/ui_tools/views.py | 31 ++++++++++++++++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/zulipterminal/config/symbols.py b/zulipterminal/config/symbols.py index e12aba46dc..4782f4c500 100644 --- a/zulipterminal/config/symbols.py +++ b/zulipterminal/config/symbols.py @@ -31,6 +31,7 @@ PINNED_STREAMS_DIVIDER = "-" # HYPHEN-MINUS, U+002D (Basic latin) COLUMN_TITLE_BAR_LINE = "━" # BOX DRAWINGS HEAVY HORIZONTAL, U+2501 (Box drawing) COLUMN_DIVIDER_LINE = "│" # BOX DRAWINGS LIGHT VERTICAL, U+2502 (Box drawing) +SECTION_DIVIDER_LINE = "─" # BOX DRAWINGS LIGHT HORIZONTAL, U+2500 (Box drawing) # NOTE: The '⏱' emoji needs an extra space while rendering. Otherwise, it # appears to overlap its subsequent text. diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 53cf3778f1..49b30dd4f9 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -23,6 +23,7 @@ CHECK_MARK, COLUMN_TITLE_BAR_LINE, PINNED_STREAMS_DIVIDER, + SECTION_DIVIDER_LINE, ) from zulipterminal.config.ui_mappings import ( BOT_TYPE_BY_ID, @@ -325,10 +326,10 @@ def __init__(self, streams_btn_list: List[Any], view: Any) -> None: tline="", lline="", trcorner="", - blcorner="─", + blcorner=SECTION_DIVIDER_LINE, rline="", - bline="─", - brcorner="─", + bline=SECTION_DIVIDER_LINE, + brcorner=SECTION_DIVIDER_LINE, ), ) self.search_lock = threading.Lock() @@ -420,7 +421,11 @@ def __init__( self, "SEARCH_TOPICS", self.update_topics ) self.header_list = urwid.Pile( - [self.stream_button, urwid.Divider("─"), self.topic_search_box] + [ + self.stream_button, + urwid.Divider(SECTION_DIVIDER_LINE), + self.topic_search_box, + ] ) super().__init__( self.list_box, @@ -430,10 +435,10 @@ def __init__( tline="", lline="", trcorner="", - blcorner="─", + blcorner=SECTION_DIVIDER_LINE, rline="", - bline="─", - brcorner="─", + bline=SECTION_DIVIDER_LINE, + brcorner=SECTION_DIVIDER_LINE, ), ) self.search_lock = threading.Lock() @@ -644,10 +649,10 @@ def __init__(self, view: Any) -> None: tline="", lline="", trcorner="", - blcorner="─", + blcorner=SECTION_DIVIDER_LINE, rline="", - bline="─", - brcorner="─", + bline=SECTION_DIVIDER_LINE, + brcorner=SECTION_DIVIDER_LINE, ) self.allow_update_user_list = True self.search_lock = threading.Lock() @@ -1945,10 +1950,10 @@ def __init__( tline="", lline="", trcorner="", - blcorner="─", + blcorner=SECTION_DIVIDER_LINE, rline="", - bline="─", - brcorner="─", + bline=SECTION_DIVIDER_LINE, + brcorner=SECTION_DIVIDER_LINE, ) self.empty_search = False self.search_lock = threading.Lock() From 47455bb2052183172ceed5a697eb812d9214ceb5 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 9 May 2023 10:57:31 -0700 Subject: [PATCH 064/276] refactor: views: Use Dividers in Piles instead of LineBoxes. In these cases we wish a solid line below, so it is simpler to use Dividers in Piles. Tests updated. --- tests/ui/test_ui_tools.py | 11 ++++++-- zulipterminal/ui_tools/views.py | 50 ++++++--------------------------- 2 files changed, 16 insertions(+), 45 deletions(-) diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index 03d56f7554..c04c9c3e2a 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -606,7 +606,12 @@ def test_init(self, mocker, topic_view): topic_view, "SEARCH_TOPICS", topic_view.update_topics ) self.header_list.assert_called_once_with( - [topic_view.stream_button, self.divider("─"), topic_view.topic_search_box] + [ + topic_view.stream_button, + self.divider("─"), + topic_view.topic_search_box, + self.divider("─"), + ] ) @pytest.mark.parametrize( @@ -953,7 +958,7 @@ def mock_external_classes(self, mocker): self.view = mocker.Mock() self.user_search = mocker.patch(VIEWS + ".PanelSearchBox") self.connect_signal = mocker.patch(VIEWS + ".urwid.connect_signal") - self.line_box = mocker.patch(VIEWS + ".urwid.LineBox") + self.pile = mocker.patch(VIEWS + ".urwid.Pile") self.thread = mocker.patch(VIEWS + ".threading") self.super = mocker.patch(VIEWS + ".urwid.Frame.__init__") self.view.model.unread_counts = { # Minimal, though an UnreadCounts @@ -976,7 +981,7 @@ def test_init(self, right_col_view): assert right_col_view.search_lock == self.thread.Lock() self.super.assert_called_once_with( right_col_view.users_view(), - header=self.line_box(right_col_view.user_search), + header=self.pile(right_col_view.user_search), ) def test_update_user_list_editor_mode(self, mocker, right_col_view): diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 49b30dd4f9..e27500a75d 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -320,16 +320,8 @@ def __init__(self, streams_btn_list: List[Any], view: Any) -> None: ) super().__init__( list_box, - header=urwid.LineBox( - self.stream_search_box, - tlcorner="", - tline="", - lline="", - trcorner="", - blcorner=SECTION_DIVIDER_LINE, - rline="", - bline=SECTION_DIVIDER_LINE, - brcorner=SECTION_DIVIDER_LINE, + header=urwid.Pile( + [self.stream_search_box, urwid.Divider(SECTION_DIVIDER_LINE)] ), ) self.search_lock = threading.Lock() @@ -425,21 +417,12 @@ def __init__( self.stream_button, urwid.Divider(SECTION_DIVIDER_LINE), self.topic_search_box, + urwid.Divider(SECTION_DIVIDER_LINE), ] ) super().__init__( self.list_box, - header=urwid.LineBox( - self.header_list, - tlcorner="", - tline="", - lline="", - trcorner="", - blcorner=SECTION_DIVIDER_LINE, - rline="", - bline=SECTION_DIVIDER_LINE, - brcorner=SECTION_DIVIDER_LINE, - ), + header=self.header_list, ) self.search_lock = threading.Lock() self.empty_search = False @@ -643,17 +626,8 @@ def __init__(self, view: Any) -> None: self.view = view self.user_search = PanelSearchBox(self, "SEARCH_PEOPLE", self.update_user_list) self.view.user_search = self.user_search - search_box = urwid.LineBox( - self.user_search, - tlcorner="", - tline="", - lline="", - trcorner="", - blcorner=SECTION_DIVIDER_LINE, - rline="", - bline=SECTION_DIVIDER_LINE, - brcorner=SECTION_DIVIDER_LINE, - ) + search_box = urwid.Pile([self.user_search, urwid.Divider(SECTION_DIVIDER_LINE)]) + self.allow_update_user_list = True self.search_lock = threading.Lock() self.empty_search = False @@ -1944,16 +1918,8 @@ def __init__( self.emoji_search = PanelSearchBox( self, "SEARCH_EMOJIS", self.update_emoji_list ) - search_box = urwid.LineBox( - self.emoji_search, - tlcorner="", - tline="", - lline="", - trcorner="", - blcorner=SECTION_DIVIDER_LINE, - rline="", - bline=SECTION_DIVIDER_LINE, - brcorner=SECTION_DIVIDER_LINE, + search_box = urwid.Pile( + [self.emoji_search, urwid.Divider(SECTION_DIVIDER_LINE)] ) self.empty_search = False self.search_lock = threading.Lock() From 273b408a13b23b068e055adf5943fbde4fc1be88 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 13 Apr 2023 13:14:05 -0700 Subject: [PATCH 065/276] refactor: messages: Improve typing of reactions_view return value. --- zulipterminal/ui_tools/messages.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index 583b9c5883..021f435434 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -248,9 +248,11 @@ def top_search_bar(self) -> Any: header.markup = title_markup return header - def reactions_view(self, reactions: List[Dict[str, Any]]) -> Any: + def reactions_view( + self, reactions: List[Dict[str, Any]] + ) -> Optional[urwid.Padding]: if not reactions: - return "" + return None try: my_user_id = self.model.user_id reaction_stats = defaultdict(list) @@ -293,7 +295,7 @@ def reactions_view(self, reactions: List[Dict[str, Any]]) -> Any: min_width=50, ) except Exception: - return "" + return None @staticmethod def footlinks_view( @@ -767,7 +769,7 @@ def main_view(self) -> List[Any]: (content_header, any_differences), (wrapped_content, True), (footlinks, footlinks is not None), - (reactions, reactions != ""), + (reactions, reactions is not None), ] self.header = [part for part, condition in parts[:2] if condition] From 83fb1e6118aca781da6392ee781ce8739920421f Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 13 May 2023 18:38:50 -0700 Subject: [PATCH 066/276] refactor: tests: model: Remove old test with limited use. --- tests/model/test_model.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index e48f672177..7a5b6f4050 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -1558,25 +1558,6 @@ def test_mark_message_ids_as_read_empty_message_view(self, model) -> None: def test__update_initial_data(self, model, initial_data): assert model.initial_data == initial_data - def test__update_initial_data_raises_exception(self, mocker, initial_data): - # Initialize Model - mocker.patch(MODEL + ".get_messages", return_value="") - mocker.patch(MODEL + ".get_all_users", return_value=[]) - mocker.patch(MODEL + "._subscribe_to_streams") - self.classify_unread_counts = mocker.patch( - MODULE + ".classify_unread_counts", return_value=[] - ) - - # Setup mocks before calling get_messages - self.client.register.return_value = initial_data - self.client.get_members.return_value = {"members": initial_data["realm_users"]} - model = Model(self.controller) - - # Test if raises Exception - self.client.register.side_effect = Exception() - with pytest.raises(Exception): - model._update_initial_data() - def test__group_info_from_realm_user_groups(self, model, user_groups_fixture): user_group_names = model._group_info_from_realm_user_groups(user_groups_fixture) assert model.user_group_by_id == { From b660acba9938b160b992ef4e11881579099e1b9b Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 23 Apr 2023 11:53:31 -0700 Subject: [PATCH 067/276] bugfix: tests: Correct bad asserts allowed by mock. Picked up by ruff. --- tests/cli/test_run.py | 4 ++-- tests/model/test_model.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 9d4a7219ad..ea2b7752f1 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -99,7 +99,7 @@ def test_get_server_settings( result = get_server_settings(realm_url) - assert mocked_get.called_once_with(url=realm_url + "/api/v1/server_settings") + mocked_get.assert_called_once_with(url=realm_url + "/api/v1/server_settings") assert result == server_settings_minimal @@ -114,7 +114,7 @@ def test_get_server_settings__not_a_zulip_organization( with pytest.raises(NotAZulipOrganizationError) as exc: get_server_settings(realm_url) - assert mocked_get.called_once_with(url=realm_url + "/api/v1/server_settings") + mocked_get.assert_called_once_with(url=realm_url + "/api/v1/server_settings") assert str(exc.value) == realm_url diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 7a5b6f4050..2319069e23 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -1953,7 +1953,7 @@ def test__update_topic_index( ), # message_fixture sender_id is 5140 (5179, {"flags": ["mentioned"]}, False, ["stream", "private"]), (5179, {"flags": ["wildcard_mentioned"]}, False, ["stream", "private"]), - (5179, {"flags": []}, True, ["stream"]), + (5179, {"flags": []}, True, ["stream", "private"]), (5179, {"flags": []}, False, ["private"]), ], ids=[ @@ -1998,7 +1998,7 @@ def test_notify_users_calling_msg_type( # TODO: Test message content too? notify.assert_called_once_with(title, mocker.ANY) else: - notify.assert_not_called + notify.assert_not_called() @pytest.mark.parametrize( "content, expected_notification_text", From 600a684358343aad33507a47babb925d48416b83 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 23 Apr 2023 12:43:38 -0700 Subject: [PATCH 068/276] requirements[dev]: Upgrade ruff from ==0.0.253 to ==0.0.267. Ignore PLC1901 (Falsey comparisons). --- pyproject.toml | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7a4c6b7f8d..bd0dec8fe6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,6 +113,7 @@ ignore = [ "B007", # Loop control variable not used within the loop body [informative!] "C408", # Unnecessary 'dict' call (rewrite as a literal) "N818", # Exception name should be named with an Error suffix + "PLC1901", # Allow explicit falsey checks in tests, eg. x == "" vs not x "SIM108", # Prefer ternary operator over if-else (as per zulip/zulip) "UP015", # Unnecessary open mode parameters [informative?] ] diff --git a/setup.py b/setup.py index 2c879056d8..ccf3fdf281 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ def long_description(): linting_deps = [ "isort~=5.11.0", "black~=23.0", - "ruff==0.0.253", + "ruff==0.0.267", "codespell[toml]~=2.2.2", "typos~=1.13.20", ] From 7e499916a5c7dc3baa0e558ecd6a5d2c1093ea78 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 13 May 2023 21:27:36 -0700 Subject: [PATCH 069/276] requirements[dev]: Upgrade mypy from ~=1.0.0 to ~=1.3.0. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ccf3fdf281..792f7178cf 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def long_description(): typing_deps = [ "lxml-stubs", - "mypy~=1.0.0", + "mypy~=1.3.0", "types-pygments", "types-python-dateutil", "types-tzlocal", From e0dd41ef9420697920ec6f067a9dcd6c73508fc6 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 13 May 2023 21:47:29 -0700 Subject: [PATCH 070/276] requirements[dev]: Upgrade typos from ~=1.13.20 to ~=1.14.9. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 792f7178cf..1c465a8dc9 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ def long_description(): "black~=23.0", "ruff==0.0.267", "codespell[toml]~=2.2.2", - "typos~=1.13.20", + "typos~=1.14.9", ] typing_deps = [ From fce0da85fc632c1dbd38529bff46a3b04b3dfb7d Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 13 May 2023 23:33:57 -0700 Subject: [PATCH 071/276] pull_request_template: Suggest small terminal windows for images/videos. --- .github/pull_request_template.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 941cb5a6c5..b06d94f49a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -39,3 +39,4 @@ + From 844df99129073bf100eed8b6089c7f382608b38b Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 14 May 2023 15:03:24 -0700 Subject: [PATCH 072/276] refactor: api_types: Add urls and dividers for different event types. --- zulipterminal/api_types.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 4e26e5388d..55e5d87533 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -257,12 +257,16 @@ class RealmUser(TypedDict): # (also helper data structures not used elsewhere) +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#message class MessageEvent(TypedDict): type: Literal["message"] message: Message flags: List[MessageFlag] +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#update_message class UpdateMessageEvent(TypedDict): type: Literal["update_message"] message_id: int @@ -277,6 +281,8 @@ class UpdateMessageEvent(TypedDict): stream_id: int +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#reaction-add and -remove class ReactionEvent(TypedDict): type: Literal["reaction"] op: str @@ -287,6 +293,8 @@ class ReactionEvent(TypedDict): message_id: int +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#realm_user-add and -remove class RealmUserEventPerson(TypedDict): user_id: int @@ -320,6 +328,9 @@ class RealmUserEvent(TypedDict): person: RealmUserEventPerson +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#subscription-update +# (also -peer_add and -peer_remove; FIXME: -add & -remove are not yet supported) class SubscriptionEvent(TypedDict): type: Literal["subscription"] op: str @@ -335,12 +346,16 @@ class SubscriptionEvent(TypedDict): message_ids: List[int] # Present when subject of msg(s) is updated +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#typing-start and -stop class TypingEvent(TypedDict): type: Literal["typing"] sender: Dict[str, Any] # 'email', ... op: str +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#update_message_flags-add and -remove class UpdateMessageFlagsEvent(TypedDict): type: Literal["update_message_flags"] messages: List[int] @@ -350,12 +365,16 @@ class UpdateMessageFlagsEvent(TypedDict): all: bool +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#update_display_settings class UpdateDisplaySettings(TypedDict): type: Literal["update_display_settings"] setting_name: str setting: bool +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#realm_emoji-update class RealmEmojiData(TypedDict): id: str name: str @@ -370,6 +389,8 @@ class UpdateRealmEmojiEvent(TypedDict): realm_emoji: Dict[str, RealmEmojiData] +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#user_settings-update # This is specifically only those supported by ZT SupportedUserSettings = Literal["send_private_typing_notifications"] @@ -381,6 +402,8 @@ class UpdateUserSettingsEvent(TypedDict): value: Any +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#update_global_notifications # This is specifically only those supported by ZT SupportedGlobalNotificationSettings = Literal["pm_content_in_desktop_notifications"] @@ -391,6 +414,7 @@ class UpdateGlobalNotificationsEvent(TypedDict): setting: Any +# ----------------------------------------------------------------------------- Event = Union[ MessageEvent, UpdateMessageEvent, From 8fb55e5ccc0f77e23bb79e07cafe1eb82b23394c Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 14 May 2023 14:04:12 -0700 Subject: [PATCH 073/276] refactor: api_types: Improve typing of TypingEvent. This uses TypingStatusChange from 10e02d0f49a77c, and introduces _TypingEventUser for a nested dict. --- zulipterminal/api_types.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 55e5d87533..b71fb99efe 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -348,10 +348,23 @@ class SubscriptionEvent(TypedDict): # ----------------------------------------------------------------------------- # See https://zulip.com/api/get-events#typing-start and -stop +class _TypingEventUser(TypedDict): + user_id: int + email: str + + class TypingEvent(TypedDict): type: Literal["typing"] - sender: Dict[str, Any] # 'email', ... - op: str + op: TypingStatusChange + sender: _TypingEventUser + + # Unused as yet + # Pre Zulip 4.0, always present; now only present if message_type == "private" + # recipients: List[_TypingEventUser] + # NOTE: These fields are all new in Zulip 4.0 / ZFL 58, if client capability sent + # message_type: NotRequired[MessageType] + # stream_id: NotRequired[int] # Only present if message_type == "stream" + # topic: NotRequired[str] # Only present if message_type == "stream" # ----------------------------------------------------------------------------- From ba84bce8b99445d96886818c80cbc39ed4e53d96 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 14 May 2023 14:58:46 -0700 Subject: [PATCH 074/276] refactor: api_types/model: Improve typing of message flag updates. This explicitly types the data passed to update_message_flags as MessagesFlagChange, and introduces MessageFlagStatusChange as valid literals for the flag change operation (op), which is also typed in the corresponding UpdateMessageFlagsEvent. --- zulipterminal/api_types.py | 19 +++++++++++++++++-- zulipterminal/model.py | 13 +++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index b71fb99efe..71173a5caf 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -87,6 +87,19 @@ class StreamComposition(TypedDict): Composition = Union[PrivateComposition, StreamComposition] +############################################################################### +# Parameters to pass in request to: +# https://zulip.com/api/update-message-flags + +MessageFlagStatusChange = Literal["add", "remove"] + + +class MessagesFlagChange(TypedDict): + messages: List[int] + op: MessageFlagStatusChange + flag: ModifiableMessageFlag + + ############################################################################### # Parameter to pass in request to: # https://zulip.com/api/update-message @@ -369,11 +382,13 @@ class TypingEvent(TypedDict): # ----------------------------------------------------------------------------- # See https://zulip.com/api/get-events#update_message_flags-add and -remove + + class UpdateMessageFlagsEvent(TypedDict): type: Literal["update_message_flags"] messages: List[int] - operation: str # NOTE: deprecated in Zulip 4.0 / ZFL 32 -> 'op' - op: str + operation: MessageFlagStatusChange # NOTE: deprecated in Zulip 4.0 / ZFL 32 -> 'op' + op: MessageFlagStatusChange flag: ModifiableMessageFlag all: bool diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 9e92d058cd..0440531c3b 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -38,6 +38,7 @@ DirectTypingNotification, EditPropagateMode, Event, + MessagesFlagChange, PrivateComposition, PrivateMessageUpdateRequest, RealmEmojiData, @@ -493,11 +494,11 @@ def save_draft(self, draft: Composition) -> None: @asynch def toggle_message_star_status(self, message: Message) -> None: - base_request = dict(flag="starred", messages=[message["id"]]) + messages = [message["id"]] if "starred" in message["flags"]: - request = dict(base_request, op="remove") + request = MessagesFlagChange(messages=messages, flag="starred", op="remove") else: - request = dict(base_request, op="add") + request = MessagesFlagChange(messages=messages, flag="starred", op="add") response = self.client.update_message_flags(request) display_error_if_present(response, self.controller) @@ -506,11 +507,7 @@ def mark_message_ids_as_read(self, id_list: List[int]) -> None: if not id_list: return response = self.client.update_message_flags( - { - "messages": id_list, - "flag": "read", - "op": "add", - } + MessagesFlagChange(messages=id_list, flag="read", op="add") ) display_error_if_present(response, self.controller) From 051c65a20d9ecd4f704356aed9ccf6c62c55dde1 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 14 May 2023 15:22:49 -0700 Subject: [PATCH 075/276] refactor: api_types: Rename, move and improve UpdateDisplaySettings. This groups this older settings event with another, under the newer UserSettingsEvent. --- zulipterminal/api_types.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 71173a5caf..d24f71eb8f 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -393,14 +393,6 @@ class UpdateMessageFlagsEvent(TypedDict): all: bool -# ----------------------------------------------------------------------------- -# See https://zulip.com/api/get-events#update_display_settings -class UpdateDisplaySettings(TypedDict): - type: Literal["update_display_settings"] - setting_name: str - setting: bool - - # ----------------------------------------------------------------------------- # See https://zulip.com/api/get-events#realm_emoji-update class RealmEmojiData(TypedDict): @@ -442,6 +434,18 @@ class UpdateGlobalNotificationsEvent(TypedDict): setting: Any +# ----------------------------------------------------------------------------- +# See https://zulip.com/api/get-events#update_display_settings +# This is specifically only those supported by ZT +SupportedDisplaySettings = Literal["twenty_four_hour_time"] + + +class UpdateDisplaySettingsEvent(TypedDict): + type: Literal["update_display_settings"] + setting_name: SupportedDisplaySettings + setting: bool + + # ----------------------------------------------------------------------------- Event = Union[ MessageEvent, @@ -450,7 +454,7 @@ class UpdateGlobalNotificationsEvent(TypedDict): SubscriptionEvent, TypingEvent, UpdateMessageFlagsEvent, - UpdateDisplaySettings, + UpdateDisplaySettingsEvent, UpdateRealmEmojiEvent, UpdateUserSettingsEvent, UpdateGlobalNotificationsEvent, From 0347518640a6ffefa413709e8cfa161679de9de2 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 14 May 2023 17:15:46 -0700 Subject: [PATCH 076/276] refactor: api_types/model: Improve typing of Subscription change types. This splits the previous SubscriptionEvent into - SubscriptionUpdateEvent (op:update): personal settings - SubscriptionPeerAddRemoveEvent (op:peer_add/remove): subscribers SubscriptionSettingChange is defined for reuse both with the SubscriptionUpdateEvent and for calls to update_subscription_settings, which is applied in the model. Specific (un)supported personal subscription settings are detailed and enforced through comments and a new PersonalSubscriptionSetting Literal. A spurious message_ids field is removed, likely from when the Event object described all possible fields. --- zulipterminal/api_types.py | 45 ++++++++++++++++++++++++++++++-------- zulipterminal/model.py | 31 +++++++++++++------------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index d24f71eb8f..88abd7db59 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -131,6 +131,27 @@ class StreamMessageUpdateRequest(TypedDict): MessageUpdateRequest = Union[PrivateMessageUpdateRequest, StreamMessageUpdateRequest] +############################################################################### +# Parameter to pass in request to: +# https://zulip.com/api/update-subscription-settings + +PersonalSubscriptionSetting = Literal[ + "in_home_view", "is_muted", "pin_to_top", "desktop_notifications" +] +# Currently unsupported in ZT: +# - color # TODO: Add support (value is str not bool) +# - audible_notifications +# - push_notifications +# - email_notifications +# - wildcard_mentions_notify # TODO: Add support + + +class SubscriptionSettingChange(TypedDict): + stream_id: int + property: PersonalSubscriptionSetting + value: bool + + ############################################################################### # In "messages" response from: # https://zulip.com/api/get-messages @@ -344,19 +365,24 @@ class RealmUserEvent(TypedDict): # ----------------------------------------------------------------------------- # See https://zulip.com/api/get-events#subscription-update # (also -peer_add and -peer_remove; FIXME: -add & -remove are not yet supported) -class SubscriptionEvent(TypedDict): + + +# Update of personal properties +class SubscriptionUpdateEvent(SubscriptionSettingChange): type: Literal["subscription"] - op: str - property: str + op: Literal["update"] - user_id: int # Present when a streams subscribers are updated. - user_ids: List[int] # NOTE: replaces 'user_id' in ZFL 35 + +# User(s) have been (un)subscribed from stream(s) +class SubscriptionPeerAddRemoveEvent(TypedDict): + type: Literal["subscription"] + op: Literal["peer_add", "peer_remove"] stream_id: int - stream_ids: List[int] # NOTE: replaces 'stream_id' in ZFL 35 for peer* + stream_ids: List[int] # NOTE: replaces 'stream_id' in ZFL 35 - value: bool - message_ids: List[int] # Present when subject of msg(s) is updated + user_id: int + user_ids: List[int] # NOTE: replaces 'user_id' in ZFL 35 # ----------------------------------------------------------------------------- @@ -451,7 +477,8 @@ class UpdateDisplaySettingsEvent(TypedDict): MessageEvent, UpdateMessageEvent, ReactionEvent, - SubscriptionEvent, + SubscriptionUpdateEvent, + SubscriptionPeerAddRemoveEvent, TypingEvent, UpdateMessageFlagsEvent, UpdateDisplaySettingsEvent, diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 0440531c3b..ae1f2c32c7 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -46,6 +46,7 @@ StreamComposition, StreamMessageUpdateRequest, Subscription, + SubscriptionSettingChange, TypingStatusChange, ) from zulipterminal.config.keys import primary_key_for_command @@ -1220,12 +1221,12 @@ def _group_info_from_realm_user_groups( def toggle_stream_muted_status(self, stream_id: int) -> None: request = [ - { - "stream_id": stream_id, - "property": "is_muted", - "value": not self.is_muted_stream(stream_id) + SubscriptionSettingChange( + stream_id=stream_id, + property="is_muted", + value=not self.is_muted_stream(stream_id) # True for muting and False for unmuting. - } + ) ] response = self.client.update_subscription_settings(request) display_error_if_present(response, self.controller) @@ -1251,11 +1252,11 @@ def is_pinned_stream(self, stream_id: int) -> bool: def toggle_stream_pinned_status(self, stream_id: int) -> bool: request = [ - { - "stream_id": stream_id, - "property": "pin_to_top", - "value": not self.is_pinned_stream(stream_id), - } + SubscriptionSettingChange( + stream_id=stream_id, + property="pin_to_top", + value=not self.is_pinned_stream(stream_id), + ) ] response = self.client.update_subscription_settings(request) return response["result"] == "success" @@ -1268,11 +1269,11 @@ def is_visual_notifications_enabled(self, stream_id: int) -> bool: def toggle_stream_visual_notifications(self, stream_id: int) -> None: request = [ - { - "stream_id": stream_id, - "property": "desktop_notifications", - "value": not self.is_visual_notifications_enabled(stream_id), - } + SubscriptionSettingChange( + stream_id=stream_id, + property="desktop_notifications", + value=not self.is_visual_notifications_enabled(stream_id), + ) ] response = self.client.update_subscription_settings(request) display_error_if_present(response, self.controller) From 1565671c3171c5675de014f2a381e1164d782e37 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 16 May 2023 19:17:28 -0700 Subject: [PATCH 077/276] FAQ: Add table of contents. Given the size of the FAQ, a manual table of contents gives an overview and allow users to find what they need quicker, rather than requiring scrolling. --- docs/FAQ.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/FAQ.md b/docs/FAQ.md index e2988f9cf0..801c22a764 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,5 +1,24 @@ # Frequently Asked Questions (FAQ) +- [What Python implementations are supported?](#what-python-implementations-are-supported) +- [What Python versions are supported?](#what-python-versions-are-supported) +- [What operating systems are supported?](#what-operating-systems-are-supported) +- [What versions of Zulip are supported?](#what-versions-of-zulip-are-supported) +- [Colors appear mismatched, don't change with theme, or look strange](#colors-appear-mismatched-dont-change-with-theme-or-look-strange) +- [It doesn't seem to run or display properly in my terminal (emulator)?](#it-doesnt-seem-to-run-or-display-properly-in-my-terminal-emulator) +- [Are there any themes available, other than the default one?](#are-there-any-themes-available-other-than-the-default-one) +- [How do links in messages work? What are footlinks?](#how-do-links-in-messages-work-what-are-footlinks) +- [When are messages marked as having been read?](#when-are-messages-marked-as-having-been-read) +- [How do I access multiple servers?](#how-do-i-access-multiple-servers) +- [What is autocomplete? Why is it useful?](#what-is-autocomplete-why-is-it-useful) +- [Unable to render symbols](#unable-to-render-symbols) +- [Unable to open links](#unable-to-open-links) +- [How small a size of terminal is supported?](#how-small-a-size-of-terminal-is-supported) +- [Mouse does not support *performing some action/feature*](#mouse-does-not-support-performing-some-action-feature) +- [Hotkeys don't work as described](#hotkeys-dont-work-as-described) +- [Zulip-term crashed!](#zulip-term-crashed) +- [Something looks wrong! Where's this feature? There's a bug!](#something-looks-wrong-wheres-this-feature-theres-a-bug) + ## What Python implementations are supported? Users and developers run regularly with the "traditional" implementation of From 8cb46baa92db6749ff8607de6d3da4d0d290c0a1 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 16 May 2023 20:30:56 -0700 Subject: [PATCH 078/276] FAQ: Add structure to table of contents and reorder entries. FAQ sections are subdivided into three categories: - Supported environments - Features - Something is not working! For these categories to be grouped in this way, two sections are moved around, both in the table of contents and the list of entries. Note that the category titles are not currently inserted in the body of the FAQ. --- docs/FAQ.md | 83 +++++++++++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 801c22a764..5173b4f1d7 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,23 +1,26 @@ # Frequently Asked Questions (FAQ) -- [What Python implementations are supported?](#what-python-implementations-are-supported) -- [What Python versions are supported?](#what-python-versions-are-supported) -- [What operating systems are supported?](#what-operating-systems-are-supported) -- [What versions of Zulip are supported?](#what-versions-of-zulip-are-supported) -- [Colors appear mismatched, don't change with theme, or look strange](#colors-appear-mismatched-dont-change-with-theme-or-look-strange) -- [It doesn't seem to run or display properly in my terminal (emulator)?](#it-doesnt-seem-to-run-or-display-properly-in-my-terminal-emulator) -- [Are there any themes available, other than the default one?](#are-there-any-themes-available-other-than-the-default-one) -- [How do links in messages work? What are footlinks?](#how-do-links-in-messages-work-what-are-footlinks) -- [When are messages marked as having been read?](#when-are-messages-marked-as-having-been-read) -- [How do I access multiple servers?](#how-do-i-access-multiple-servers) -- [What is autocomplete? Why is it useful?](#what-is-autocomplete-why-is-it-useful) -- [Unable to render symbols](#unable-to-render-symbols) -- [Unable to open links](#unable-to-open-links) -- [How small a size of terminal is supported?](#how-small-a-size-of-terminal-is-supported) -- [Mouse does not support *performing some action/feature*](#mouse-does-not-support-performing-some-action-feature) -- [Hotkeys don't work as described](#hotkeys-dont-work-as-described) -- [Zulip-term crashed!](#zulip-term-crashed) -- [Something looks wrong! Where's this feature? There's a bug!](#something-looks-wrong-wheres-this-feature-theres-a-bug) +- Supported environments + - [What Python implementations are supported?](#what-python-implementations-are-supported) + - [What Python versions are supported?](#what-python-versions-are-supported) + - [What operating systems are supported?](#what-operating-systems-are-supported) + - [What versions of Zulip are supported?](#what-versions-of-zulip-are-supported) + - [It doesn't seem to run or display properly in my terminal (emulator)?](#it-doesnt-seem-to-run-or-display-properly-in-my-terminal-emulator) + - [How small a size of terminal is supported?](#how-small-a-size-of-terminal-is-supported) +- Features + - [Are there any themes available, other than the default one?](#are-there-any-themes-available-other-than-the-default-one) + - [How do links in messages work? What are footlinks?](#how-do-links-in-messages-work-what-are-footlinks) + - [When are messages marked as having been read?](#when-are-messages-marked-as-having-been-read) + - [How do I access multiple servers?](#how-do-i-access-multiple-servers) + - [What is autocomplete? Why is it useful?](#what-is-autocomplete-why-is-it-useful) +- Something is not working! + - [Colors appear mismatched, don't change with theme, or look strange](#colors-appear-mismatched-dont-change-with-theme-or-look-strange) + - [Unable to render symbols](#unable-to-render-symbols) + - [Unable to open links](#unable-to-open-links) + - [Mouse does not support *performing some action/feature*](#mouse-does-not-support-performing-some-actionfeature) + - [Hotkeys don't work as described](#hotkeys-dont-work-as-described) + - [Zulip-term crashed!](#zulip-term-crashed) + - [Something looks wrong! Where's this feature? There's a bug!](#something-looks-wrong-wheres-this-feature-theres-a-bug) ## What Python implementations are supported? @@ -88,12 +91,6 @@ Note that a subset of features in more recent Zulip versions are supported, and could in some cases be present when using this client, particularly if the feature relies upon a client-side implementation. -## Colors appear mismatched, don't change with theme, or look strange - -Some terminal emulators support specifying custom colors, or custom color schemes. If you do this then this can override the colors that Zulip Terminal attempts to use. - -**NOTE** If you have color issues, also note that urwid version 2.1.1 should have fixed these for various terminal emulators (including rxvt, urxvt, mosh and Terminal.app), so please ensure you are running the latest Zulip Terminal release and at least this urwid version before reporting issues. - ## It doesn't seem to run or display properly in my terminal (emulator)? We have reports of success on the following terminal emulators: @@ -119,6 +116,22 @@ Issues have been reported with the following: Please let us know if you have feedback on the success or failure in these or any other terminal emulator! +## How small a size of terminal is supported? + +While most users likely do not use such sizes, we aim to support sizes from the standard 80 columns by 24 rows upwards. + +If you use a width from approximately 100 columns upwards, everything is expected to work as documented. + +However, since we currently use a fixed width for the left and right side panels, for widths from approximately 80-100 columns the message list can become too narrow. +In these situations we recommend using the `autohide` option in your configuration file (see [configuration file](https://github.com/zulip/zulip-terminal/#configuration) notes) or on the command-line in a particular session via `--autohide`. + +If you use a small terminal size (or simply read long messages), you may find +it useful to read a message which is too long to fit in the window by opening +the Message Information (i) for a message and scrolling through the Full +rendered message (f). + +If you experience problems related to small sizes that are not resolved using the above, please check [#1005](https://www.github.com/zulip/zulip-terminal/issues/1005)) for any unresolved such issues and report them there. + ## Are there any themes available, other than the default one? Yes. There are five supported themes: @@ -294,6 +307,12 @@ Since each of the stream (1), topic (2) and direct message recipients (3) areas **NOTE:** If a direct message recipient's name contains comma(s) (`,`), they are currently treated as comma-separated recipients. +## Colors appear mismatched, don't change with theme, or look strange + +Some terminal emulators support specifying custom colors, or custom color schemes. If you do this then this can override the colors that Zulip Terminal attempts to use. + +**NOTE** If you have color issues, also note that urwid version 2.1.1 should have fixed these for various terminal emulators (including rxvt, urxvt, mosh and Terminal.app), so please ensure you are running the latest Zulip Terminal release and at least this urwid version before reporting issues. + ## Unable to render symbols If some symbols don't render properly on your terminal, it could likely be because of the symbols not being supported on your terminal emulator and/or font. @@ -316,22 +335,6 @@ If you are still facing problems, please discuss them at [#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal) or open issues for them mentioning your terminal name, version, and OS. -## How small a size of terminal is supported? - -While most users likely do not use such sizes, we aim to support sizes from the standard 80 columns by 24 rows upwards. - -If you use a width from approximately 100 columns upwards, everything is expected to work as documented. - -However, since we currently use a fixed width for the left and right side panels, for widths from approximately 80-100 columns the message list can become too narrow. -In these situations we recommend using the `autohide` option in your configuration file (see [configuration file](https://github.com/zulip/zulip-terminal/#configuration) notes) or on the command-line in a particular session via `--autohide`. - -If you use a small terminal size (or simply read long messages), you may find -it useful to read a message which is too long to fit in the window by opening -the Message Information (i) for a message and scrolling through the Full -rendered message (f). - -If you experience problems related to small sizes that are not resolved using the above, please check [#1005](https://www.github.com/zulip/zulip-terminal/issues/1005)) for any unresolved such issues and report them there. - ## Mouse does not support *performing some action/feature* We think of Zulip Terminal as a keyboard-centric client. Consequently, while functionality via the mouse does work in places, mouse support is not currently a priority for the project (see also [#248](https://www.github.com/zulip/zulip-terminal/issues/248)). From 2193b677153c610c306ea31bd2da8e45c3c8a4b3 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 17 May 2023 10:37:23 -0700 Subject: [PATCH 079/276] FAQ: Adjust section titles to improve detail & consistency. --- docs/FAQ.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index 5173b4f1d7..f3d0d8eb82 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -5,7 +5,7 @@ - [What Python versions are supported?](#what-python-versions-are-supported) - [What operating systems are supported?](#what-operating-systems-are-supported) - [What versions of Zulip are supported?](#what-versions-of-zulip-are-supported) - - [It doesn't seem to run or display properly in my terminal (emulator)?](#it-doesnt-seem-to-run-or-display-properly-in-my-terminal-emulator) + - [Will it run and display properly in my terminal (emulator)?](#will-it-run-and-display-properly-in-my-terminal-emulator) - [How small a size of terminal is supported?](#how-small-a-size-of-terminal-is-supported) - Features - [Are there any themes available, other than the default one?](#are-there-any-themes-available-other-than-the-default-one) @@ -15,7 +15,7 @@ - [What is autocomplete? Why is it useful?](#what-is-autocomplete-why-is-it-useful) - Something is not working! - [Colors appear mismatched, don't change with theme, or look strange](#colors-appear-mismatched-dont-change-with-theme-or-look-strange) - - [Unable to render symbols](#unable-to-render-symbols) + - [Symbols look different to in the provided screenshots, or just look incorrect](#symbols-look-different-to-in-the-provided-screenshots-or-just-look-incorrect) - [Unable to open links](#unable-to-open-links) - [Mouse does not support *performing some action/feature*](#mouse-does-not-support-performing-some-actionfeature) - [Hotkeys don't work as described](#hotkeys-dont-work-as-described) @@ -91,7 +91,7 @@ Note that a subset of features in more recent Zulip versions are supported, and could in some cases be present when using this client, particularly if the feature relies upon a client-side implementation. -## It doesn't seem to run or display properly in my terminal (emulator)? +## Will it run and display properly in my terminal (emulator)? We have reports of success on the following terminal emulators: @@ -313,7 +313,7 @@ Some terminal emulators support specifying custom colors, or custom color scheme **NOTE** If you have color issues, also note that urwid version 2.1.1 should have fixed these for various terminal emulators (including rxvt, urxvt, mosh and Terminal.app), so please ensure you are running the latest Zulip Terminal release and at least this urwid version before reporting issues. -## Unable to render symbols +## Symbols look different to in the provided screenshots, or just look incorrect If some symbols don't render properly on your terminal, it could likely be because of the symbols not being supported on your terminal emulator and/or font. From 754254039d964ddedc71190dda9a015668bacab9 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 17 May 2023 11:23:16 -0700 Subject: [PATCH 080/276] refactor: FAQ: Split long lines and sentences. This does not modify content, only shortens lines to make diffs easier to read. Visually compared to previous version as rendered on GitHub to ensure no changes. --- docs/FAQ.md | 236 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 164 insertions(+), 72 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index f3d0d8eb82..b7501883e9 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -110,27 +110,42 @@ We have reports of success on the following terminal emulators: Issues have been reported with the following: * (**major**) terminal app **Mac only** - - Issues with some default keypresses, including for sending messages [zulip-terminal#680](https://github.com/zulip/zulip-terminal/issues/680) -* (**minor**) Microsoft/Windows Terminal (https://github.com/Microsoft/Terminal) **Windows only** - - Bold text isn't actually bold, it either renders the same as normal text or renders in a different colour [microsoft/terminal#109](https://github.com/microsoft/terminal/issues/109) + - Issues with some default keypresses, including for sending messages + [zulip-terminal#680](https://github.com/zulip/zulip-terminal/issues/680) +* (**minor**) Microsoft/Windows Terminal + (https://github.com/Microsoft/Terminal) **Windows only** + - Bold text isn't actually bold, it either renders the same as normal text or + renders in a different colour + [microsoft/terminal#109](https://github.com/microsoft/terminal/issues/109) -Please let us know if you have feedback on the success or failure in these or any other terminal emulator! +Please let us know if you have feedback on the success or failure in these or +any other terminal emulator! ## How small a size of terminal is supported? -While most users likely do not use such sizes, we aim to support sizes from the standard 80 columns by 24 rows upwards. +While most users likely do not use such sizes, we aim to support sizes from the +standard 80 columns by 24 rows upwards. -If you use a width from approximately 100 columns upwards, everything is expected to work as documented. +If you use a width from approximately 100 columns upwards, everything is +expected to work as documented. -However, since we currently use a fixed width for the left and right side panels, for widths from approximately 80-100 columns the message list can become too narrow. -In these situations we recommend using the `autohide` option in your configuration file (see [configuration file](https://github.com/zulip/zulip-terminal/#configuration) notes) or on the command-line in a particular session via `--autohide`. +However, since we currently use a fixed width for the left and right side +panels, for widths from approximately 80-100 columns the message list can +become too narrow. +In these situations we recommend using the `autohide` option in your +configuration file (see [configuration +file](https://github.com/zulip/zulip-terminal/#configuration) notes) or on the +command-line in a particular session via `--autohide`. If you use a small terminal size (or simply read long messages), you may find it useful to read a message which is too long to fit in the window by opening -the Message Information (i) for a message and scrolling through the Full -rendered message (f). +the Message Information (i) for a message and scrolling through the +Full rendered message (f). -If you experience problems related to small sizes that are not resolved using the above, please check [#1005](https://www.github.com/zulip/zulip-terminal/issues/1005)) for any unresolved such issues and report them there. +If you experience problems related to small sizes that are not resolved using +the above, please check +[#1005](https://www.github.com/zulip/zulip-terminal/issues/1005)) for any +unresolved such issues and report them there. ## Are there any themes available, other than the default one? @@ -141,24 +156,30 @@ Yes. There are five supported themes: - `zt_light` (alias: `light`) - `zt_blue` (alias: `blue`) -You can specify one of them on the command-line using the command-line option `--theme ` or `-t ` (where _theme_ is the name of the theme, or its alias). You can also specify it in the `zuliprc` file like this: +You can specify one of them on the command-line using the command-line option +`--theme ` or `-t ` (where _theme_ is the name of the theme, or +its alias). +You can also specify it in the `zuliprc` file like this: ``` [zterm] theme= ``` (where _theme_name_ is the name of theme or its alias). -**NOTE** Theme aliases are likely to be deprecated in the future, so we recommend using the full theme names. +**NOTE** Theme aliases are likely to be deprecated in the future, so we +recommend using the full theme names. ## How do links in messages work? What are footlinks? -Each link (hyperlink) in a Zulip message resembles those on the internet, and is split into two parts: -- the representation a user would see on the web page (eg. a textual description) +Each link (hyperlink) in a Zulip message resembles those on the internet, and +is split into two parts: +- the representation a user would see on the web page (eg. a textual + description) - the location the link would go to (if clicking, in a GUI web browser) To avoid squashing these two parts next to each other within the message -content, ZT places only the first within the content, followed by a number in square -brackets, eg. `Zulip homepage [1]`. +content, ZT places only the first within the content, followed by a number in +square brackets, eg. `Zulip homepage [1]`. Underneath the message content, each location is then listed next to the related number, eg. `1: zulip.com`. Within ZT we term these **footlinks**, @@ -199,8 +220,9 @@ content. The same scrolling keys as used elsewhere in the application can be used in this popup, and you may notice as you move that different lines of the popup -will be highlighted. If a link is highlighted and you press Enter, -an action may occur depending on the type of link: +will be highlighted. +If a link is highlighted and you press Enter, an action may occur +depending on the type of link: - *Attachments to a particular message* (eg. images, text files, pdfs, etc) * will be downloaded, with an option given to open it with your default application (from version 0.7.0) @@ -211,14 +233,14 @@ an action may occur depending on the type of link: * no internal action is supported at this time Any method supported by your terminal emulator to select and copy text should -also be suitable to extract these links. Some emulators can identify links to -open, and may do so in simple cases; however, while the popup is wider than the -message column, it will not fit all lengths of links, and so can fail in the -multiline case (see -[#622](https://www.github.com/zulip/zulip-terminal/issues/622)). Some emulators -may support area selection, as opposed to selecting multiple lines of the -terminal, but it's unclear how common this support is or if it converts such -text into one line suitable for use as a link. +also be suitable to extract these links. +Some emulators can identify links to open, and may do so in simple cases; +however, while the popup is wider than the message column, it will not fit all +lengths of links, and so can fail in the multiline case (see +[#622](https://www.github.com/zulip/zulip-terminal/issues/622)). +Some emulators may support area selection, as opposed to selecting multiple +lines of the terminal, but it's unclear how common this support is or if it +converts such text into one line suitable for use as a link. ## When are messages marked as having been read? @@ -226,61 +248,101 @@ The approach currently taken is that that a message is marked read when * it has been selected *or* * it is a message that you have sent -Unlike the web and mobile apps, we **don't** currently mark as read based on visibility, eg. if you have a message selected and all newer messages are also visible. This makes the marking-read process more explicit, balanced against needing to scroll through messages to mark them. Our styling is intended to promote moving onto unread messages to more easily read them. - -In contrast, like with the web app, we don't mark messages as read while in a search - but if you go to a message visible in a search within a topic or stream context then it will be marked as read, just like normal. - -An additional feature to other front-ends is **explore mode**, which can be enabled when starting the application (with `-e` or `--explore`); this allows browsing through all your messages and interacting within the application like normal, with the exception that messages are never marked as read. Other than providing a means to test the application with no change in state (ie. *explore* it), this can be useful to scan through your messages quickly when you intend to return to look at them properly later. +Unlike the web and mobile apps, we **don't** currently mark as read based on +visibility, eg. if you have a message selected and all newer messages are also +visible. +This makes the marking-read process more explicit, balanced against needing to +scroll through messages to mark them. +Our styling is intended to promote moving onto unread messages to more easily +read them. + +In contrast, like with the web app, we don't mark messages as read while in a +search - but if you go to a message visible in a search within a topic or +stream context then it will be marked as read, just like normal. + +An additional feature to other front-ends is **explore mode**, which can be +enabled when starting the application (with `-e` or `--explore`); this allows +browsing through all your messages and interacting within the application like +normal, with the exception that messages are never marked as read. +Other than providing a means to test the application with no change in state +(ie. *explore* it), this can be useful to scan through your messages quickly +when you intend to return to look at them properly later. ## How do I access multiple servers? -One session of Zulip Terminal represents a connection of one user to one Zulip server. Each session refers to a zuliprc file to identify how to connect to the server, so by running Zulip Terminal and specifying a different zuliprc file (using `-c` or `--config-file`), you may connect to a different server. You might choose to do that after exiting from one Zulip Terminal session, or open another terminal and run it concurrently there. - -Since we expect the above to be straightforward for most users and it allows the code to remain dramatically simpler, we are unlikely to support multiple Zulip servers within the same session in at least the short/medium term. -However, we are certainly likely to move towards a system to make access of the different servers simpler, which should be made easier through work such as [zulip-terminal#678](https://github.com/zulip/zulip-terminal/issues/678). -In the longer term we may move to multiple servers per session, which is tracked in [zulip-terminal#961](https://github.com/zulip/zulip-terminal/issues/961). +One session of Zulip Terminal represents a connection of one user to one Zulip +server. +Each session refers to a zuliprc file to identify how to connect to the server, +so by running Zulip Terminal and specifying a different zuliprc file (using +`-c` or `--config-file`), you may connect to a different server. +You might choose to do that after exiting from one Zulip Terminal session, or +open another terminal and run it concurrently there. + +Since we expect the above to be straightforward for most users and it allows +the code to remain dramatically simpler, we are unlikely to support multiple +Zulip servers within the same session in at least the short/medium term. +However, we are certainly likely to move towards a system to make access of the +different servers simpler, which should be made easier through work such as +[zulip-terminal#678](https://github.com/zulip/zulip-terminal/issues/678). +In the longer term we may move to multiple servers per session, which is +tracked in +[zulip-terminal#961](https://github.com/zulip/zulip-terminal/issues/961). ## What is autocomplete? Why is it useful? -Autocomplete can be used to request matching options, and cycle through each option in turn, including: +Autocomplete can be used to request matching options, and cycle through each +option in turn, including: - helping to specify users for new direct messages (eg. after x) -- helping to specify streams and existing topics for new stream messages (eg. after c) +- helping to specify streams and existing topics for new stream messages (eg. + after c) - mentioning a user or user-group (in message content) - linking to a stream or existing topic (in message content) - emojis (in message content) This helps ensure that: -- messages are sent to valid users, streams, and matching existing topics if appropriate - so they - are sent to the correct location; -- message content has references to valid users, user-groups, streams, topics and emojis, with - correct syntax - so is rendered well in all Zulip clients. +- messages are sent to valid users, streams, and matching existing topics if + appropriate - so they are sent to the correct location; +- message content has references to valid users, user-groups, streams, topics + and emojis, with correct syntax - so is rendered well in all Zulip clients. > Note that if using the left or right panels of the application to search for -> streams, topics or users, this is **not** part of the autocomplete system. In -> those cases, as you type, the results of these searches are shown +> streams, topics or users, this is **not** part of the autocomplete system. +> In those cases, as you type, the results of these searches are shown > automatically by limiting what is displayed in the existing area, until the -> search is cleared. Autocomplete operates differently, and uses the bottom -> line(s) of the screen to show a limited set of matches. +> search is cleared. +> Autocomplete operates differently, and uses the bottom line(s) of the screen +> to show a limited set of matches. ### Hotkeys triggering autocomplete -We use ctrl+**f** and ctrl+**r** for cycling through autocompletes (**forward** & **reverse** respectively). +We use ctrl+**f** and ctrl+**r** +for cycling through autocompletes (**forward** & **reverse** respectively). -**NOTE:** We don't use tab/shift+tab (although it is widely used elsewhere) for cycling through matches. However, recall that we do use tab to cycle through recipient and content boxes. (See [hotkeys for composing a message](https://github.com/zulip/zulip-terminal/blob/main/docs/hotkeys.md#composing-a-message)) +**NOTE:** We don't use tab/shift+tab (although +it is widely used elsewhere) for cycling through matches. +However, recall that we do use tab to cycle through recipient and +content boxes. (See +[hotkeys for composing a message](https://github.com/zulip/zulip-terminal/blob/main/docs/hotkeys.md#composing-a-message)) ### Example: Using autocomplete to add a recognized emoji in your message content -1. Type a prefix designated for an autocomplete (e.g., `:` for autocompleting emojis). -2. Along with the prefix, type the initial letters of the text (e.g., `air` for `airplane`). -3. Now hit the hotkey. You'd see suggestions being listed in the footer (a maximum of 10) if there are any. -4. Cycle between different suggestions as described in above hotkey section. (Notice that a selected suggestion is highlighted) -5. Cycling past the end of suggestions goes back to the prefix you entered (`:air` for this case). +1. Type a prefix designated for an autocomplete (e.g., `:` for autocompleting + emojis). +2. Along with the prefix, type the initial letters of the text (e.g., `air` for + `airplane`). +3. Now hit the hotkey. You'd see suggestions being listed in the footer (a + maximum of 10) if there are any. +4. Cycle between different suggestions as described in above hotkey section. + (Notice that a selected suggestion is highlighted) +5. Cycling past the end of suggestions goes back to the prefix you entered + (`:air` for this case). ![selected_footer_autocomplete](https://user-images.githubusercontent.com/55916430/116669526-53cfb880-a9bc-11eb-8073-11b220e6f15a.gif) ### Autocomplete in the message content -As in the example above, a specific prefix is required to indicate which action to perform (what text to insert) via the autocomplete: +As in the example above, a specific prefix is required to indicate which action +to perform (what text to insert) via the autocomplete: |Action|Prefix text(s)|Autocompleted text format| | :--- | :---: | :---: | @@ -293,7 +355,9 @@ As in the example above, a specific prefix is required to indicate which action ### Autocomplete of message recipients -Since each of the stream (1), topic (2) and direct message recipients (3) areas are very specific, no prefix must be manually entered and values provided through autocomplete depend upon the context automatically. +Since each of the stream (1), topic (2) and direct message recipients (3) areas +are very specific, no prefix must be manually entered and values provided +through autocomplete depend upon the context automatically. ![Stream header](https://user-images.githubusercontent.com/55916430/118403323-8e5b7580-b68b-11eb-9c8a-734c2fe6b774.png) @@ -305,52 +369,80 @@ Since each of the stream (1), topic (2) and direct message recipients (3) areas ![PM recipients header](https://user-images.githubusercontent.com/55916430/118403345-9d422800-b68b-11eb-9005-6d2af74adab9.png) -**NOTE:** If a direct message recipient's name contains comma(s) (`,`), they are currently treated as comma-separated recipients. +**NOTE:** If a direct message recipient's name contains comma(s) (`,`), they +are currently treated as comma-separated recipients. ## Colors appear mismatched, don't change with theme, or look strange -Some terminal emulators support specifying custom colors, or custom color schemes. If you do this then this can override the colors that Zulip Terminal attempts to use. +Some terminal emulators support specifying custom colors, or custom color +schemes. If you do this then this can override the colors that Zulip Terminal +attempts to use. -**NOTE** If you have color issues, also note that urwid version 2.1.1 should have fixed these for various terminal emulators (including rxvt, urxvt, mosh and Terminal.app), so please ensure you are running the latest Zulip Terminal release and at least this urwid version before reporting issues. +**NOTE** If you have color issues, also note that urwid version 2.1.1 should +have fixed these for various terminal emulators (including rxvt, urxvt, mosh +and Terminal.app), so please ensure you are running the latest Zulip Terminal +release and at least this urwid version before reporting issues. ## Symbols look different to in the provided screenshots, or just look incorrect -If some symbols don't render properly on your terminal, it could likely be because of the symbols not being supported on your terminal emulator and/or font. +If some symbols don't render properly on your terminal, it could likely be +because of the symbols not being supported on your terminal emulator and/or +font. -We provide a tool that you can run with the command `zulip-term-check-symbols` to check whether or not the symbols render properly on your terminal emulator and font configuration. +We provide a tool that you can run with the command `zulip-term-check-symbols` +to check whether or not the symbols render properly on your terminal emulator +and font configuration. -Ideally, you should see something similar to the following screenshot (taken on the GNOME Terminal) as a result of running the tool: +Ideally, you should see something similar to the following screenshot (taken on +the GNOME Terminal) as a result of running the tool: ![Render Symbols Screenshot](https://user-images.githubusercontent.com/60441372/115103315-9a5df580-9f6e-11eb-8c90-3b2585817d08.png) -If you are unable to observe a similar result upon running the tool, please take a screenshot and let us know about it along with your terminal and font configuration by opening an issue or at [#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal). +If you are unable to observe a similar result upon running the tool, please +take a screenshot and let us know about it along with your terminal and font +configuration by opening an issue or at +[#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal). ## Unable to open links -If you are unable to open links in messages, then try double right-click on the link. +If you are unable to open links in messages, then try double right-click on the +link. -Alternatively, you might try different modifier keys (eg. shift, ctrl, alt) with a right-click. +Alternatively, you might try different modifier keys (eg. shift, ctrl, alt) +with a right-click. If you are still facing problems, please discuss them at -[#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal) or open issues -for them mentioning your terminal name, version, and OS. +[#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal) or +open issues for them mentioning your terminal name, version, and OS. ## Mouse does not support *performing some action/feature* -We think of Zulip Terminal as a keyboard-centric client. Consequently, while functionality via the mouse does work in places, mouse support is not currently a priority for the project (see also [#248](https://www.github.com/zulip/zulip-terminal/issues/248)). +We think of Zulip Terminal as a keyboard-centric client. +Consequently, while functionality via the mouse does work in places, mouse +support is not currently a priority for the project (see also +[#248](https://www.github.com/zulip/zulip-terminal/issues/248)). ## Hotkeys don't work as described -If any of the hotkeys don't work for you, feel free to open an issue or discuss it on [#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal). +If any of the hotkeys don't work for you, feel free to open an issue or discuss +it on +[#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal). ## Zulip-term crashed! -We hope this doesn't happen, but would love to hear about this in order to fix it, since the application should be increasingly stable! Please let us know the problem, and if you're able to duplicate the issue, on the github issue-tracker or at [#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal). +We hope this doesn't happen, but would love to hear about this in order to fix +it, since the application should be increasingly stable! +Please let us know the problem, and if you're able to duplicate the issue, on +the github issue-tracker or at +[#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal). -This process would be helped if you could send us the 'traceback' showing the cause of the error, which should be output in such cases: +This process would be helped if you could send us the 'traceback' showing the +cause of the error, which should be output in such cases: * version 0.3.1 and earlier: the error is shown on the terminal; * versions 0.3.2+: the error is present/appended to the file `zulip-terminal-tracebacks.log`. ## Something looks wrong! Where's this feature? There's a bug! -Come meet us on the [#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal) stream on *chat.zulip.org*. +Come meet us on the +[#zulip-terminal](https://chat.zulip.org/#narrow/stream/206-zulip-terminal) +stream on *chat.zulip.org*. From 5a2c4d173711d21703882d65536655649b1c3d26 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 15 May 2023 19:54:07 -0700 Subject: [PATCH 081/276] refactor: api_types/model: Split & improve update_message event type(s). All fields specific to update_message events are documented, even if many are commented due to being unused at this time. This event is quite complex, with sets of fields only present when certain updates are made. To simplify the expression of this condition, the original event is split into a base type (fields always present) and two derived types (UpdateMessageContentEvent, UpdateMessagesLocationEvent). Since a given update_message event can be one or both of these types, the _handle_update_message_event method uses casting based on the presence of fields unique to each type of event. --- zulipterminal/api_types.py | 59 +++++++++++++++++++++++++++++++++----- zulipterminal/model.py | 15 ++++++---- 2 files changed, 62 insertions(+), 12 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 88abd7db59..bd3fae7785 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -301,18 +301,62 @@ class MessageEvent(TypedDict): # ----------------------------------------------------------------------------- # See https://zulip.com/api/get-events#update_message -class UpdateMessageEvent(TypedDict): +# NOTE: A single "update_message" event can be both derived event classes + + +class BaseUpdateMessageEvent(TypedDict): type: Literal["update_message"] + + # Present in both cases: + # - specific message content being updated + # - move one message (change_one) or this message and those later (change_later) message_id: int - # FIXME: These groups of types are not always present - # A: Content needs re-rendering + # Present in both cases; message_id may change read/mention/alert status + # flags: List[MessageFlag] + + # Omitted before Zulip 5.0 / ZFL 114 for rendering-only updates + # Subsequently always present (and None for rendering_only==True) + # user_id: NotRequired[Optional[int]] # sender + # edit_timestamp: NotRequired[int] + + # When True, does not relate to user-generated edit or message history + # Prior to Zulip 5.0 / ZFL 114, detect via presence/absence of user_id + # rendering_only: NotRequired[bool] # New in Zulip 5.0 / ZFL 114 + + +class UpdateMessageContentEvent(BaseUpdateMessageEvent): + # stream_name: str # Not recommended; prefer stream_id + # stream_id: NotRequired[int] # Only if a stream message + + # orig_rendered_content: str rendered_content: str - # B: Subject of these message ids needs updating? + + # Not used since we parse the rendered_content only + # orig_content: str + # content: str + + # is_me_message: bool + + +class UpdateMessagesLocationEvent(BaseUpdateMessageEvent): + # All previously sent to stream_id with topic orig_subject message_ids: List[int] + + # Old location of messages + # stream_name: str # Not recommended; prefer stream_id + stream_id: int orig_subject: str - subject: str + propagate_mode: EditPropagateMode - stream_id: int + + # Only present if messages are moved to a different topic + # eg. if subject unchanged, but stream does change, these will be absent + subject: NotRequired[str] + # subject_links: NotRequired[List[Any]] + # topic_links: NotRequired[List[Any]] + + # Only present if messages are moved to a different stream + # new_stream_id: NotRequired[int] # ----------------------------------------------------------------------------- @@ -475,7 +519,8 @@ class UpdateDisplaySettingsEvent(TypedDict): # ----------------------------------------------------------------------------- Event = Union[ MessageEvent, - UpdateMessageEvent, + UpdateMessageContentEvent, + UpdateMessagesLocationEvent, ReactionEvent, SubscriptionUpdateEvent, SubscriptionPeerAddRemoveEvent, diff --git a/zulipterminal/model.py b/zulipterminal/model.py index ae1f2c32c7..48ddf966dc 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -48,6 +48,8 @@ Subscription, SubscriptionSettingChange, TypingStatusChange, + UpdateMessageContentEvent, + UpdateMessagesLocationEvent, ) from zulipterminal.config.keys import primary_key_for_command from zulipterminal.config.symbols import STREAM_TOPIC_SEPARATOR @@ -1588,6 +1590,7 @@ def _handle_update_message_event(self, event: Event) -> None: Handle updated (edited) messages (changed content/subject) """ assert event["type"] == "update_message" + # Update edited message status from single message id # NOTE: If all messages in topic have topic edited, # they are not all marked as edited, as per server optimization @@ -1599,7 +1602,8 @@ def _handle_update_message_event(self, event: Event) -> None: # Update the rendered content, if the message is indexed if "rendered_content" in event and indexed_message: - indexed_message["content"] = event["rendered_content"] + content_event = cast(UpdateMessageContentEvent, event) + indexed_message["content"] = content_event["rendered_content"] self.index["messages"][message_id] = indexed_message self._update_rendered_view(message_id) @@ -1608,14 +1612,15 @@ def _handle_update_message_event(self, event: Event) -> None: # * 'subject' is not present in update event if # the event didn't have a 'subject' update. if "subject" in event: - new_subject = event["subject"] - stream_id = event["stream_id"] - old_subject = event["orig_subject"] + location_event = cast(UpdateMessagesLocationEvent, event) + new_subject = location_event["subject"] + stream_id = location_event["stream_id"] + old_subject = location_event["orig_subject"] msg_ids_by_topic = self.index["topic_msg_ids"][stream_id] # Remove each message_id from the old topic's `topic_msg_ids` set # if it exists, and update & re-render the message if it is indexed. - for msg_id in event["message_ids"]: + for msg_id in location_event["message_ids"]: # Ensure that the new topic is not the same as the old one # (no-op topic edit). if new_subject != old_subject: From 1f01c5f35c37b2ac68ff5bb324909a3f5f8db534 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 21 May 2023 21:53:02 -0700 Subject: [PATCH 082/276] bugfix: api_types/model: Correctly rerender when /me status changes. Previously the is_me_message flag in messages were not updated locally, if the content was updated to be recognized as such by the server. Such a flag is provided directly in update_message events (is_me_message) as in message objects fetched from the server, but was not synchronized until now. This had no effect on /me messages which were edited to lose that status, since the message rendering code did not find the matching /me message content and so updated the rendering to show as a regular message. However, for messages which were initially not /me messages, the /me rendering path could never be entered if they were subsequently edited to become /me messages, since the property of the message was not updated to allow this. Tests updated and new test cases added. --- tests/model/test_model.py | 119 ++++++++++++++++++++++++++++++++++--- zulipterminal/api_types.py | 2 +- zulipterminal/model.py | 1 + 3 files changed, 113 insertions(+), 9 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 2319069e23..f7bfdc6004 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -2081,7 +2081,8 @@ def test_notify_users_hides_PM_content_based_on_user_setting( ) @pytest.mark.parametrize( - "event, expected_times_messages_rerendered, expected_index, topic_view_enabled", + "event, initially_me_message," + "expected_times_messages_rerendered, expected_index, topic_view_enabled", [ case( { # Only subject of 1 message is updated. @@ -2091,6 +2092,7 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "stream_id": 10, "message_ids": [1], }, + False, 1, { "messages": { @@ -2099,12 +2101,14 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "stream_id": 10, "content": "old content", "subject": "new subject", + "is_me_message": False, }, 2: { "id": 2, "stream_id": 10, "content": "old content", "subject": "old subject", + "is_me_message": False, }, }, "topic_msg_ids": { @@ -2124,6 +2128,7 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "stream_id": 10, "message_ids": [1, 2], }, + False, 2, { "messages": { @@ -2132,12 +2137,14 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "stream_id": 10, "content": "old content", "subject": "new subject", + "is_me_message": False, }, 2: { "id": 2, "stream_id": 10, "content": "old content", "subject": "new subject", + "is_me_message": False, }, }, "topic_msg_ids": { @@ -2154,7 +2161,9 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "message_id": 1, "stream_id": 10, "rendered_content": "

new content

", + "is_me_message": False, }, + False, 1, { "messages": { @@ -2163,12 +2172,49 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "stream_id": 10, "content": "

new content

", "subject": "old subject", + "is_me_message": False, + }, + 2: { + "id": 2, + "stream_id": 10, + "content": "old content", + "subject": "old subject", + "is_me_message": False, + }, + }, + "topic_msg_ids": { + 10: {"new subject": set(), "old subject": {1, 2}}, + }, + "edited_messages": {1}, + "topics": {10: ["new subject", "old subject"]}, + }, + False, + id="Message content is updated; both not me-messages", + ), + case( + { + "message_id": 1, + "stream_id": 10, + "rendered_content": "

/me has new content

", + "is_me_message": True, + }, + False, + 1, + { + "messages": { + 1: { + "id": 1, + "stream_id": 10, + "content": "

/me has new content

", + "subject": "old subject", + "is_me_message": True, }, 2: { "id": 2, "stream_id": 10, "content": "old content", "subject": "old subject", + "is_me_message": False, }, }, "topic_msg_ids": { @@ -2178,17 +2224,54 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "topics": {10: ["new subject", "old subject"]}, }, False, - id="Message content is updated", + id="Message content is updated; now a me-message", + ), + case( + { + "message_id": 1, + "stream_id": 10, + "rendered_content": "

new content

", + "is_me_message": False, + }, + True, + 1, + { + "messages": { + 1: { + "id": 1, + "stream_id": 10, + "content": "

new content

", + "subject": "old subject", + "is_me_message": False, + }, + 2: { + "id": 2, + "stream_id": 10, + "content": "/me dances (old)", + "subject": "old subject", + "is_me_message": True, + }, + }, + "topic_msg_ids": { + 10: {"new subject": set(), "old subject": {1, 2}}, + }, + "edited_messages": {1}, + "topics": {10: ["new subject", "old subject"]}, + }, + False, + id="Message content is updated; was a me-message, not now", ), case( { # Both message content and subject is updated. "message_id": 1, "rendered_content": "

new content

", + "is_me_message": False, "orig_subject": "old subject", "subject": "new subject", "stream_id": 10, "message_ids": [1], }, + False, 2, { # 2=update of subject & content "messages": { @@ -2197,12 +2280,14 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "stream_id": 10, "content": "

new content

", "subject": "new subject", + "is_me_message": False, }, 2: { "id": 2, "stream_id": 10, "content": "old content", "subject": "old subject", + "is_me_message": False, }, }, "topic_msg_ids": { @@ -2219,6 +2304,7 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "message_id": 1, "foo": "boo", }, + False, 0, { "messages": { @@ -2227,12 +2313,14 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "stream_id": 10, "content": "old content", "subject": "old subject", + "is_me_message": False, }, 2: { "id": 2, "stream_id": 10, "content": "old content", "subject": "old subject", + "is_me_message": False, }, }, "topic_msg_ids": { @@ -2248,11 +2336,13 @@ def test_notify_users_hides_PM_content_based_on_user_setting( { # message_id not present in index, topic view closed. "message_id": 3, "rendered_content": "

new content

", + "is_me_message": False, "orig_subject": "old subject", "subject": "new subject", "stream_id": 10, "message_ids": [3], }, + False, 0, { "messages": { @@ -2261,12 +2351,14 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "stream_id": 10, "content": "old content", "subject": "old subject", + "is_me_message": False, }, 2: { "id": 2, "stream_id": 10, "content": "old content", "subject": "old subject", + "is_me_message": False, }, }, "topic_msg_ids": { @@ -2282,11 +2374,13 @@ def test_notify_users_hides_PM_content_based_on_user_setting( { # message_id not present in index, topic view is enabled. "message_id": 3, "rendered_content": "

new content

", + "is_me_message": False, "orig_subject": "old subject", "subject": "new subject", "stream_id": 10, "message_ids": [3], }, + False, 0, { "messages": { @@ -2295,12 +2389,14 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "stream_id": 10, "content": "old content", "subject": "old subject", + "is_me_message": False, }, 2: { "id": 2, "stream_id": 10, "content": "old content", "subject": "old subject", + "is_me_message": False, }, }, "topic_msg_ids": { @@ -2316,11 +2412,13 @@ def test_notify_users_hides_PM_content_based_on_user_setting( { # Message content is updated and topic view is enabled. "message_id": 1, "rendered_content": "

new content

", + "is_me_message": False, "orig_subject": "old subject", "subject": "new subject", "stream_id": 10, "message_ids": [1], }, + False, 2, { "messages": { @@ -2329,12 +2427,14 @@ def test_notify_users_hides_PM_content_based_on_user_setting( "stream_id": 10, "content": "

new content

", "subject": "new subject", + "is_me_message": False, }, 2: { "id": 2, "stream_id": 10, "content": "old content", "subject": "old subject", + "is_me_message": False, }, }, "topic_msg_ids": { @@ -2353,20 +2453,23 @@ def test__handle_update_message_event( mocker, model, event, + initially_me_message, expected_index, expected_times_messages_rerendered, topic_view_enabled, ): event["type"] = "update_message" + initial_message_data = { # for all messages in index, base data + "stream_id": 10, + "content": "/me dances (old)" if initially_me_message else "old content", + "subject": "old subject", + "is_me_message": initially_me_message, + } + model.index = { "messages": { - message_id: { - "id": message_id, - "stream_id": 10, - "content": "old content", - "subject": "old subject", - } + message_id: dict(initial_message_data, id=message_id) for message_id in [1, 2] }, "topic_msg_ids": { # FIXME? consider test for eg. absence of empty set diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index bd3fae7785..e9d75e297a 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -335,7 +335,7 @@ class UpdateMessageContentEvent(BaseUpdateMessageEvent): # orig_content: str # content: str - # is_me_message: bool + is_me_message: bool class UpdateMessagesLocationEvent(BaseUpdateMessageEvent): diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 48ddf966dc..906660d021 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -1604,6 +1604,7 @@ def _handle_update_message_event(self, event: Event) -> None: if "rendered_content" in event and indexed_message: content_event = cast(UpdateMessageContentEvent, event) indexed_message["content"] = content_event["rendered_content"] + indexed_message["is_me_message"] = content_event["is_me_message"] self.index["messages"][message_id] = indexed_message self._update_rendered_view(message_id) From 5d34ef0484c867167758404087909b7f262f220f Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 16 May 2023 16:09:53 -0700 Subject: [PATCH 083/276] mailmap: Add preferred form for commits from Mounil K. Shah. --- .mailmap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.mailmap b/.mailmap index 5f2ab4ea61..518228ec3e 100644 --- a/.mailmap +++ b/.mailmap @@ -13,3 +13,5 @@ Sumanth V Rao Tim Abbott Zeeshan Equbal <54993043+zee-bit@users.noreply.github.com> Zeeshan Equbal +Mounil K. Shah +Mounil K. Shah From cbafaa016d944903f0ab79200a374f8ee10483be Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Tue, 23 May 2023 23:56:17 +0000 Subject: [PATCH 084/276] refactor: views/core: Parameterize command in UserInfoView initializer. Replaces hardcoded command name, 'USER_INFO', with a new parameter, 'command', thus allowing UserInfoView to be usable for different commands. Fixes calls to UserInfoView initializer with new signature. Test updated. --- tests/ui_tools/test_popups.py | 2 +- zulipterminal/core.py | 4 +++- zulipterminal/ui_tools/views.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py index 3205a13d52..194888b016 100644 --- a/tests/ui_tools/test_popups.py +++ b/tests/ui_tools/test_popups.py @@ -272,7 +272,7 @@ def mock_external_classes( ) self.user_info_view = UserInfoView( - self.controller, 10000, "User Info (up/down scrolls)" + self.controller, 10000, "User Info (up/down scrolls)", "USER_INFO" ) @pytest.mark.parametrize( diff --git a/zulipterminal/core.py b/zulipterminal/core.py index 572efd10f2..aac07be966 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -314,7 +314,9 @@ def show_about(self) -> None: def show_user_info(self, user_id: int) -> None: self.show_pop_up( - UserInfoView(self, user_id, "User Information (up/down scrolls)"), + UserInfoView( + self, user_id, "User Information (up/down scrolls)", "USER_INFO" + ), "area:user", ) diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index e27500a75d..4f63396860 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -1083,7 +1083,7 @@ def __init__( class UserInfoView(PopUpView): - def __init__(self, controller: Any, user_id: int, title: str) -> None: + def __init__(self, controller: Any, user_id: int, title: str, command: str) -> None: display_data = self._fetch_user_data(controller, user_id) user_details = [ @@ -1096,7 +1096,7 @@ def __init__(self, controller: Any, user_id: int, title: str) -> None: ) widgets = self.make_table_with_categories(user_view_content, column_widths) - super().__init__(controller, widgets, "USER_INFO", popup_width, title) + super().__init__(controller, widgets, command, popup_width, title) @staticmethod def _fetch_user_data(controller: Any, user_id: int) -> Dict[str, Any]: From f0016cfd8f27059a6d2d9e1cfa8640bfc68252ff Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Tue, 23 May 2023 23:42:21 +0000 Subject: [PATCH 085/276] keys: Add key for viewing message sender information. Binds key 'u' to the 'MSG_SENDER_INFO' command which shows user information for the sender of a message. --- docs/hotkeys.md | 1 + zulipterminal/config/keys.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docs/hotkeys.md b/docs/hotkeys.md index db4972036f..c9677e8258 100644 --- a/docs/hotkeys.md +++ b/docs/hotkeys.md @@ -58,6 +58,7 @@ |Add/remove thumbs-up reaction to the current message|+| |Add/remove star status of the current message|ctrl + s / *| |Show/hide message information|i| +|Show/hide message sender information|u| |Show/hide edit history (from message information)|e| |View current message in browser (from message information)|v| |Show/hide full rendered message (from message information)|f| diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py index 453b319053..27c9f35428 100644 --- a/zulipterminal/config/keys.py +++ b/zulipterminal/config/keys.py @@ -264,6 +264,11 @@ class KeyBinding(TypedDict): 'help_text': 'Show/hide message information', 'key_category': 'msg_actions', }, + 'MSG_SENDER_INFO': { + 'keys': ['u'], + 'help_text': 'Show/hide message sender information', + 'key_category': 'msg_actions', + }, 'EDIT_HISTORY': { 'keys': ['e'], 'help_text': 'Show/hide edit history (from message information)', From bef2a8fb7e120b5a1f97f0af873771871338d7e0 Mon Sep 17 00:00:00 2001 From: Israel Galadima Date: Wed, 24 May 2023 00:15:45 +0000 Subject: [PATCH 086/276] core/messages: Display message sender information on hotkey 'u'. Defines method show_msg_sender_info which renders a popup that shows user information for a message sender, and is triggered when key 'u' is pressed on an active MessageBox. Fixes #1397. --- zulipterminal/core.py | 11 +++++++++++ zulipterminal/ui_tools/messages.py | 2 ++ 2 files changed, 13 insertions(+) diff --git a/zulipterminal/core.py b/zulipterminal/core.py index aac07be966..b72b1f870b 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -320,6 +320,17 @@ def show_user_info(self, user_id: int) -> None: "area:user", ) + def show_msg_sender_info(self, user_id: int) -> None: + self.show_pop_up( + UserInfoView( + self, + user_id, + "Message Sender Information (up/down scrolls)", + "MSG_SENDER_INFO", + ), + "area:user", + ) + def show_full_rendered_message( self, message: Message, diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index 021f435434..fa35985055 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -1110,4 +1110,6 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: ) elif is_command_key("ADD_REACTION", key): self.model.controller.show_emoji_picker(self.message) + elif is_command_key("MSG_SENDER_INFO", key): + self.model.controller.show_msg_sender_info(self.message["sender_id"]) return key From 6e75b37343b43352ce82e1a3913f1b689f425b70 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 9 Jun 2023 02:02:18 -0700 Subject: [PATCH 087/276] tests: themes: Add standalone test for complete_and_incomplete_themes. The existing test determines whether the function is accurate based upon the built-in theme data. This test applies to a general theme, independently of this data, more directly testing the function. --- tests/config/test_themes.py | 54 ++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index 73ae9cd301..c96d90b1d6 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -5,6 +5,7 @@ import pytest from pygments.styles.perldoc import PerldocStyle +from pytest import param as case from pytest_mock import MockerFixture from zulipterminal.config.regexes import REGEX_COLOR_VALID_FORMATS @@ -22,6 +23,8 @@ ) +MODULE = "zulipterminal.config.themes" + expected_complete_themes = { "zt_dark", "gruvbox_dark", @@ -86,7 +89,7 @@ def test_builtin_theme_completeness(theme_name: str) -> None: assert all(theme_meta[metadata][c] for c in config) -def test_complete_and_incomplete_themes() -> None: +def test_complete_and_incomplete_themes__bundled_theme_output() -> None: # These are sorted to ensure reproducibility result = ( sorted(expected_complete_themes), @@ -95,6 +98,55 @@ def test_complete_and_incomplete_themes() -> None: assert result == complete_and_incomplete_themes() +@pytest.mark.parametrize( + "missing, expected_complete", + [ + case({}, True, id="keys_complete"), + case({"STYLES": "incomplete_style"}, False, id="STYLES_incomplete"), + case({"META": {}}, False, id="META_empty"), + case({"META": {"pygments": {}}}, False, id="META_pygments_empty"), + ], +) +def test_complete_and_incomplete_themes__single_theme_completeness( + mocker: MockerFixture, + missing: Dict[str, Any], + expected_complete: bool, + style: str = "s", + fake_theme_name: str = "sometheme", +) -> None: + class FakeColor(Enum): + COLOR_1 = "a a #" + COLOR_2 = "k b #" + + class FakeTheme: + Color = FakeColor + STYLES = { + style: (FakeColor.COLOR_1, FakeColor.COLOR_2) for style in REQUIRED_STYLES + } + META = { + "pygments": { + "styles": None, + "background": None, + "overrides": None, + } + } + + incomplete_style = {style: (FakeColor.COLOR_1, FakeColor.COLOR_2)} + + for field, action in missing.items(): + if action == "incomplete_style": + setattr(FakeTheme, field, incomplete_style) + else: + setattr(FakeTheme, field, action) + + mocker.patch(MODULE + ".THEMES", {fake_theme_name: FakeTheme}) + + if expected_complete: + assert complete_and_incomplete_themes() == ([fake_theme_name], []) + else: + assert complete_and_incomplete_themes() == ([], [fake_theme_name]) + + @pytest.mark.parametrize( "color_depth, expected_urwid_theme", [ From 0ffec5d90f63a46f15beb6a20b82579983115aeb Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 9 Jun 2023 02:22:31 -0700 Subject: [PATCH 088/276] bugfix: themes: Consider theme with no Color/STYLES/META as incomplete. Previously if STYLES or META were missing, this triggered an AttributeError. Tests extended. --- tests/config/test_themes.py | 5 +++++ zulipterminal/config/themes.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index c96d90b1d6..139c0bc4fe 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -102,7 +102,10 @@ def test_complete_and_incomplete_themes__bundled_theme_output() -> None: "missing, expected_complete", [ case({}, True, id="keys_complete"), + case({"Color": None}, False, id="Color_absent"), + case({"STYLES": None}, False, id="STYLES_absent"), case({"STYLES": "incomplete_style"}, False, id="STYLES_incomplete"), + case({"META": None}, False, id="META_absent"), case({"META": {}}, False, id="META_empty"), case({"META": {"pygments": {}}}, False, id="META_pygments_empty"), ], @@ -136,6 +139,8 @@ class FakeTheme: for field, action in missing.items(): if action == "incomplete_style": setattr(FakeTheme, field, incomplete_style) + elif action is None: + delattr(FakeTheme, field) else: setattr(FakeTheme, field, action) diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py index 3d497294ca..975bd110d1 100644 --- a/zulipterminal/config/themes.py +++ b/zulipterminal/config/themes.py @@ -146,7 +146,10 @@ def complete_and_incomplete_themes() -> Tuple[List[str], List[str]]: complete = { name for name, theme in THEMES.items() + if getattr(theme, "Color", None) + if getattr(theme, "STYLES", None) if set(theme.STYLES) == set(REQUIRED_STYLES) + if getattr(theme, "META", None) if set(theme.META) == set(REQUIRED_META) for meta, conf in theme.META.items() if set(conf) == set(REQUIRED_META.get(meta, {})) From ccb724381124bf651ed5796168c0e53d4d729c6f Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 8 Jun 2023 23:35:25 -0700 Subject: [PATCH 089/276] tests: themes: Add basic test for generate_theme. --- tests/config/test_themes.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index 139c0bc4fe..1e1ea4790c 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -17,6 +17,7 @@ add_pygments_style, all_themes, complete_and_incomplete_themes, + generate_theme, parse_themefile, valid_16_color_codes, validate_colors, @@ -152,6 +153,30 @@ class FakeTheme: assert complete_and_incomplete_themes() == ([], [fake_theme_name]) +def test_generate_theme( + mocker: MockerFixture, + fake_theme_name: str = "fake_theme", + depth: int = 256, # Only test one depth; others covered in parse_themefile tests + single_style: str = "somestyle", +) -> None: + class FakeColor(Enum): + COLOR_1 = "a a #" + COLOR_2 = "k b #" + + theme_styles = {single_style: (FakeColor.COLOR_1, FakeColor.COLOR_2)} + + class FakeTheme: + STYLES = theme_styles + Color = FakeColor # Required for validate_colors + + mocker.patch(MODULE + ".THEMES", {fake_theme_name: FakeTheme}) + + generated_theme = generate_theme(fake_theme_name, depth) + + assert len(generated_theme) == len(theme_styles) + assert (single_style, "", "", "", "a", "b") in generated_theme + + @pytest.mark.parametrize( "color_depth, expected_urwid_theme", [ From d79e4faf4e100faa291f834bcb802199da72b53d Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 9 Jun 2023 00:36:58 -0700 Subject: [PATCH 090/276] themes: Only pass Color enum directly to validate_colors. This function no longer needs to be aware of the full THEME structure, only that expected of Color enums. Error messages are adjusted slightly, since the function no longer has access to the theme name. Test updated and simplified. --- tests/config/test_themes.py | 29 ++++++----------------------- zulipterminal/config/themes.py | 13 +++++++------ 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index 1e1ea4790c..e6910c9df1 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -279,21 +279,8 @@ def test_add_pygments_style( assert style in urwid_theme -# Validate 16-color-codes -@pytest.mark.parametrize( - "color_depth, theme_name", - [ - (16, "zt_dark"), - (16, "gruvbox_dark"), - (16, "gruvbox_light"), - (16, "zt_light"), - (16, "zt_blue"), - ], -) -def test_validate_colors(theme_name: str, color_depth: int) -> None: - theme = THEMES[theme_name] - - header_text = f"Invalid 16-color codes in theme '{theme_name}':\n" +def test_validate_colors(color_depth: int = 16) -> None: + header_text = "Invalid 16-color codes found in this theme:\n" # No invalid colors class Color(Enum): @@ -303,8 +290,7 @@ class Color(Enum): GRAY_244 = "dark_gray h244 #928374" LIGHT2 = "white h250 #d5c4a1" - theme.Color = Color - validate_colors(theme_name, 16) + validate_colors(Color, color_depth) # One invalid color class Color1(Enum): @@ -314,9 +300,8 @@ class Color1(Enum): GRAY_244 = "dark_gray h244 #928374" LIGHT2 = "white h250 #d5c4a1" - theme.Color = Color1 with pytest.raises(InvalidThemeColorCode) as e: - validate_colors(theme_name, 16) + validate_colors(Color1, color_depth) assert str(e.value) == header_text + "- DARK0_HARD = blac" # Two invalid colors @@ -327,9 +312,8 @@ class Color2(Enum): GRAY_244 = "dark_gra h244 #928374" LIGHT2 = "white h250 #d5c4a1" - theme.Color = Color2 with pytest.raises(InvalidThemeColorCode) as e: - validate_colors(theme_name, 16) + validate_colors(Color2, color_depth) assert ( str(e.value) == header_text + "- DARK0_HARD = blac\n" + "- GRAY_244 = dark_gra" ) @@ -342,9 +326,8 @@ class Color3(Enum): GRAY_244 = "dark_gra h244 #928374" LIGHT2 = "whit h250 #d5c4a1" - theme.Color = Color3 with pytest.raises(InvalidThemeColorCode) as e: - validate_colors(theme_name, 16) + validate_colors(Color3, color_depth) assert ( str(e.value) == header_text diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py index 975bd110d1..fb98dc1585 100644 --- a/zulipterminal/config/themes.py +++ b/zulipterminal/config/themes.py @@ -1,7 +1,6 @@ """ Styles and their colour mappings in each theme, with helper functions """ - from typing import Any, Dict, List, Optional, Tuple, Union from pygments.token import STANDARD_TYPES @@ -160,7 +159,8 @@ def complete_and_incomplete_themes() -> Tuple[List[str], List[str]]: def generate_theme(theme_name: str, color_depth: int) -> ThemeSpec: theme_styles = THEMES[theme_name].STYLES - validate_colors(theme_name, color_depth) + theme_colors = THEMES[theme_name].Color + validate_colors(theme_colors, color_depth) urwid_theme = parse_themefile(theme_styles, color_depth) try: @@ -172,17 +172,18 @@ def generate_theme(theme_name: str, color_depth: int) -> ThemeSpec: return urwid_theme -def validate_colors(theme_name: str, color_depth: int) -> None: +# color_enum can be one of many enums satisfying the specification +# There is currently no generic enum type +def validate_colors(color_enum: Any, color_depth: int) -> None: """ This function validates color-codes for a given theme, given colors are in `Color`. If any color is not in accordance with urwid default 16-color codes then the function raises InvalidThemeColorCode with the invalid colors. """ - theme_colors = THEMES[theme_name].Color failure_text = [] if color_depth == 16: - for color in theme_colors: + for color in color_enum: color_16code = color.value.split()[0] if color_16code not in valid_16_color_codes: invalid_16_color_code = str(color.name) @@ -191,7 +192,7 @@ def validate_colors(theme_name: str, color_depth: int) -> None: return else: text = "\n".join( - [f"Invalid 16-color codes in theme '{theme_name}':"] + failure_text + ["Invalid 16-color codes found in this theme:"] + failure_text ) raise InvalidThemeColorCode(text) From 4a367cb9aad9dd29ee6d4cf1668c56080aad9f80 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 9 Jun 2023 01:11:18 -0700 Subject: [PATCH 091/276] themes: Raise exception in theme generation if theme has no Color. Test added. --- tests/config/test_themes.py | 17 +++++++++++++++++ zulipterminal/config/themes.py | 17 ++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index e6910c9df1..0f7803bb89 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -13,6 +13,7 @@ REQUIRED_STYLES, THEMES, InvalidThemeColorCode, + MissingThemeAttributeError, ThemeSpec, add_pygments_style, all_themes, @@ -177,6 +178,22 @@ class FakeTheme: assert (single_style, "", "", "", "a", "b") in generated_theme +def test_generate_theme__no_Color_in_theme( + mocker: MockerFixture, + fake_theme_name: str = "fake_theme", + depth: int = 256, + style: str = "somestyle", +) -> None: + class FakeTheme: + pass + + mocker.patch(MODULE + ".THEMES", {fake_theme_name: FakeTheme}) + + with pytest.raises(MissingThemeAttributeError) as e: + generate_theme(fake_theme_name, depth) + assert str(e.value) == "Theme is missing required attribute 'Color'" + + @pytest.mark.parametrize( "color_depth, expected_urwid_theme", [ diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py index fb98dc1585..1446533235 100644 --- a/zulipterminal/config/themes.py +++ b/zulipterminal/config/themes.py @@ -133,6 +133,11 @@ class InvalidThemeColorCode(Exception): pass +class MissingThemeAttributeError(Exception): + def __init__(self, attribute: str) -> None: + super().__init__(f"Theme is missing required attribute '{attribute}'") + + def all_themes() -> List[str]: return list(THEMES.keys()) @@ -158,13 +163,19 @@ def complete_and_incomplete_themes() -> Tuple[List[str], List[str]]: def generate_theme(theme_name: str, color_depth: int) -> ThemeSpec: - theme_styles = THEMES[theme_name].STYLES - theme_colors = THEMES[theme_name].Color + theme_module = THEMES[theme_name] + + try: + theme_colors = theme_module.Color + except AttributeError: + raise MissingThemeAttributeError("Color") from None validate_colors(theme_colors, color_depth) + + theme_styles = theme_module.STYLES urwid_theme = parse_themefile(theme_styles, color_depth) try: - theme_meta = THEMES[theme_name].META + theme_meta = theme_module.META add_pygments_style(theme_meta, urwid_theme) except AttributeError: pass From 22c1f0dc88bab5b54c989486d6586993612c78ce Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 9 Jun 2023 02:06:37 -0700 Subject: [PATCH 092/276] themes: Raise exception in theme generation if theme has no STYLES. Test updated. --- tests/config/test_themes.py | 14 +++++++++++++- zulipterminal/config/themes.py | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index 0f7803bb89..f8eb32a1db 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -178,7 +178,7 @@ class FakeTheme: assert (single_style, "", "", "", "a", "b") in generated_theme -def test_generate_theme__no_Color_in_theme( +def test_generate_theme__missing_attributes_in_theme( mocker: MockerFixture, fake_theme_name: str = "fake_theme", depth: int = 256, @@ -189,10 +189,22 @@ class FakeTheme: mocker.patch(MODULE + ".THEMES", {fake_theme_name: FakeTheme}) + # No attributes (STYLES or META) - flag missing Color with pytest.raises(MissingThemeAttributeError) as e: generate_theme(fake_theme_name, depth) assert str(e.value) == "Theme is missing required attribute 'Color'" + # Color but missing STYLES - flag missing STYLES + class FakeColor(Enum): + COLOR_1 = "a a #" + COLOR_2 = "k b #" + + FakeTheme.Color = FakeColor # type: ignore [attr-defined] + + with pytest.raises(MissingThemeAttributeError) as e: + generate_theme(fake_theme_name, depth) + assert str(e.value) == "Theme is missing required attribute 'STYLES'" + @pytest.mark.parametrize( "color_depth, expected_urwid_theme", diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py index 1446533235..debab09037 100644 --- a/zulipterminal/config/themes.py +++ b/zulipterminal/config/themes.py @@ -171,7 +171,10 @@ def generate_theme(theme_name: str, color_depth: int) -> ThemeSpec: raise MissingThemeAttributeError("Color") from None validate_colors(theme_colors, color_depth) - theme_styles = theme_module.STYLES + try: + theme_styles = theme_module.STYLES + except AttributeError: + raise MissingThemeAttributeError("STYLES") from None urwid_theme = parse_themefile(theme_styles, color_depth) try: From e0032551f238ce57a63349d7689d67cd4e2d26a5 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 9 Jun 2023 01:37:17 -0700 Subject: [PATCH 093/276] bugfix: themes/run: Output specific error if theme is missing attribute. Previously this raised an exception with no output, except directing to check a logfile. This applies an exception hierarchy in place of the previous single theme-related exception, so handles both cases. --- zulipterminal/cli/run.py | 4 ++-- zulipterminal/config/themes.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index e6f5f22c06..937f2c754c 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -18,7 +18,7 @@ from zulipterminal.api_types import ServerSettings from zulipterminal.config.themes import ( - InvalidThemeColorCode, + ThemeError, aliased_themes, all_themes, complete_and_incomplete_themes, @@ -571,7 +571,7 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None: zt_logger.info("\n\n%s\n\n", e) zt_logger.exception(e) exit_with_error(f"\nError connecting to Zulip server: {e}.") - except InvalidThemeColorCode as e: + except ThemeError as e: # Acts as separator between logs zt_logger.info("\n\n%s\n\n", e) zt_logger.exception(e) diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py index debab09037..5a24c0e50c 100644 --- a/zulipterminal/config/themes.py +++ b/zulipterminal/config/themes.py @@ -129,11 +129,15 @@ ] -class InvalidThemeColorCode(Exception): +class ThemeError(Exception): pass -class MissingThemeAttributeError(Exception): +class InvalidThemeColorCode(ThemeError): + pass + + +class MissingThemeAttributeError(ThemeError): def __init__(self, attribute: str) -> None: super().__init__(f"Theme is missing required attribute '{attribute}'") From a0ddefbd7ca1bd5e2a0c2e8c8025e7111392b516 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 9 Jun 2023 18:57:34 -0700 Subject: [PATCH 094/276] test: themes: Extend valid generate_theme test for presence of META. --- tests/config/test_themes.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index f8eb32a1db..168c7b5aa0 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -4,7 +4,9 @@ from typing import Any, Dict, Optional, Tuple import pytest +from pygments.styles.material import MaterialStyle from pygments.styles.perldoc import PerldocStyle +from pygments.token import STANDARD_TYPES from pytest import param as case from pytest_mock import MockerFixture @@ -154,8 +156,27 @@ class FakeTheme: assert complete_and_incomplete_themes() == ([], [fake_theme_name]) -def test_generate_theme( +@pytest.mark.parametrize( + "META, expected_pygments_length", + [ + case(None, 0, id="META_absent"), + case( + { + "pygments": { + "styles": MaterialStyle().styles, + "background": "h80", + "overrides": {}, + } + }, + len(STANDARD_TYPES), + id="META_with_valid_values", + ), + ], +) +def test_generate_theme__has_required_attributes( mocker: MockerFixture, + META: Optional[Dict[str, Dict[str, Any]]], + expected_pygments_length: int, fake_theme_name: str = "fake_theme", depth: int = 256, # Only test one depth; others covered in parse_themefile tests single_style: str = "somestyle", @@ -170,11 +191,14 @@ class FakeTheme: STYLES = theme_styles Color = FakeColor # Required for validate_colors + if META is not None: + FakeTheme.META = META # type: ignore [attr-defined] + mocker.patch(MODULE + ".THEMES", {fake_theme_name: FakeTheme}) generated_theme = generate_theme(fake_theme_name, depth) - assert len(generated_theme) == len(theme_styles) + assert len(generated_theme) == len(theme_styles) + expected_pygments_length assert (single_style, "", "", "", "a", "b") in generated_theme From 11e1e846912c710eeb9f604cf7246ac825ed04e0 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 8 Jun 2023 21:52:20 -0700 Subject: [PATCH 095/276] refactor: themes: Only pass pygments data to add_pygments_style. This function no longer needs to be aware of the full THEME structure, only that expected of the pygments data. This is an equivalent refactor to 75e2d3caf, but for pygments rather than color data. Test updated. --- tests/config/test_themes.py | 18 ++++++++---------- zulipterminal/config/themes.py | 5 ++--- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index 168c7b5aa0..4bc04df638 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -282,17 +282,15 @@ class Color(Enum): @pytest.mark.parametrize( - "theme_meta, expected_styles", + "pygments_data, expected_styles", [ ( { - "pygments": { - "styles": PerldocStyle().styles, - "background": "#def", - "overrides": { - "k": "#abc", - "sd": "#123, bold", - }, + "styles": PerldocStyle().styles, + "background": "#def", + "overrides": { + "k": "#abc", + "sd": "#123, bold", }, }, [ @@ -318,12 +316,12 @@ class Color(Enum): ], ) def test_add_pygments_style( - mocker: MockerFixture, theme_meta: Dict[str, Any], expected_styles: ThemeSpec + mocker: MockerFixture, pygments_data: Dict[str, Any], expected_styles: ThemeSpec ) -> None: urwid_theme: ThemeSpec = [(None, "#xxx", "#yyy")] original_urwid_theme = deepcopy(urwid_theme) - add_pygments_style(theme_meta, urwid_theme) + add_pygments_style(pygments_data, urwid_theme) # Check if original exists assert original_urwid_theme[0] in urwid_theme diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py index 5a24c0e50c..a69350d6fc 100644 --- a/zulipterminal/config/themes.py +++ b/zulipterminal/config/themes.py @@ -183,7 +183,7 @@ def generate_theme(theme_name: str, color_depth: int) -> ThemeSpec: try: theme_meta = theme_module.META - add_pygments_style(theme_meta, urwid_theme) + add_pygments_style(theme_meta["pygments"], urwid_theme) except AttributeError: pass @@ -246,7 +246,7 @@ def parse_themefile( return urwid_theme -def add_pygments_style(theme_meta: Dict[str, Any], urwid_theme: ThemeSpec) -> None: +def add_pygments_style(pygments: Dict[str, Any], urwid_theme: ThemeSpec) -> None: """ This function adds pygments styles for use in syntax highlighting of code blocks and inline code. @@ -261,7 +261,6 @@ def add_pygments_style(theme_meta: Dict[str, Any], urwid_theme: ThemeSpec) -> No used to override certain pygments styles to match to urwid format. It can also be used to customize the syntax style. """ - pygments = theme_meta["pygments"] pygments_styles = pygments["styles"] pygments_bg = pygments["background"] pygments_overrides = pygments["overrides"] From 47765e52c4f0c33ef3f5738a8b1f557ce08f236c Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 9 Jun 2023 18:10:25 -0700 Subject: [PATCH 096/276] bugfix: themes: Raise exception in theme generation if META no pygments. Currently META is only used for pygments data. This now gives an error in situations such as an empty META dict, or one without a pygments key, which may indicate a misspelling. Test extended. --- tests/config/test_themes.py | 9 +++++++++ zulipterminal/config/themes.py | 12 +++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index 4bc04df638..ca8b9d2c1c 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -229,6 +229,15 @@ class FakeColor(Enum): generate_theme(fake_theme_name, depth) assert str(e.value) == "Theme is missing required attribute 'STYLES'" + # Color, STYLES and META, but no pygments data in META + not_all_styles = {style: (FakeColor.COLOR_1, FakeColor.COLOR_2)} + FakeTheme.STYLES = not_all_styles # type: ignore [attr-defined] + FakeTheme.META = {} # type: ignore [attr-defined] + + with pytest.raises(MissingThemeAttributeError) as e: + generate_theme(fake_theme_name, depth) + assert str(e.value) == """Theme is missing required attribute 'META["pygments"]'""" + @pytest.mark.parametrize( "color_depth, expected_urwid_theme", diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py index a69350d6fc..8678bbc975 100644 --- a/zulipterminal/config/themes.py +++ b/zulipterminal/config/themes.py @@ -181,11 +181,13 @@ def generate_theme(theme_name: str, color_depth: int) -> ThemeSpec: raise MissingThemeAttributeError("STYLES") from None urwid_theme = parse_themefile(theme_styles, color_depth) - try: - theme_meta = theme_module.META - add_pygments_style(theme_meta["pygments"], urwid_theme) - except AttributeError: - pass + # META is not required, but if present should contain pygments data + theme_meta = getattr(theme_module, "META", None) + if theme_meta is not None: + pygments_data = theme_meta.get("pygments", None) + if pygments_data is None: + raise MissingThemeAttributeError('META["pygments"]') from None + add_pygments_style(pygments_data, urwid_theme) return urwid_theme From ce2cf9c65f3578ef003e82350103741d0f7b2f86 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 8 Jun 2023 22:14:13 -0700 Subject: [PATCH 097/276] refactor: themes: Return pygments styles instead of modifying argument. This decouples the urwid styles generated from the simple and pygments data, limiting one function from accidentally modifying the others' data, and simplifying testing. Due to different behavior, this also renames add_pygments_style to generate_pygments_styles. Test updated. --- tests/config/test_themes.py | 14 ++++---------- zulipterminal/config/themes.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index ca8b9d2c1c..f7671df631 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -1,5 +1,4 @@ import re -from copy import deepcopy from enum import Enum from typing import Any, Dict, Optional, Tuple @@ -17,9 +16,9 @@ InvalidThemeColorCode, MissingThemeAttributeError, ThemeSpec, - add_pygments_style, all_themes, complete_and_incomplete_themes, + generate_pygments_styles, generate_theme, parse_themefile, valid_16_color_codes, @@ -324,19 +323,14 @@ class Color(Enum): ) ], ) -def test_add_pygments_style( +def test_generate_pygments_styles( mocker: MockerFixture, pygments_data: Dict[str, Any], expected_styles: ThemeSpec ) -> None: - urwid_theme: ThemeSpec = [(None, "#xxx", "#yyy")] - original_urwid_theme = deepcopy(urwid_theme) + pygments_styles = generate_pygments_styles(pygments_data) - add_pygments_style(pygments_data, urwid_theme) - - # Check if original exists - assert original_urwid_theme[0] in urwid_theme # Check for overrides(k,sd) and inheriting styles (kr) for style in expected_styles: - assert style in urwid_theme + assert style in pygments_styles def test_validate_colors(color_depth: int = 16) -> None: diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py index 8678bbc975..563ae890b9 100644 --- a/zulipterminal/config/themes.py +++ b/zulipterminal/config/themes.py @@ -187,7 +187,11 @@ def generate_theme(theme_name: str, color_depth: int) -> ThemeSpec: pygments_data = theme_meta.get("pygments", None) if pygments_data is None: raise MissingThemeAttributeError('META["pygments"]') from None - add_pygments_style(pygments_data, urwid_theme) + pygments_styles = generate_pygments_styles(pygments_data) + else: + pygments_styles = [] + + urwid_theme.extend(pygments_styles) return urwid_theme @@ -248,7 +252,7 @@ def parse_themefile( return urwid_theme -def add_pygments_style(pygments: Dict[str, Any], urwid_theme: ThemeSpec) -> None: +def generate_pygments_styles(pygments: Dict[str, Any]) -> ThemeSpec: """ This function adds pygments styles for use in syntax highlighting of code blocks and inline code. @@ -270,6 +274,7 @@ def add_pygments_style(pygments: Dict[str, Any], urwid_theme: ThemeSpec) -> None term16_styles = term16.styles term16_bg = term16.background_color + theme_styles_from_pygments: ThemeSpec = [] for token, css_class in STANDARD_TYPES.items(): if css_class in pygments_overrides: pygments_styles[token] = pygments_overrides[css_class] @@ -298,4 +303,5 @@ def add_pygments_style(pygments: Dict[str, Any], urwid_theme: ThemeSpec) -> None pygments_styles[token], pygments_bg, ) - urwid_theme.append(new_style) + theme_styles_from_pygments.append(new_style) + return theme_styles_from_pygments From f71997b08f8eedaa5ad2c83332ea2b5505688307 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 11 Jun 2023 14:29:15 -0700 Subject: [PATCH 098/276] bugfix: themes: Raise exception in theme generation if partial pygments. This now gives the user an error if any of the 3 pygments fields are absent. The overrides field could strictly be optional, but it is simpler to require all to be present. Test extended. --- tests/config/test_themes.py | 12 ++++++++++++ zulipterminal/config/themes.py | 3 +++ 2 files changed, 15 insertions(+) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index f7671df631..b06e35df39 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -237,6 +237,18 @@ class FakeColor(Enum): generate_theme(fake_theme_name, depth) assert str(e.value) == """Theme is missing required attribute 'META["pygments"]'""" + # Color, STYLES and META, but incomplete pygments in META + FakeTheme.META = { # type: ignore [attr-defined] + "pygments": {"styles": "", "background": ""} + } + + with pytest.raises(MissingThemeAttributeError) as e: + generate_theme(fake_theme_name, depth) + assert ( + str(e.value) + == """Theme is missing required attribute 'META["pygments"]["overrides"]'""" + ) + @pytest.mark.parametrize( "color_depth, expected_urwid_theme", diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py index 563ae890b9..c9e5174a40 100644 --- a/zulipterminal/config/themes.py +++ b/zulipterminal/config/themes.py @@ -187,6 +187,9 @@ def generate_theme(theme_name: str, color_depth: int) -> ThemeSpec: pygments_data = theme_meta.get("pygments", None) if pygments_data is None: raise MissingThemeAttributeError('META["pygments"]') from None + for key in REQUIRED_META["pygments"]: + if pygments_data.get(key) is None: + raise MissingThemeAttributeError(f'META["pygments"]["{key}"]') from None pygments_styles = generate_pygments_styles(pygments_data) else: pygments_styles = [] From 4e2f87ee761015f259d768975d0f5f053a24030a Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Sat, 8 Apr 2023 15:29:16 -0700 Subject: [PATCH 099/276] model/api_types/helper: Add _clean_and_order_custom_profile_data method. This commit adds the _clean_and_order_custom_profile_data function to model.py. The function takes the profile data (in Zulip API format) as input and processes fields to get label, value, type and order of the field. It also sorts profile data according to the "order" attribute. The value of field type 6 (Person picker) is stored as a list of user ids. The values for all other field types are stored as strings. Data structures related to custom profile fields are added to api_types.py (CustomProfileField, CustomFieldValue) and helper.py (CustomProfileData). Test and related fixtures added. --- tests/conftest.py | 227 ++++++++++++++++++++++++++++++++++++- tests/model/test_model.py | 13 +++ zulipterminal/api_types.py | 24 ++++ zulipterminal/helper.py | 8 ++ zulipterminal/model.py | 52 +++++++++ 5 files changed, 322 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3e77d51f33..12932f431b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,13 +6,18 @@ from pytest_mock import MockerFixture from urwid import Widget -from zulipterminal.api_types import Message, MessageType +from zulipterminal.api_types import ( + CustomFieldValue, + CustomProfileField, + Message, + MessageType, +) from zulipterminal.config.keys import ( ZT_TO_URWID_CMD_MAPPING, keys_for_command, primary_key_for_command, ) -from zulipterminal.helper import Index, TidiedUserInfo +from zulipterminal.helper import CustomProfileData, Index, TidiedUserInfo from zulipterminal.helper import initial_index as helper_initial_index from zulipterminal.ui_tools.buttons import StreamButton, TopicButton, UserButton from zulipterminal.ui_tools.messages import MessageBox @@ -626,6 +631,224 @@ def mentioned_messages_combination(request: Any) -> Tuple[Set[int], Set[int]]: return deepcopy(request.param) +@pytest.fixture +def custom_profile_fields_fixture() -> List[CustomProfileField]: + return [ + { + # Short text: For one line responses, like "Job title". + # Responses are limited to 50 characters. + "id": 1, + "name": "Phone number", + "type": 1, + "hint": "", + "field_data": "", + "order": 1, + }, + { + # Long text: For multiline responses, like "Biography". + "id": 2, + "name": "Biography", + "type": 2, + "hint": "What are you known for?", + "field_data": "", + "order": 2, + }, + { + # Another example of Short text field. + "id": 3, + "name": "Favorite food", + "type": 1, + "hint": "Or drink, if you'd prefer", + "field_data": "", + "order": 3, + }, + { + # List of options: Creates a dropdown with a list of options. + "id": 4, + "name": "Favorite editor", + "type": 3, + "hint": "", + "field_data": ( + '{"0":{"text":"Vim","order":"1"},"1":{"text":"Emacs","order":"2"}}' + ), + "order": 4, + }, + { + # Date picker: For dates, like "Birthday". + "id": 5, + "name": "Birthday", + "type": 4, + "hint": "", + "field_data": "", + "order": 5, + }, + { + # Link: For links to websites. + "id": 6, + "name": "Favorite website", + "type": 5, + "hint": "Or your personal blog's URL", + "field_data": "", + "order": 6, + }, + { + # Person picker: For selecting one or more users, + # like "Manager" or "Direct reports". + "id": 7, + "name": "Manager", + "type": 6, + "hint": "Only one person", + "field_data": "", + "order": 7, + }, + { + # Another person picker: Use case with multiple users. + "id": 8, + "name": "Mentor", + "type": 6, + "hint": "", + "field_data": "", + "order": 8, + }, + { + # External account: For linking to GitHub, Twitter, etc. + # GitHub example + "id": 9, + "name": "GitHub username", + "type": 7, + "hint": "", + "field_data": '{"subtype":"github"}', + "order": 9, + }, + { + # Twitter example + "id": 10, + "name": "Twitter username", + "type": 7, + "hint": "", + "field_data": '{"subtype":"twitter"}', + "order": 10, + }, + { + # Custom example + "id": 11, + "name": "Reddit username", + "type": 7, + "hint": "", + "field_data": '{"subtype":"custom", "url_pattern":"https://www.reddit.com/u/%(username)s"}', + "order": 11, + }, + { + # Pronouns: What pronouns should people use to refer to the user? + "id": 12, + "name": "Pronouns", + "type": 8, + "hint": "What pronouns should people use to refer to you?", + "field_data": "", + "order": 12, + }, + ] + + +@pytest.fixture +def custom_profile_data_fixture() -> Dict[str, CustomFieldValue]: + return { + "1": {"value": "6352813452", "rendered_value": "

6352813452

"}, + "2": { + "value": "Simplicity\nThis is a multiline field", + "rendered_value": "

Simplicity\nThis is a multiline field

", + }, + "3": {"value": "cola", "rendered_value": "

cola

"}, + "4": {"value": "0"}, + "5": {"value": "2023-04-22"}, + "6": {"value": "https://www.google.com"}, + "7": {"value": "[11]"}, + "8": {"value": "[11, 13]"}, + "9": {"value": "gitmaster"}, + "10": {"value": "twittermaster"}, + "11": {"value": "redditmaster"}, + "12": {"value": "he/him"}, + } + + +@pytest.fixture +def clean_custom_profile_data_fixture() -> List[CustomProfileData]: + return [ + { + "label": "Phone number", + "value": "6352813452", + "type": 1, + "order": 1, + }, + { + "label": "Biography", + "value": "Simplicity\nThis is a multiline field", + "type": 2, + "order": 2, + }, + { + "label": "Favorite food", + "value": "cola", + "type": 1, + "order": 3, + }, + { + "label": "Favorite editor", + "value": "Vim", + "type": 3, + "order": 4, + }, + { + "label": "Birthday", + "value": "2023-04-22", + "type": 4, + "order": 5, + }, + { + "label": "Favorite website", + "value": "https://www.google.com", + "type": 5, + "order": 6, + }, + { + "label": "Manager", + "value": [11], + "type": 6, + "order": 7, + }, + { + "label": "Mentor", + "value": [11, 13], + "type": 6, + "order": 8, + }, + { + "label": "GitHub username", + "value": "https://github.com/gitmaster", + "type": 7, + "order": 9, + }, + { + "label": "Twitter username", + "value": "https://twitter.com/twittermaster", + "type": 7, + "order": 10, + }, + { + "label": "Reddit username", + "value": "https://www.reddit.com/u/redditmaster", + "type": 7, + "order": 11, + }, + { + "label": "Pronouns", + "value": "he/him", + "type": 8, + "order": 12, + }, + ] + + @pytest.fixture def initial_data( logged_on_user: Dict[str, Any], diff --git a/tests/model/test_model.py b/tests/model/test_model.py index f7bfdc6004..d7108f81f1 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -1570,6 +1570,19 @@ def test__group_info_from_realm_user_groups(self, model, user_groups_fixture): } assert user_group_names == ["Group 1", "Group 2", "Group 3", "Group 4"] + def test__clean_and_order_custom_profile_data( + self, + model, + custom_profile_fields_fixture, + custom_profile_data_fixture, + clean_custom_profile_data_fixture, + ): + model.initial_data["custom_profile_fields"] = custom_profile_fields_fixture + assert ( + clean_custom_profile_data_fixture + == model._clean_and_order_custom_profile_data(custom_profile_data_fixture) + ) + @pytest.mark.parametrize( ["to_vary_in_each_user", "key", "expected_value"], [ diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index e9d75e297a..ca18fb89a5 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -237,6 +237,25 @@ class Subscription(TypedDict): # in_home_view: bool # Replaced by is_muted in Zulip 2.1; still present in updates +############################################################################### +# In "custom_profile_fields" response from: +# https://zulip.com/api/register-queue +# Also directly from: +# https://zulip.com/api/get-events#custom_profile_fields +# NOTE: This data structure is currently used in conftest.py to improve +# typing of fixtures, and can be used when initial_data is refactored to have +# better typing. + + +class CustomProfileField(TypedDict): + id: int + name: str + type: Literal[1, 2, 3, 4, 5, 6, 7, 8] # Field types range from 1 to 8. + hint: str + field_data: str + order: int + + ############################################################################### # In "realm_user" response from: # https://zulip.com/api/register-queue @@ -248,6 +267,11 @@ class Subscription(TypedDict): # NOTE: Responses between versions & endpoints vary +class CustomFieldValue(TypedDict): + value: str + rendered_value: NotRequired[str] + + class RealmUser(TypedDict): user_id: int full_name: str diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index 0b7f59b328..af8fc41325 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -25,6 +25,7 @@ Set, Tuple, TypeVar, + Union, ) from urllib.parse import unquote @@ -63,6 +64,13 @@ class EmojiData(TypedDict): NamedEmojiData = Dict[str, EmojiData] +class CustomProfileData(TypedDict): + label: str + value: Union[str, List[int]] + type: int + order: int + + class TidiedUserInfo(TypedDict): full_name: str email: str diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 906660d021..945a9944a9 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -35,6 +35,7 @@ MAX_STREAM_NAME_LENGTH, MAX_TOPIC_NAME_LENGTH, Composition, + CustomFieldValue, DirectTypingNotification, EditPropagateMode, Event, @@ -60,6 +61,7 @@ StreamAccessType, ) from zulipterminal.helper import ( + CustomProfileData, Message, NamedEmojiData, StreamData, @@ -975,6 +977,56 @@ def get_other_subscribers_in_stream( if sub != self.user_id ] + def _clean_and_order_custom_profile_data( + self, custom_profile_data: Dict[str, CustomFieldValue] + ) -> List[CustomProfileData]: + # Get custom profile fields + profile_fields = { + str(field["id"]): field + for field in self.initial_data["custom_profile_fields"] + } + + cleaned_profile_data = [] + for field_id in custom_profile_data: + field = profile_fields[field_id] + raw_value = custom_profile_data[field_id]["value"] + + if field["type"] == 3: # List of options + field_options = json.loads(field["field_data"]) + field_value = field_options[raw_value]["text"] + + elif field["type"] == 6: # Person picker + field_value = list( + map( + int, + raw_value.strip("][").split(","), + ) + ) + + elif field["type"] == 7: # External Account + field_options = json.loads(field["field_data"]) + field_subtype = field_options["subtype"] + if field_subtype == "github": + url_pattern = "https://github.com/%(username)s" + elif field_subtype == "twitter": + url_pattern = "https://twitter.com/%(username)s" + else: + url_pattern = field_options["url_pattern"] + field_value = url_pattern % {"username": raw_value} + else: + field_value = raw_value + + data: CustomProfileData = { + "label": field["name"], + "value": field_value, + "type": field["type"], + "order": field["order"], + } + cleaned_profile_data.append(data) + + cleaned_profile_data.sort(key=lambda field: field["order"]) + return cleaned_profile_data + def get_user_info(self, user_id: int) -> Optional[TidiedUserInfo]: api_user_data: Optional[RealmUser] = self._all_users_by_id.get(user_id, None) From c1387cea2c980d00b113e0c2ab6525d06b28850c Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Fri, 7 Apr 2023 19:32:08 -0700 Subject: [PATCH 100/276] model/api_types/helper: Include custom profile data in get_user_info. This commit adds fetching of custom profile fields from the Zulip API upon connection to the server. This enables the get_user_info method to also clean a user's custom profile data using the _clean_and_order_custom_profile_data function, which directly uses the custom profile fields. To support this, the RealmUser class in api_types.py is updated to include the profile_data field, and the TidiedUserInfo class in helper.py to include the custom_profile_data field. Tests and fixtures updated. --- tests/conftest.py | 18 ++++++++++++++++-- tests/model/test_model.py | 21 +++++++++++++++++++++ zulipterminal/api_types.py | 3 ++- zulipterminal/helper.py | 1 + zulipterminal/model.py | 6 ++++++ 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 12932f431b..5f18bfc909 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -125,7 +125,10 @@ def msg_box( @pytest.fixture -def users_fixture(logged_on_user: Dict[str, Any]) -> List[Dict[str, Any]]: +def users_fixture( + logged_on_user: Dict[str, Any], + custom_profile_data_fixture: Dict[str, CustomFieldValue], +) -> List[Dict[str, Any]]: users = [logged_on_user] for i in range(1, 3): users.append( @@ -154,11 +157,19 @@ def users_fixture(logged_on_user: Dict[str, Any]) -> List[Dict[str, Any]]: "is_admin": False, } ) + # Add custom profile data to user 12 + for user in users: + if user["user_id"] == 12: + user["profile_data"] = custom_profile_data_fixture + else: + user["profile_data"] = {} return users @pytest.fixture -def tidied_user_info_response() -> TidiedUserInfo: +def tidied_user_info_response( + clean_custom_profile_data_fixture: List[CustomProfileData], +) -> TidiedUserInfo: # FIXME: Refactor this to use a more generic user? return { "full_name": "Human 2", @@ -170,6 +181,7 @@ def tidied_user_info_response() -> TidiedUserInfo: "bot_type": None, "bot_owner_name": "", "last_active": "", + "custom_profile_data": clean_custom_profile_data_fixture, } @@ -855,6 +867,7 @@ def initial_data( users_fixture: List[Dict[str, Any]], streams_fixture: List[Dict[str, Any]], realm_emojis: Dict[str, Dict[str, Any]], + custom_profile_fields_fixture: List[Dict[str, Union[str, int]]], ) -> Dict[str, Any]: """ Response from /register API request. @@ -1029,6 +1042,7 @@ def initial_data( "zulip_version": MINIMUM_SUPPORTED_SERVER_VERSION[0], "zulip_feature_level": MINIMUM_SUPPORTED_SERVER_VERSION[1], "starred_messages": [1117554, 1117558, 1117574], + "custom_profile_fields": custom_profile_fields_fixture, } diff --git a/tests/model/test_model.py b/tests/model/test_model.py index d7108f81f1..3713eef342 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -254,6 +254,7 @@ def test_register_initial_desired_events(self, mocker, initial_data): "update_display_settings", "user_settings", "realm_emoji", + "custom_profile_fields", "zulip_version", ] model.client.register.assert_called_once_with( @@ -1638,6 +1639,26 @@ def test__clean_and_order_custom_profile_data( id="user_bot_has_owner:preZulip_3.0", ), case({}, "bot_owner_name", "", id="user_bot_has_no_owner"), + case( + { + "profile_data": { + "2": { + "value": "Simplicity", + "rendered_value": "

Simplicity

", + }, + }, + }, + "custom_profile_data", + [ + { + "label": "Biography", + "value": "Simplicity", + "type": 2, + "order": 2, + }, + ], + id="user_has_custom_profile_data", + ), ], ) def test_get_user_info( diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index ca18fb89a5..6d5567e71c 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -298,8 +298,9 @@ class RealmUser(TypedDict): is_admin: bool is_guest: bool # NOTE: added /users/me ZFL 10; other changes before that + profile_data: Dict[str, CustomFieldValue] + # To support in future: - # profile_data: Dict # NOTE: Only if requested # is_active: bool # NOTE: Dependent upon realm_users vs realm_non_active_users # delivery_email: str # NOTE: Only available if admin, and email visibility limited diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index af8fc41325..59aa4af893 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -78,6 +78,7 @@ class TidiedUserInfo(TypedDict): timezone: str role: int last_active: str + custom_profile_data: List[CustomProfileData] is_bot: bool # Below fields are only meaningful if is_bot == True diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 945a9944a9..6506fb5f7d 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -141,6 +141,7 @@ def __init__(self, controller: Any) -> None: "update_display_settings", "user_settings", "realm_emoji", + "custom_profile_fields", # zulip_version and zulip_feature_level are always returned in # POST /register from Feature level 3. "zulip_version", @@ -1046,6 +1047,10 @@ def get_user_info(self, user_id: int) -> Optional[TidiedUserInfo]: else: user_role = raw_user_role + custom_profile_data = api_user_data.get("profile_data", {}) + cleaned_custom_profile_data = self._clean_and_order_custom_profile_data( + custom_profile_data + ) # TODO: Add custom fields later as an enhancement user_info: TidiedUserInfo = dict( full_name=api_user_data.get("full_name", "(No name)"), @@ -1054,6 +1059,7 @@ def get_user_info(self, user_id: int) -> Optional[TidiedUserInfo]: timezone=api_user_data.get("timezone", ""), is_bot=api_user_data.get("is_bot", False), role=user_role, + custom_profile_data=cleaned_custom_profile_data, bot_type=api_user_data.get("bot_type", None), bot_owner_name="", # Can be non-empty only if is_bot == True last_active="", From 393a365d17f022724f6fe7117fabc9bec4aa186b Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Sat, 3 Jun 2023 17:01:50 +0530 Subject: [PATCH 101/276] refactor: api_types: Use Union to improve type of RealmUserEventPerson. This commit improves the type checking of the RealmUserEventPerson TypedDict, changing it to be a Union of TypedDict classes for each of the expected groups of fields. --- zulipterminal/api_types.py | 55 +++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 6d5567e71c..25f37fd660 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List, Optional, Union -from typing_extensions import Final, Literal, NotRequired, TypedDict +from typing_extensions import Final, Literal, NotRequired, TypedDict, final # These are documented in the zulip package (python-zulip-api repo) from zulip import EditPropagateMode # one/all/later @@ -398,33 +398,80 @@ class ReactionEvent(TypedDict): # ----------------------------------------------------------------------------- # See https://zulip.com/api/get-events#realm_user-add and -remove -class RealmUserEventPerson(TypedDict): - user_id: int + +@final +class RealmUserUpdateName(TypedDict): + user_id: int full_name: str + +@final +class RealmUserUpdateAvatar(TypedDict): + user_id: int avatar_url: str avatar_source: str avatar_url_medium: str avatar_version: int + +@final +class RealmUserUpdateTimeZone(TypedDict): + user_id: int # NOTE: This field will be removed in future as it is redundant with the user_id # email: str timezone: str + +@final +class RealmUserUpdateBotOwner(TypedDict): + user_id: int bot_owner_id: int + +@final +class RealmUserUpdateRole(TypedDict): + user_id: int role: int + +@final +class RealmUserUpdateBillingRole(TypedDict): + user_id: int is_billing_admin: bool # New in ZFL 73 (Zulip 5.0) + +@final +class RealmUserUpdateDeliveryEmail(TypedDict): + user_id: int delivery_email: str # NOTE: Only sent to admins - # custom_profile_field: Dict # TODO: Requires checking before implementation +@final +class RealmUserUpdateCustomProfileField(TypedDict): + # TODO: Requires checking before implementation + user_id: int + custom_profile_field: Dict[str, Any] + + +@final +class RealmUserUpdateEmail(TypedDict): + user_id: int new_email: str +RealmUserEventPerson = Union[ + RealmUserUpdateName, + RealmUserUpdateAvatar, + RealmUserUpdateTimeZone, + RealmUserUpdateBotOwner, + RealmUserUpdateRole, + RealmUserUpdateBillingRole, + RealmUserUpdateDeliveryEmail, + RealmUserUpdateEmail, +] + + class RealmUserEvent(TypedDict): type: Literal["realm_user"] op: Literal["update"] From bad0c71b5de40d74ae8ecb4df37783ce87b93922 Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Mon, 10 Apr 2023 15:46:00 -0700 Subject: [PATCH 102/276] model/api_types: Include custom profile data in realm_user update event. This commit adds event handling functionality to custom profile data on realm_user 'update' events. In the case that the field data is None, the field is removed from the user profile data, otherwise the field is updated according to the event data. Tests added. --- tests/model/test_model.py | 162 ++++++++++++++++++++++++++++++++++++- zulipterminal/api_types.py | 10 ++- zulipterminal/model.py | 15 ++++ 3 files changed, 184 insertions(+), 3 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 3713eef342..aa5c9bf587 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -1,3 +1,4 @@ +import copy import json from collections import OrderedDict from copy import deepcopy @@ -3614,7 +3615,7 @@ def test__handle_subscription_event_subscribers_one_user_multiple_streams( "delivery_email", ], ) - def test__handle_realm_user_event( + def test__handle_realm_user_event__general( self, person, event_field, updated_field_if_different, model, initial_data ): # id 11 matches initial_data["realm_users"][1] in the initial_data fixture @@ -3632,6 +3633,165 @@ def test__handle_realm_user_event( == event["person"][event_field] ) + @pytest.mark.parametrize("user_id", [11, 12], ids=["no_custom", "many_custom"]) + @pytest.mark.parametrize( + "update_data, expected_modified_field_id", + [ + case( + { + "id": 1, + "value": "7237032732", + "rendered_value": "

7237032732

", + }, + "1", + id="Short Text 1", + ), + case( + { + "id": 2, + "value": "Complexity", + "rendered_value": "

Complexity

", + }, + "2", + id="Long Text", + ), + case( + { + "id": 3, + "value": "pizza", + "rendered_value": "

pizza

", + }, + "3", + id="Short Text 2", + ), + case( + { + "id": 4, + "value": "0", + }, + "4", + id="List of Options", + ), + case( + { + "id": 5, + "value": "2023-04-22", + }, + "5", + id="Date Picker", + ), + case( + { + "id": 6, + "value": "https://www.google.com", + }, + "6", + id="Link", + ), + case( + { + "id": 7, + "value": "[13]", + }, + "7", + id="Person Picker", + ), + case( + { + "id": 9, + "value": "githubmaster", + }, + "9", + id="External Account", + ), + case( + { + "id": 12, + "value": "she/her", + }, + "12", + id="Pronouns", + ), + ], + ) + def test__handle_realm_user_event__custom_profile_data__update_data( + self, + user_id, + update_data, + expected_modified_field_id, + model, + initial_data, + ): + REALM_USER_INDEX = user_id - 10 + user_data = initial_data["realm_users"][REALM_USER_INDEX] + # Ensure indices match user id + assert user_data["user_id"] == user_id, "unexpected test configuration" + # Updated value is expected to vary from existing value + assert ( + user_data["profile_data"].get(update_data["id"], {}).get("value", None) + != update_data["value"] + ), "unexpected test configuration" + + person = {"custom_profile_field": update_data, "user_id": user_id} + event = {"type": "realm_user", "op": "update", "id": 1000, "person": person} + + profile_data_before_update = copy.deepcopy(user_data["profile_data"]) + expected_profile_data = { + key: update_data[key] for key in update_data if key != "id" + } + + model._handle_realm_user_event(event) + + assert ( + user_data["profile_data"][expected_modified_field_id] + == expected_profile_data + ) + assert all( + user_data["profile_data"][field_id] == profile_data_before_update[field_id] + for field_id in profile_data_before_update + if field_id != expected_modified_field_id + ) + + @pytest.mark.parametrize("user_id", [11, 12], ids=["no_custom", "many_custom"]) + @pytest.mark.parametrize( + "update_data, expected_removed_field_id", + [ + ( + { + "id": 8, + "value": None, + }, + "8", + ), + ], + ) + def test__handle_realm_user_event__custom_profile_data__remove_data( + self, + user_id, + update_data, + expected_removed_field_id, + model, + initial_data, + ): + REALM_USER_INDEX = user_id - 10 + user_data = initial_data["realm_users"][REALM_USER_INDEX] + # Ensure indices match user id + assert user_data["user_id"] == user_id, "unexpected test configuration" + + person = {"custom_profile_field": update_data, "user_id": user_id} + event = {"type": "realm_user", "op": "update", "id": 1000, "person": person} + + profile_data_before_update = copy.deepcopy(user_data["profile_data"]) + + model._handle_realm_user_event(event) + + assert expected_removed_field_id not in user_data["profile_data"] + assert all( + user_data["profile_data"][field_id] == profile_data_before_update[field_id] + for field_id in profile_data_before_update + if field_id != expected_removed_field_id + ) + @pytest.mark.parametrize("value", [True, False]) def test__handle_user_settings_event(self, mocker, model, value): setting = "send_private_typing_notifications" diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 25f37fd660..23247a8324 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -400,6 +400,12 @@ class ReactionEvent(TypedDict): # See https://zulip.com/api/get-events#realm_user-add and -remove +class UpdateCustomFieldValue(TypedDict): + id: int + value: Optional[str] + rendered_value: NotRequired[str] + + @final class RealmUserUpdateName(TypedDict): user_id: int @@ -449,9 +455,8 @@ class RealmUserUpdateDeliveryEmail(TypedDict): @final class RealmUserUpdateCustomProfileField(TypedDict): - # TODO: Requires checking before implementation user_id: int - custom_profile_field: Dict[str, Any] + custom_profile_field: UpdateCustomFieldValue @final @@ -468,6 +473,7 @@ class RealmUserUpdateEmail(TypedDict): RealmUserUpdateRole, RealmUserUpdateBillingRole, RealmUserUpdateDeliveryEmail, + RealmUserUpdateCustomProfileField, RealmUserUpdateEmail, ] diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 6506fb5f7d..ac4cd1dea0 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -1927,6 +1927,21 @@ def _handle_realm_user_event(self, event: Event) -> None: # realm_users has 'email' attribute and not 'new_email' if "new_email" in updated_details: realm_user["email"] = updated_details["new_email"] + + elif "custom_profile_field" in updated_details: + profile_field_data = updated_details["custom_profile_field"] + profile_field_id = str(profile_field_data["id"]) + + if profile_field_data["value"] is None: + # Ignore if field does not exist + realm_user["profile_data"].pop(profile_field_id, None) + else: + updated_data = { + key: value + for key, value in profile_field_data.items() + if key != "id" + } + realm_user["profile_data"][profile_field_id] = updated_data else: realm_user.update(updated_details) break From 3081df9fe74b934a2f6d90becf6e044ceb7e4263 Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Tue, 4 Apr 2023 17:23:33 -0700 Subject: [PATCH 103/276] views: Update user info popup to display custom profile fields. This commit updates the user information popup to display the custom profile fields stored in the model, including processing additional data received from the model in _fetch_user_data. All fields except the Person picker field are strings, so if a Person picker field is present, it is converted to a string of usernames separate by commas. Tests added and updated. Fixes #1338. --- tests/ui_tools/test_popups.py | 70 ++++++++++++++++++++++++++++++--- zulipterminal/ui_tools/views.py | 34 ++++++++++++++-- 2 files changed, 95 insertions(+), 9 deletions(-) diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py index 194888b016..c378ba9eb0 100644 --- a/tests/ui_tools/test_popups.py +++ b/tests/ui_tools/test_popups.py @@ -9,7 +9,7 @@ from zulipterminal.api_types import Message from zulipterminal.config.keys import is_command_key, keys_for_command from zulipterminal.config.ui_mappings import EDIT_MODE_CAPTIONS -from zulipterminal.helper import TidiedUserInfo +from zulipterminal.helper import CustomProfileData, TidiedUserInfo from zulipterminal.ui_tools.messages import MessageBox from zulipterminal.ui_tools.views import ( AboutView, @@ -271,6 +271,15 @@ def mock_external_classes( return_value="Tue Mar 13 10:55 AM", ) + mocked_user_name_from_id = { + 11: "Human 1", + 12: "Human 2", + 13: "Human 3", + } + self.controller.model.user_name_from_id = mocker.Mock( + side_effect=lambda param: mocked_user_name_from_id.get(param, "(No name)") + ) + self.user_info_view = UserInfoView( self.controller, 10000, "User Info (up/down scrolls)", "USER_INFO" ) @@ -334,26 +343,77 @@ def mock_external_classes( ) def test__fetch_user_data( self, - mocker: MockerFixture, to_vary_in_each_user: Dict[str, Any], expected_key: str, expected_value: Optional[str], ) -> None: data = dict(self.user_data, **to_vary_in_each_user) - mocker.patch.object(self.controller.model, "get_user_info", return_value=data) + self.controller.model.get_user_info.return_value = data - display_data = self.user_info_view._fetch_user_data(self.controller, 1) + display_data, custom_profile_data = self.user_info_view._fetch_user_data( + self.controller, 1 + ) assert display_data.get(expected_key, None) == expected_value + @pytest.mark.parametrize( + [ + "to_vary_in_each_user", + "expected_value", + ], + [ + case( + [], + {}, + id="user_has_no_custom_profile_data", + ), + case( + [ + { + "label": "Biography", + "value": "Simplicity", + "type": 2, + "order": 2, + }, + { + "label": "Mentor", + "value": [11, 12], + "type": 6, + "order": 7, + }, + ], + {"Biography": "Simplicity", "Mentor": "Human 1, Human 2"}, + id="user_has_custom_profile_data", + ), + ], + ) + def test__fetch_user_data__custom_profile_data( + self, + to_vary_in_each_user: List[CustomProfileData], + expected_value: Dict[str, str], + ) -> None: + data = dict(self.user_data) + data["custom_profile_data"] = to_vary_in_each_user + + self.controller.model.get_user_info.return_value = data + + display_data, custom_profile_data = self.user_info_view._fetch_user_data( + self.controller, 1 + ) + + assert custom_profile_data == expected_value + def test__fetch_user_data_USER_NOT_FOUND(self, mocker: MockerFixture) -> None: mocker.patch.object(self.controller.model, "get_user_info", return_value=dict()) - display_data = self.user_info_view._fetch_user_data(self.controller, 1) + display_data, custom_profile_data = self.user_info_view._fetch_user_data( + self.controller, 1 + ) assert display_data["Name"] == "(Unavailable)" assert display_data["Error"] == "User data not found" + assert custom_profile_data == {} @pytest.mark.parametrize( "key", {*keys_for_command("GO_BACK"), *keys_for_command("USER_INFO")} diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 4f63396860..04acd52b8f 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -1084,13 +1084,20 @@ def __init__( class UserInfoView(PopUpView): def __init__(self, controller: Any, user_id: int, title: str, command: str) -> None: - display_data = self._fetch_user_data(controller, user_id) + display_data, display_custom_profile_data = self._fetch_user_data( + controller, user_id + ) user_details = [ (key, value) for key, value in display_data.items() if key != "Name" ] user_view_content = [(display_data["Name"], user_details)] + if display_custom_profile_data: + user_view_content.extend( + [("Additional Details", list(display_custom_profile_data.items()))] + ) + popup_width, column_widths = self.calculate_table_widths( user_view_content, len(title) ) @@ -1099,16 +1106,19 @@ def __init__(self, controller: Any, user_id: int, title: str, command: str) -> N super().__init__(controller, widgets, command, popup_width, title) @staticmethod - def _fetch_user_data(controller: Any, user_id: int) -> Dict[str, Any]: + def _fetch_user_data( + controller: Any, user_id: int + ) -> Tuple[Dict[str, str], Dict[str, str]]: # Get user data from model data: TidiedUserInfo = controller.model.get_user_info(user_id) + display_custom_profile_data = {} if not data: display_data = { "Name": "(Unavailable)", "Error": "User data not found", } - return display_data + return (display_data, display_custom_profile_data) # Style the data obtained to make it displayable display_data = {"Name": data["full_name"]} @@ -1144,7 +1154,23 @@ def _fetch_user_data(controller: Any, user_id: int) -> Dict[str, Any]: if data["last_active"]: display_data["Last active"] = data["last_active"] - return display_data + # This will be an empty dict in case of bot users + custom_profile_data = data["custom_profile_data"] + + if custom_profile_data: + for field in custom_profile_data: + if field["type"] == 6: # Person picker + user_names = [ + controller.model.user_name_from_id(user) + for user in field["value"] + ] + field["value"] = ", ".join(user_names) + # After conversion of field type 6, all values are str + assert isinstance(field["value"], str) + + display_custom_profile_data[field["label"]] = field["value"] + + return (display_data, display_custom_profile_data) class HelpView(PopUpView): From f749006e896cc809ae2a06dd59e526f4ea4c8aa7 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 18 Jun 2023 09:13:12 -0700 Subject: [PATCH 104/276] pyproject: Expand and reorder mypy options to match mypy --help. The strict section now matches entries and ordering from mypy --strict. Other general checking options are extracted into their own sections according to the order in the mypy --help page, except for those already in the strict section. Where the main command-line flag loosens the type-checking, these options are treated as mypy defaults, and commented in each section. This commit also starts enforcing the following, which require no changes to code: - strict_concatenate [strict] - truthy-iterable [error-code] - ignore-without-code [error-code] - unused-awaitable [error-code] --- pyproject.toml | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bd0dec8fe6..3a7fd5abcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,24 @@ scripts_are_modules = true show_traceback = true cache_dir = ".mypy_cache" -# Options to make the checking stricter, as would be mypy --strict +# Non-strict dynamic typing (unlikely to be enabled) +disallow_any_unimported = false +disallow_any_expr = false +disallow_any_decorated = false +disallow_any_explicit = false # May be useful to improve checking of non-UI files + +# Optional handling (mypy defaults) +no_implicit_optional = true +strict_optional = true + +# Warnings (mypy defaults) +warn_no_return = true + +# Misc strictness (mypy defaults) +disallow_untyped_globals = true +disallow_redefinition = true + +# Options to make the checking stricter, as included in mypy --strict # (in order of mypy --help docs for --strict) warn_unused_configs = true disallow_any_generics = true @@ -57,16 +74,18 @@ disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true disallow_untyped_decorators = false -no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true warn_return_any = false no_implicit_reexport = false strict_equality = true +strict_concatenate = true -# These are now defaults and we could remove them -warn_no_return = true -strict_optional = true +enable_error_code = [ + "truthy-iterable", + "ignore-without-code", + "unused-awaitable", # Even if await unused, it may be in future +] # If a library is typed, that's fine, otherwise don't worry ignore_missing_imports = true From 80488f8ed551277e9264f7c2cceb3bc32c612526 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 25 Jun 2023 17:42:35 -0700 Subject: [PATCH 105/276] pyproject/lint-and-test: Enable mypy untyped_decorators checks. This is part of the strict block of checks. This change passes mypy in a local full development environment, but to pass in GitHub Actions the test dependencies must now be added to the type-checking dependencies. --- .github/workflows/lint-and-test.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index f52d1af047..1660bcc7e2 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -26,8 +26,8 @@ jobs: python-version: 3.7 cache: 'pip' cache-dependency-path: 'setup.py' - - name: Install with type-checking tools - run: pip install .[typing] + - name: Install with type-checking tools, stubs & test libraries + run: pip install .[typing,testing] - name: Run mypy run: ./tools/run-mypy diff --git a/pyproject.toml b/pyproject.toml index 3a7fd5abcc..14a46e0562 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ disallow_untyped_calls = false disallow_untyped_defs = true disallow_incomplete_defs = true check_untyped_defs = true -disallow_untyped_decorators = false +disallow_untyped_decorators = true warn_redundant_casts = true warn_unused_ignores = true warn_return_any = false From ec909c0f01c026fe1a99ad9c3c29f7c83ff50071 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 18 Jun 2023 09:37:19 -0700 Subject: [PATCH 106/276] helper/core: Remove unreachable code. Confirmed after identification from mypy with warn_unreachable option. --- zulipterminal/core.py | 12 ++---------- zulipterminal/helper.py | 2 -- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/zulipterminal/core.py b/zulipterminal/core.py index b72b1f870b..6ac1ee4740 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -119,16 +119,8 @@ def raise_exception_in_main_thread( # Exceptions shouldn't occur before the pipe is set assert hasattr(self, "_exception_pipe") - if isinstance(exc_info, tuple): - self._exception_info = exc_info - self._critical_exception = critical - else: - self._exception_info = ( - RuntimeError, - f"Invalid cross-thread exception info '{exc_info}'", - None, - ) - self._critical_exception = True + self._exception_info = exc_info + self._critical_exception = critical os.write(self._exception_pipe, b"1") def is_in_editor_mode(self) -> bool: diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index 59aa4af893..318c55b118 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -801,8 +801,6 @@ def download_media( controller.report_success([" Downloaded ", ("bold", media_name)]) return normalized_file_path(local_path) - return "" - @asynch def open_media(controller: Any, tool: str, media_path: str) -> None: From c04a8e0155cb7542291aed6689d034b788de2a64 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 18 Jun 2023 09:44:16 -0700 Subject: [PATCH 107/276] refactor: api_types/views/tests: Correct types so mypy reaches code. These changes ensure that mypy will not flag branches of code as unreachable. One type could be inferred instead, but is amended and remains to help with readability, in _fetch_user_data. --- tests/ui/test_ui.py | 2 +- zulipterminal/api_types.py | 8 ++++---- zulipterminal/ui_tools/views.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/ui/test_ui.py b/tests/ui/test_ui.py index f047bbc516..8693869696 100644 --- a/tests/ui/test_ui.py +++ b/tests/ui/test_ui.py @@ -419,7 +419,7 @@ def test_keypress_OPEN_DRAFT( self, view: View, mocker: MockerFixture, - draft: Composition, + draft: Optional[Composition], key: str, autohide: bool, widget_size: Callable[[Widget], urwid_Box], diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 23247a8324..c0f36727da 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -500,11 +500,11 @@ class SubscriptionPeerAddRemoveEvent(TypedDict): type: Literal["subscription"] op: Literal["peer_add", "peer_remove"] - stream_id: int - stream_ids: List[int] # NOTE: replaces 'stream_id' in ZFL 35 + stream_id: NotRequired[int] + stream_ids: NotRequired[List[int]] # NOTE: replaces 'stream_id' in ZFL 35 - user_id: int - user_ids: List[int] # NOTE: replaces 'user_id' in ZFL 35 + user_id: NotRequired[int] + user_ids: NotRequired[List[int]] # NOTE: replaces 'user_id' in ZFL 35 # ----------------------------------------------------------------------------- diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 04acd52b8f..a3c8b2da45 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -1110,9 +1110,9 @@ def _fetch_user_data( controller: Any, user_id: int ) -> Tuple[Dict[str, str], Dict[str, str]]: # Get user data from model - data: TidiedUserInfo = controller.model.get_user_info(user_id) + data: Optional[TidiedUserInfo] = controller.model.get_user_info(user_id) - display_custom_profile_data = {} + display_custom_profile_data: Dict[str, str] = {} if not data: display_data = { "Name": "(Unavailable)", From a88adfd677681c4c8d1a1269cfc55d5ffb0c4efb Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 18 Jun 2023 13:38:03 -0700 Subject: [PATCH 108/276] pyproject: Enable mypy warn_unreachable checks. This enforces this check after adjustments in previous commits. This is a warning not included in mypy strict mode, so is added to a new section. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 14a46e0562..c668bea18c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,9 @@ warn_no_return = true disallow_untyped_globals = true disallow_redefinition = true +# Warnings not in strict +warn_unreachable = true + # Options to make the checking stricter, as included in mypy --strict # (in order of mypy --help docs for --strict) warn_unused_configs = true From ed11873d90d6838d65677381ac8fbcb07a762d8c Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 18 Jun 2023 20:36:58 -0700 Subject: [PATCH 109/276] run-mypy: Remove many hard-coded and command-line options. These hard-coded options are now better stored in the pyproject.toml configuration, while command-line options have not changed in some time and are no longer necessary. --- tools/run-mypy | 53 -------------------------------------------------- 1 file changed, 53 deletions(-) diff --git a/tools/run-mypy b/tools/run-mypy index b30ef68f81..06b5e79c05 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -54,14 +54,6 @@ parser.add_argument( find out which files fail mypy check.""", ) -parser.add_argument( - "--no-disallow-untyped-defs", - dest="disallow_untyped_defs", - action="store_false", - default=True, - help="""Don't throw errors when functions are not annotated""", -) - parser.add_argument( "--scripts-only", dest="scripts_only", @@ -70,37 +62,6 @@ parser.add_argument( help="""Only type check extensionless python scripts""", ) -parser.add_argument( - "--no-strict-optional", - dest="strict_optional", - action="store_false", - default=True, - help="""Don't use the --strict-optional flag with mypy""", -) - -parser.add_argument( - "--warn-unused-ignores", - dest="warn_unused_ignores", - action="store_true", - default=False, - help="""Use the --warn-unused-ignores flag with mypy""", -) - -parser.add_argument( - "--no-ignore-missing-imports", - dest="ignore_missing_imports", - action="store_false", - default=True, - help="""Don't use the --ignore-missing-imports flag with mypy""", -) - -parser.add_argument( - "--quick", - action="store_true", - default=False, - help="""Use the --quick flag with mypy""", -) - args = parser.parse_args() files_dict = cast( @@ -136,22 +97,8 @@ for file_path in python_files: mypy_command = "mypy" extra_args = [ - "--check-untyped-defs", "--follow-imports=silent", - "--scripts-are-modules", - "--disallow-any-generics", - "-i", ] -if args.disallow_untyped_defs: - extra_args.append("--disallow-untyped-defs") -if args.warn_unused_ignores: - extra_args.append("--warn-unused-ignores") -if args.strict_optional: - extra_args.append("--strict-optional") -if args.ignore_missing_imports: - extra_args.append("--ignore-missing-imports") -if args.quick: - extra_args.append("--quick") # run mypy status = 0 From 10b0721425367fcb933a6c99bab4fa5b6b094543 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 18 Jun 2023 20:52:27 -0700 Subject: [PATCH 110/276] lister/run-mypy/pyproject: Improve follow-imports & type-check lister. lister.py was previously explicitly excluded from type-checking, but if we remove the hard-coded `follow-imports=silent` in tools/run-mypy, it is evident that it was previously being checked due to being imported where it was used - but error messages were not reported. With minimal typing and a minor change to lister.py, the file can be made to pass type-checking, enabling an overall move to the better default `follow-imports=normal` setting in pyproject.toml. Moreover, this also allows lister.py to be removed from the list of files excluded from type-checking we maintain in pyproject.toml and run-mypy. --- pyproject.toml | 6 +++--- tools/lister.py | 20 ++++++++++---------- tools/run-mypy | 5 +---- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c668bea18c..f22aa44208 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,9 @@ enable_error_code = [ "unused-awaitable", # Even if await unused, it may be in future ] -# If a library is typed, that's fine, otherwise don't worry +# Follow imports to look for typing information, ignoring if not present +# NOTE: If imports are not present and ignored, all types are set to be Any +follow_imports = "normal" ignore_missing_imports = true [[tool.mypy.overrides]] @@ -167,8 +169,6 @@ select = [ "tests/model/test_model.py" = ["ANN"] "tests/ui/test_ui_tools.py" = ["ANN"] "tests/ui_tools/test_messages.py" = ["ANN"] -# ANN: This tool is an old variation from zulip/zulip and is only updated where necessary -"tools/lister.py" = ["ANN"] # E501: Ignore length in tools for now (and otherwise where noqa specified) # T20: Expect print output from CLI from tools "tools/*" = ["E501", "T20"] diff --git a/tools/lister.py b/tools/lister.py index 00f1eb4795..a3f81aa6fe 100755 --- a/tools/lister.py +++ b/tools/lister.py @@ -6,7 +6,7 @@ import subprocess import sys from collections import defaultdict -from typing import Dict, List +from typing import Dict, List, Sequence, Union def get_ftype(fpath: str, use_shebang: bool) -> str: @@ -33,14 +33,14 @@ def get_ftype(fpath: str, use_shebang: bool) -> str: def list_files( - targets=[], - ftypes=[], - use_shebang=True, - modified_only=False, - exclude=[], - group_by_ftype=False, - extless_only=False, -): + targets: Sequence[str] = [], + ftypes: Sequence[str] = [], + use_shebang: bool = True, + modified_only: bool = False, + exclude: Sequence[str] = [], + group_by_ftype: bool = False, + extless_only: bool = False, +) -> Union[Dict[str, List[str]], List[str]]: """ List files tracked by git. Returns a list of files which are either in targets or in directories in @@ -74,7 +74,7 @@ def list_files( os.path.abspath(os.path.join(repository_root, fpath)) for fpath in exclude ] - cmdline = ["git", "ls-files"] + targets + cmdline = ["git", "ls-files", *targets] if modified_only: cmdline.append("-m") diff --git a/tools/run-mypy b/tools/run-mypy index 06b5e79c05..d0d3e6f6eb 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -15,7 +15,6 @@ EXCLUDE_FILES = [ "tools/fetch-pull-request", "tools/fetch-rebase-pull-request", "tools/check-branch", - "tools/lister.py", # Came from zulip/zulip, now in zulint? ] python_project_folders = ["zulipterminal", "tests", "tools"] @@ -96,9 +95,7 @@ for file_path in python_files: mypy_command = "mypy" -extra_args = [ - "--follow-imports=silent", -] +extra_args: List[str] = [] # run mypy status = 0 From bdb64501fb594d3f62203e6be18dbd40fac0436b Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 18 Jun 2023 21:01:23 -0700 Subject: [PATCH 111/276] bugfix: messages: Correct edge cases in use of bs4 via type-checking. This was surfaced by the use of the type stubs for beautifulsoup4. Notes on the changes: - tag_attr.get("attribute", []) can return a str or List[str] at runtime, depending upon whether the tag can be multivalued. Using .get_attribute_list() instead makes this explicit, in the absence of type stubs for .get() overloaded on the name of the parameter. - Similarly, "title" is a single-entry field, so it is more accurate to set the default to the empty string. - Lastly, .find() only applies to a Tag, and can result in a Tag, but also potentially None (already checked) or a NavigableString. Based on the structure of the document the latter is unlikely, but this is now ensured by converting the not-None check to whether the result is explicitly a Tag. --- zulipterminal/ui_tools/messages.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index fa35985055..ea50bb89ac 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -383,7 +383,7 @@ def soup2markup( # if/elif/else chain below for improving legibility. tag = element.name tag_attrs = element.attrs - tag_classes = tag_attrs.get("class", []) + tag_classes: List[str] = element.get_attribute_list("class", []) tag_text = element.text if isinstance(element, NavigableString): @@ -402,7 +402,7 @@ def soup2markup( markup.append(unrendered_template.format(text)) elif tag == "img" and tag_classes == ["emoji"]: # CUSTOM EMOJIS AND ZULIP_EXTRA_EMOJI - emoji_name = tag_attrs.get("title", []) + emoji_name: str = tag_attrs.get("title", "") markup.append(("msg_emoji", f":{emoji_name}:")) elif tag in unrendered_tags: # UNRENDERED SIMPLE TAGS @@ -814,7 +814,7 @@ def transform_content( time_mentions=list(), ) # type: Dict[str, Any] - if body and body.find(name="blockquote"): + if isinstance(body, Tag) and body.find(name="blockquote"): metadata["bq_len"] = cls.indent_quoted_content(soup, QUOTED_TEXT_MARKER) markup, message_links, time_mentions = cls.soup2markup(body, metadata) From 47159b0da09e0ca9e2d9f554ce5af0775683d60d Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 18 Jun 2023 20:55:53 -0700 Subject: [PATCH 112/276] requirements[dev]: Add types-beautifulsoup4 to improve type-checking. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 1c465a8dc9..13c304fc00 100644 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ def long_description(): typing_deps = [ "lxml-stubs", "mypy~=1.3.0", + "types-beautifulsoup4", "types-pygments", "types-python-dateutil", "types-tzlocal", From 9b9d73318451e8f4b37cfa9a1601048d3904b485 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 18 Jun 2023 20:59:07 -0700 Subject: [PATCH 113/276] pyproject/tools: Error on missing types in libraries, with exceptions. Rather than mypy simply ignoring any missing type information on imports, this change sets this to be an error by default. Separate sections for imports used in the main application and only during development are added, to exclude certain libraries (import patterns) from resulting in errors, where the type information is not available. This makes the lack of typing more explicit and limited. In addition, an import in tools/convert-unicode-emoji-data is marked with a type-ignore comment, since the imported file is only temporary. --- pyproject.toml | 21 +++++++++++++++++++-- tools/convert-unicode-emoji-data | 5 ++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f22aa44208..90828f1396 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,9 +90,26 @@ enable_error_code = [ "unused-awaitable", # Even if await unused, it may be in future ] -# Follow imports to look for typing information, ignoring if not present -# NOTE: If imports are not present and ignored, all types are set to be Any +# Follow imports to look for typing information, erroring if not present follow_imports = "normal" +ignore_missing_imports = false + +[[tool.mypy.overrides]] +# Libraries used in main application, for which typing is unavailable +# NOTE: When missing imports are ignored, all types are set to be Any +module = [ + "urwid.*", # Minimal typing of this is in urwid_types.py + "urwid_readline", # Typing this likely depends on typing urwid + "pyperclip", # Hasn't been updated in some time, unlikely to be typed + "pudb", # This is barely used & could be optional/dev dependency +] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +# Development-only tools, for which typing is unavailable +module = [ + "gitlint.*", # This lack of typing should only impact our custom rules +] ignore_missing_imports = true [[tool.mypy.overrides]] diff --git a/tools/convert-unicode-emoji-data b/tools/convert-unicode-emoji-data index 00a28fdc66..74acc1e685 100755 --- a/tools/convert-unicode-emoji-data +++ b/tools/convert-unicode-emoji-data @@ -7,7 +7,10 @@ from pathlib import Path, PurePath try: - from zulipterminal.unicode_emoji_dict import EMOJI_NAME_MAPS + # Ignored for type-checking, as it is a temporary file, deleted at the end of file + from zulipterminal.unicode_emoji_dict import ( # type: ignore [import] + EMOJI_NAME_MAPS, + ) except ModuleNotFoundError: print( "ERROR: Could not find downloaded unicode emoji\n" From e5d7eaa2504cb2a8542dda156663408a1eea2ea5 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 23 Jun 2023 10:53:33 -0700 Subject: [PATCH 114/276] pyproject/views: Enable mypy truthy-bool error code checks. Specific lines involving ModListWalker are ignored, since this class has an implementation of __len__ in the base class, but that base class is identified as Any due to limited type information at this time. --- pyproject.toml | 1 + zulipterminal/ui_tools/views.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 90828f1396..ea59410c83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,7 @@ strict_equality = true strict_concatenate = true enable_error_code = [ + "truthy-bool", "truthy-iterable", "ignore-without-code", "unused-awaitable", # Even if await unused, it may be in future diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index a3c8b2da45..2f9386fc7f 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -79,7 +79,7 @@ def set_focus(self, position: int) -> None: def _set_focus(self, index: int) -> None: # This method is called when directly setting focus via # self.focus = focus_position - if not self: + if not self: # type: ignore[truthy-bool] # Implemented in base class self._focus = 0 return if index < 0 or index >= len(self): @@ -132,7 +132,7 @@ def load_old_messages(self, anchor: int) -> None: self.old_loading = True ids_to_keep = self.model.get_message_ids_in_current_narrow() - if self.log: + if self.log: # type: ignore[truthy-bool] # Implemented in base class top_message_id = self.log[0].original_widget.message["id"] ids_to_keep.remove(top_message_id) # update this id no_update_baseline = {top_message_id} @@ -144,7 +144,7 @@ def load_old_messages(self, anchor: int) -> None: # Only update if more messages are provided if ids_to_process != no_update_baseline: - if self.log: + if self.log: # type: ignore[truthy-bool] # Implemented in base class self.log.remove(self.log[0]) # avoid duplication when updating message_list = create_msg_box_list(self.model, ids_to_process) @@ -164,7 +164,7 @@ def load_new_messages(self, anchor: int) -> None: current_ids = self.model.get_message_ids_in_current_narrow() self.model.get_messages(num_before=0, num_after=30, anchor=anchor) new_ids = self.model.get_message_ids_in_current_narrow() - current_ids - if self.log: + if self.log: # type: ignore[truthy-bool] # Implemented in base class last_message = self.log[-1].original_widget.message else: last_message = None From 7d094b218610899731a9d015abf8cc4e1b251516 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 24 Jun 2023 16:27:32 -0700 Subject: [PATCH 115/276] pyproject/model: Enable mypy enable_redundant_expr error code checks. An always-true condition in _handle_user_settings_event is moved from a conditional into an assert to satisfy this check. --- pyproject.toml | 1 + zulipterminal/model.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ea59410c83..f093977655 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,7 @@ strict_equality = true strict_concatenate = true enable_error_code = [ + "redundant-expr", "truthy-bool", "truthy-iterable", "ignore-without-code", diff --git a/zulipterminal/model.py b/zulipterminal/model.py index ac4cd1dea0..3387ca206f 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -1887,8 +1887,9 @@ def _handle_user_settings_event(self, event: Event) -> None: """ assert event["type"] == "user_settings" # We only expect these to be "update" event operations + assert event["op"] == "update" # Update the setting (property) to the value, but only if already initialized - if event["op"] == "update" and event["property"] in self._user_settings: + if event["property"] in self._user_settings: setting = event["property"] self._user_settings[setting] = event["value"] From 2b685a347a26a16a01f3fd7fd0f71a195468f755 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 24 Jun 2023 17:58:12 -0700 Subject: [PATCH 116/276] model/buttons/messages/views: Import Message directly from api_types.py. Other than shortening the chain of imports, this makes it clearer where Message is originally defined, and that it is considered part of the API (api_types.py), not an internal structure (helper.py). --- zulipterminal/model.py | 2 +- zulipterminal/ui_tools/buttons.py | 4 ++-- zulipterminal/ui_tools/messages.py | 3 ++- zulipterminal/ui_tools/views.py | 3 +-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 3387ca206f..704c3b27f5 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -39,6 +39,7 @@ DirectTypingNotification, EditPropagateMode, Event, + Message, MessagesFlagChange, PrivateComposition, PrivateMessageUpdateRequest, @@ -62,7 +63,6 @@ ) from zulipterminal.helper import ( CustomProfileData, - Message, NamedEmojiData, StreamData, TidiedUserInfo, diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index 5b1584da77..fe2116981c 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -10,12 +10,12 @@ import urwid from typing_extensions import TypedDict -from zulipterminal.api_types import RESOLVED_TOPIC_PREFIX, EditPropagateMode +from zulipterminal.api_types import RESOLVED_TOPIC_PREFIX, EditPropagateMode, Message from zulipterminal.config.keys import is_command_key, primary_key_for_command from zulipterminal.config.regexes import REGEX_INTERNAL_LINK_STREAM_ID from zulipterminal.config.symbols import CHECK_MARK, MUTE_MARKER from zulipterminal.config.ui_mappings import EDIT_MODE_CAPTIONS, STREAM_ACCESS_TYPE -from zulipterminal.helper import Message, StreamData, hash_util_decode, process_media +from zulipterminal.helper import StreamData, hash_util_decode, process_media from zulipterminal.urwid_types import urwid_MarkupTuple, urwid_Size diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index ea50bb89ac..e73143ad66 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -15,6 +15,7 @@ from bs4.element import NavigableString, Tag from tzlocal import get_localzone +from zulipterminal.api_types import Message from zulipterminal.config.keys import is_command_key, primary_key_for_command from zulipterminal.config.symbols import ( MESSAGE_CONTENT_MARKER, @@ -24,7 +25,7 @@ TIME_MENTION_MARKER, ) from zulipterminal.config.ui_mappings import STATE_ICON -from zulipterminal.helper import Message, get_unused_fence +from zulipterminal.helper import get_unused_fence from zulipterminal.server_url import near_message_url from zulipterminal.ui_tools.tables import render_table from zulipterminal.urwid_types import urwid_MarkupTuple, urwid_Size diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 2f9386fc7f..64eebeb255 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -10,7 +10,7 @@ import urwid from typing_extensions import Literal -from zulipterminal.api_types import EditPropagateMode +from zulipterminal.api_types import EditPropagateMode, Message from zulipterminal.config.keys import ( HELP_CATEGORIES, KEY_BINDINGS, @@ -35,7 +35,6 @@ ) from zulipterminal.config.ui_sizes import LEFT_WIDTH from zulipterminal.helper import ( - Message, TidiedUserInfo, asynch, match_emoji, From 518e097ce5172a9670aacd03c5a830494d22d16b Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 24 Jun 2023 17:44:08 -0700 Subject: [PATCH 117/276] run-mypy: Add support for repo/project-specific mypy arguments. In general this behavior is now handled in pyproject.toml instead, but in certain cases this is not possible. This approach enables options to be applied differently when running mypy on repos/projects (folders) which interact, eg. import from each other, and changing one folder of files would otherwise be necessary *only* to satisfy mypy when running on the other. --- tools/run-mypy | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/run-mypy b/tools/run-mypy index d0d3e6f6eb..0917eeea86 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -17,7 +17,12 @@ EXCLUDE_FILES = [ "tools/check-branch", ] -python_project_folders = ["zulipterminal", "tests", "tools"] +project_mypy_args: Dict[str, List[str]] = { + "zulipterminal": [], + "tests": [], + "tools": [], +} +python_project_folders = list(project_mypy_args) TOOLS_DIR = os.path.dirname(os.path.abspath(__file__)) os.chdir(os.path.dirname(TOOLS_DIR)) @@ -101,8 +106,9 @@ extra_args: List[str] = [] status = 0 for repo, python_files in repo_python_files.items(): print(f"Running mypy for `{repo}`.", flush=True) + repo_args = project_mypy_args[repo] if python_files: - result = subprocess.call([mypy_command] + extra_args + python_files) + result = subprocess.call([mypy_command] + extra_args + repo_args + python_files) if result != 0: status = result else: From c7f5d7026690875e93a8d0cf3ca657a7c7b5e164 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 24 Jun 2023 17:45:54 -0700 Subject: [PATCH 118/276] pyproject/run-mypy/api_types: Enable mypy no_implicit_reexport option. This is enabled primarily in pyproject.toml. The flags used when applying mypy to tests/ (in run-mypy) are specifically adjusted to avoid this option. This cannot be achieved by applying an override to "tests.*" in pyproject.toml, since the error is flagged as being in imported non-test files. The source could be adjusted to take this into account, but it appears cleaner to import from the file being tested. Two names imported from the zulip library are explicitly exported from api_types.py to enable their use elsewhere in the source, using __all__. The alternative is to use the `from X import Y as Y` technique, but this triggers ruff error PLC0414 and is verbose. --- pyproject.toml | 2 +- tools/run-mypy | 2 +- zulipterminal/api_types.py | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f093977655..b7c1ec28f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ disallow_untyped_decorators = true warn_redundant_casts = true warn_unused_ignores = true warn_return_any = false -no_implicit_reexport = false +no_implicit_reexport = true # NOTE: Disabled explicitly for tests/ in run-mypy strict_equality = true strict_concatenate = true diff --git a/tools/run-mypy b/tools/run-mypy index 0917eeea86..560a301a55 100755 --- a/tools/run-mypy +++ b/tools/run-mypy @@ -19,7 +19,7 @@ EXCLUDE_FILES = [ project_mypy_args: Dict[str, List[str]] = { "zulipterminal": [], - "tests": [], + "tests": ["--implicit-reexport"], "tools": [], } python_project_folders = list(project_mypy_args) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index c0f36727da..8acf4d05e2 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -15,6 +15,13 @@ from zulip import ModifiableMessageFlag # directly modifiable read/starred/collapsed +# This marks imported names that are intended for importing elsewhere +__all__ = [ + "EditPropagateMode", + "EmojiType", +] + + RESOLVED_TOPIC_PREFIX = "✔ " ############################################################################### From a79451bf60d5641d34b213536a5dcffc1fdd8085 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 12 Jul 2023 10:29:45 -0700 Subject: [PATCH 119/276] refactor: model: Define & type user data together in model initializer. Some fields were previously only defined in Model.get_all_users, but it is cleaner to initialize them to sane values prior to that method call. Others were defined above that call, but not directly, making it less clear that they were directly updated by it. --- zulipterminal/model.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 704c3b27f5..a20a2cf2c4 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -168,12 +168,13 @@ def __init__(self, controller: Any) -> None: # lose any updates while messages are being fetched. self._fetch_initial_data() - self._all_users_by_id: Dict[int, RealmUser] = {} - self._cross_realm_bots_by_id: Dict[int, RealmUser] = {} - self.server_version = self.initial_data["zulip_version"] self.server_feature_level = self.initial_data.get("zulip_feature_level") + self.user_dict: Dict[str, Dict[str, Any]] = {} + self.user_id_email_dict: Dict[int, str] = {} + self._all_users_by_id: Dict[int, RealmUser] = {} + self._cross_realm_bots_by_id: Dict[int, RealmUser] = {} self.users = self.get_all_users() self.stream_dict: Dict[int, Any] = {} @@ -1093,8 +1094,8 @@ def get_all_users(self) -> List[Dict[str, Any]]: # Construct a dict of each user in the realm to look up by email # and a user-id to email mapping - self.user_dict: Dict[str, Dict[str, Any]] = dict() - self.user_id_email_dict: Dict[int, str] = dict() + self.user_dict = dict() + self.user_id_email_dict = dict() for user in self.initial_data["realm_users"]: if self.user_id == user["user_id"]: self._all_users_by_id[self.user_id] = user From cdceb62071186804ad49458e88aee327cdcc6d9f Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 12 Jul 2023 10:34:52 -0700 Subject: [PATCH 120/276] refactor: model: Rename Model.get_all_users to reflect functionality. This method does currently return some data, but also updates other data as a side-effect, and is now only used internally to synchronize users data from 'initial data' from API register() calls, or subsequent updates to that data from events. The revised name of _update_users_data_from_initial_data reflects these factors. Tests updated. --- tests/model/test_model.py | 20 +++++++++++--------- zulipterminal/model.py | 6 +++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index aa5c9bf587..52b686a990 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -44,7 +44,7 @@ def mock_external_classes(self, mocker: Any) -> None: def model(self, mocker, initial_data, user_profile, unicode_emojis): mocker.patch(MODEL + ".get_messages", return_value="") self.client.register.return_value = initial_data - mocker.patch(MODEL + ".get_all_users", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) # NOTE: PATCH WHERE USED NOT WHERE DEFINED self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] @@ -85,7 +85,7 @@ def test_init( assert model.user_email == user_profile["email"] assert model.server_name == initial_data["realm_name"] # FIXME Add test here for model.server_url - model.get_all_users.assert_called_once_with() + model._update_users_data_from_initial_data.assert_called_once_with() assert model.users == [] self.classify_unread_counts.assert_called_once_with(model) assert model.unread_counts == [] @@ -192,7 +192,7 @@ def test_init_InvalidAPIKey_response(self, mocker, initial_data): MODEL + "._register_desired_events", return_value="Invalid API key" ) - mocker.patch(MODEL + ".get_all_users", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) mocker.patch(MODEL + "._subscribe_to_streams") self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] @@ -210,7 +210,7 @@ def test_init_ZulipError_exception(self, mocker, initial_data, exception_text="X MODEL + "._register_desired_events", side_effect=ZulipError(exception_text) ) - mocker.patch(MODEL + ".get_all_users", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) mocker.patch(MODEL + "._subscribe_to_streams") self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] @@ -223,7 +223,7 @@ def test_init_ZulipError_exception(self, mocker, initial_data, exception_text="X def test_register_initial_desired_events(self, mocker, initial_data): mocker.patch(MODEL + ".get_messages", return_value="") - mocker.patch(MODEL + ".get_all_users") + mocker.patch(MODEL + "._update_users_data_from_initial_data") self.client.register.return_value = initial_data model = Model(self.controller) @@ -1283,7 +1283,7 @@ def test_success_get_messages( num_after=10, ): self.client.register.return_value = initial_data - mocker.patch(MODEL + ".get_all_users", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) mocker.patch(MODEL + "._subscribe_to_streams") self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] @@ -1406,7 +1406,7 @@ def test_get_message_false_first_anchor( # Initialize Model self.client.register.return_value = initial_data - mocker.patch(MODEL + ".get_all_users", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) mocker.patch(MODEL + "._subscribe_to_streams") self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] @@ -1434,7 +1434,7 @@ def test_fail_get_messages( ): # Initialize Model self.client.register.return_value = initial_data - mocker.patch(MODEL + ".get_all_users", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) mocker.patch(MODEL + "._subscribe_to_streams") self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] @@ -1687,7 +1687,9 @@ def test_get_user_info_sample_response( model._all_users_by_id = _all_users_by_id assert model.get_user_info(12) == tidied_user_info_response - def test_get_all_users(self, mocker, initial_data, user_list, user_dict, user_id): + def test__update_users_data_from_initial_data( + self, mocker, initial_data, user_list, user_dict, user_id + ): mocker.patch(MODEL + ".get_messages", return_value="") self.client.register.return_value = initial_data mocker.patch(MODEL + "._subscribe_to_streams") diff --git a/zulipterminal/model.py b/zulipterminal/model.py index a20a2cf2c4..043c657f84 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -175,7 +175,7 @@ def __init__(self, controller: Any) -> None: self.user_id_email_dict: Dict[int, str] = {} self._all_users_by_id: Dict[int, RealmUser] = {} self._cross_realm_bots_by_id: Dict[int, RealmUser] = {} - self.users = self.get_all_users() + self.users = self._update_users_data_from_initial_data() self.stream_dict: Dict[int, Any] = {} self.muted_streams: Set[int] = set() @@ -437,7 +437,7 @@ def _start_presence_updates(self) -> None: response = self._notify_server_of_presence() if response["result"] == "success": self.initial_data["presences"] = response["presences"] - self.users = self.get_all_users() + self.users = self._update_users_data_from_initial_data() if hasattr(self.controller, "view"): view = self.controller.view view.users_view.update_user_list(user_list=self.users) @@ -1088,7 +1088,7 @@ def get_user_info(self, user_id: int) -> Optional[TidiedUserInfo]: return user_info - def get_all_users(self) -> List[Dict[str, Any]]: + def _update_users_data_from_initial_data(self) -> List[Dict[str, Any]]: # Dict which stores the active/idle status of users (by email) presences = self.initial_data["presences"] From 13d66a9282034666a08483565c789fb4182e7ad5 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 12 Jul 2023 10:57:57 -0700 Subject: [PATCH 121/276] refactor: helper/ui_mappings/model: Move Literal types to helper. StreamAccessType and UserStatus are internal types used to key into ui_mappings dicts. Since they are not part of the UI or Zulip API directly, other files seeking to import them would be better served importing from helper.py instead of the UI-centric mappings file. --- zulipterminal/config/ui_mappings.py | 10 ++-------- zulipterminal/helper.py | 9 +++++++-- zulipterminal/model.py | 8 ++------ 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/zulipterminal/config/ui_mappings.py b/zulipterminal/config/ui_mappings.py index 8f94c5f3d6..8f8392c1f8 100644 --- a/zulipterminal/config/ui_mappings.py +++ b/zulipterminal/config/ui_mappings.py @@ -4,8 +4,6 @@ from typing import Dict -from typing_extensions import Literal - from zulipterminal.api_types import EditPropagateMode from zulipterminal.config.symbols import ( BOT_MARKER, @@ -17,6 +15,7 @@ STREAM_MARKER_PUBLIC, STREAM_MARKER_WEB_PUBLIC, ) +from zulipterminal.helper import StreamAccessType, UserStatus EDIT_MODE_CAPTIONS: Dict[EditPropagateMode, str] = { @@ -25,9 +24,6 @@ "change_all": "Also change previous and following messages to this topic", } - -UserStatus = Literal["active", "idle", "offline", "inactive", "bot"] - # Mapping that binds user activity status to corresponding markers. # NOTE: Ordering of keys affects display order STATE_ICON: Dict[UserStatus, str] = { @@ -39,9 +35,7 @@ } -StreamAccessType = Literal["public", "private", "web-public"] - -STREAM_ACCESS_TYPE = { +STREAM_ACCESS_TYPE: Dict[StreamAccessType, Dict[str, str]] = { "public": {"description": "Public", "icon": STREAM_MARKER_PUBLIC}, "private": {"description": "Private", "icon": STREAM_MARKER_PRIVATE}, "web-public": {"description": "Web public", "icon": STREAM_MARKER_WEB_PUBLIC}, diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index 318c55b118..6e614164ae 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -30,7 +30,7 @@ from urllib.parse import unquote import requests -from typing_extensions import ParamSpec, TypedDict +from typing_extensions import Literal, ParamSpec, TypedDict from zulipterminal.api_types import Composition, EmojiType, Message from zulipterminal.config.keys import primary_key_for_command @@ -39,7 +39,6 @@ REGEX_COLOR_6_DIGIT, REGEX_QUOTED_FENCE_LENGTH, ) -from zulipterminal.config.ui_mappings import StreamAccessType from zulipterminal.platform_code import ( PLATFORM, normalized_file_path, @@ -47,6 +46,9 @@ ) +StreamAccessType = Literal["public", "private", "web-public"] + + class StreamData(TypedDict): name: str id: int @@ -86,6 +88,9 @@ class TidiedUserInfo(TypedDict): bot_owner_name: str +UserStatus = Literal["active", "idle", "offline", "inactive", "bot"] + + class Index(TypedDict): pointer: Dict[str, Optional[int]] # narrow_str, message_id (or no data) # Various sets of downloaded message ids (all, starred, ...) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 043c657f84..2a9678f7a0 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -55,15 +55,11 @@ ) from zulipterminal.config.keys import primary_key_for_command from zulipterminal.config.symbols import STREAM_TOPIC_SEPARATOR -from zulipterminal.config.ui_mappings import ( - EDIT_TOPIC_POLICY, - ROLE_BY_ID, - STATE_ICON, - StreamAccessType, -) +from zulipterminal.config.ui_mappings import EDIT_TOPIC_POLICY, ROLE_BY_ID, STATE_ICON from zulipterminal.helper import ( CustomProfileData, NamedEmojiData, + StreamAccessType, StreamData, TidiedUserInfo, asynch, From 1fe91e7c059edd86b8fae7d86c67f7a8aa2b6e4b Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 12 Jul 2023 13:41:30 -0700 Subject: [PATCH 122/276] refactor: helper/model: Introduce MinimalUserData to type user data. Fixture and tests updated. --- tests/conftest.py | 9 +++++++-- tests/core/test_core.py | 9 ++++++++- tests/ui_tools/test_boxes.py | 4 ++-- zulipterminal/helper.py | 7 +++++++ zulipterminal/model.py | 16 ++++++++++------ 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5f18bfc909..b42e955cbf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,12 @@ keys_for_command, primary_key_for_command, ) -from zulipterminal.helper import CustomProfileData, Index, TidiedUserInfo +from zulipterminal.helper import ( + CustomProfileData, + Index, + MinimalUserData, + TidiedUserInfo, +) from zulipterminal.helper import initial_index as helper_initial_index from zulipterminal.ui_tools.buttons import StreamButton, TopicButton, UserButton from zulipterminal.ui_tools.messages import MessageBox @@ -1227,7 +1232,7 @@ def error_response() -> Dict[str, str]: @pytest.fixture -def user_dict(logged_on_user: Dict[str, Any]) -> Dict[str, Dict[str, Any]]: +def user_dict(logged_on_user: Dict[str, Any]) -> Dict[str, MinimalUserData]: """ User_dict created according to `initial_data` fixture. """ diff --git a/tests/core/test_core.py b/tests/core/test_core.py index 17517d77f7..9f13a4061a 100644 --- a/tests/core/test_core.py +++ b/tests/core/test_core.py @@ -222,7 +222,14 @@ def test_narrow_to_user( controller.view.message_view = mocker.patch("urwid.ListBox") controller.model.user_id = 5140 controller.model.user_email = "some@email" - controller.model.user_dict = {user_email: {"user_id": user_id}} + controller.model.user_dict = { + user_email: { + "user_id": user_id, + "full_name": "", + "email": "", + "status": "active", + } + } emails = [user_email] diff --git a/tests/ui_tools/test_boxes.py b/tests/ui_tools/test_boxes.py index 975b14c416..869cc981b9 100644 --- a/tests/ui_tools/test_boxes.py +++ b/tests/ui_tools/test_boxes.py @@ -15,7 +15,7 @@ STREAM_MARKER_WEB_PUBLIC, ) from zulipterminal.config.ui_mappings import StreamAccessType -from zulipterminal.helper import Index +from zulipterminal.helper import Index, MinimalUserData from zulipterminal.ui_tools.boxes import PanelSearchBox, WriteBox, _MessageEditState from zulipterminal.urwid_types import urwid_Size @@ -40,7 +40,7 @@ def write_box( user_groups_fixture: List[Dict[str, Any]], streams_fixture: List[Dict[str, Any]], unicode_emojis: "OrderedDict[str, Dict[str, Any]]", - user_dict: Dict[str, Dict[str, Any]], + user_dict: Dict[str, MinimalUserData], ) -> WriteBox: self.view.model.active_emoji_data = unicode_emojis self.view.model.all_emoji_names = list(unicode_emojis.keys()) diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index 6e614164ae..ce77c52a8c 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -91,6 +91,13 @@ class TidiedUserInfo(TypedDict): UserStatus = Literal["active", "idle", "offline", "inactive", "bot"] +class MinimalUserData(TypedDict): + full_name: str + email: str + user_id: int + status: UserStatus + + class Index(TypedDict): pointer: Dict[str, Optional[int]] # narrow_str, message_id (or no data) # Various sets of downloaded message ids (all, starred, ...) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 2a9678f7a0..b00c5efce5 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -58,10 +58,12 @@ from zulipterminal.config.ui_mappings import EDIT_TOPIC_POLICY, ROLE_BY_ID, STATE_ICON from zulipterminal.helper import ( CustomProfileData, + MinimalUserData, NamedEmojiData, StreamAccessType, StreamData, TidiedUserInfo, + UserStatus, asynch, canonicalize_color, classify_unread_counts, @@ -167,11 +169,11 @@ def __init__(self, controller: Any) -> None: self.server_version = self.initial_data["zulip_version"] self.server_feature_level = self.initial_data.get("zulip_feature_level") - self.user_dict: Dict[str, Dict[str, Any]] = {} + self.user_dict: Dict[str, MinimalUserData] = {} self.user_id_email_dict: Dict[int, str] = {} self._all_users_by_id: Dict[int, RealmUser] = {} self._cross_realm_bots_by_id: Dict[int, RealmUser] = {} - self.users = self._update_users_data_from_initial_data() + self.users: List[MinimalUserData] = self._update_users_data_from_initial_data() self.stream_dict: Dict[int, Any] = {} self.muted_streams: Set[int] = set() @@ -1062,7 +1064,7 @@ def get_user_info(self, user_id: int) -> Optional[TidiedUserInfo]: last_active="", ) - bot_owner: Optional[Union[RealmUser, Dict[str, Any]]] = None + bot_owner: Optional[Union[RealmUser, MinimalUserData]] = None if api_user_data.get("bot_owner_id", None): bot_owner = self._all_users_by_id.get(api_user_data["bot_owner_id"], None) @@ -1084,7 +1086,7 @@ def get_user_info(self, user_id: int) -> Optional[TidiedUserInfo]: return user_info - def _update_users_data_from_initial_data(self) -> List[Dict[str, Any]]: + def _update_users_data_from_initial_data(self) -> List[MinimalUserData]: # Dict which stores the active/idle status of users (by email) presences = self.initial_data["presences"] @@ -1095,7 +1097,7 @@ def _update_users_data_from_initial_data(self) -> List[Dict[str, Any]]: for user in self.initial_data["realm_users"]: if self.user_id == user["user_id"]: self._all_users_by_id[self.user_id] = user - current_user = { + current_user: MinimalUserData = { "full_name": user["full_name"], "email": user["email"], "user_id": user["user_id"], @@ -1103,6 +1105,8 @@ def _update_users_data_from_initial_data(self) -> List[Dict[str, Any]]: } continue email = user["email"] + + status: UserStatus if user["is_bot"]: # Bot has no dynamic status, so avoid presence lookup status = "bot" @@ -1128,7 +1132,7 @@ def _update_users_data_from_initial_data(self) -> List[Dict[str, Any]]: * If there are several ClientPresence objects with the greatest * UserStatus, an arbitrary one is chosen. """ - aggregate_status = "offline" + aggregate_status: UserStatus = "offline" for client in presences[email].items(): client_name = client[0] status = client[1]["status"] From 8551c142bb389095dafe800d07e1a87bc92787e1 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 12 Jul 2023 11:37:06 -0700 Subject: [PATCH 123/276] refactor: model: Avoid Model.users special treatment via return value. _update_users_data_from_initial_data also updates other model attributes, so it's clearer to not treat it differently. Tests updated. --- tests/model/test_model.py | 12 ++++++------ zulipterminal/model.py | 9 +++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 52b686a990..2bf150a2f6 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -44,7 +44,7 @@ def mock_external_classes(self, mocker: Any) -> None: def model(self, mocker, initial_data, user_profile, unicode_emojis): mocker.patch(MODEL + ".get_messages", return_value="") self.client.register.return_value = initial_data - mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data") # NOTE: PATCH WHERE USED NOT WHERE DEFINED self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] @@ -192,7 +192,7 @@ def test_init_InvalidAPIKey_response(self, mocker, initial_data): MODEL + "._register_desired_events", return_value="Invalid API key" ) - mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data") mocker.patch(MODEL + "._subscribe_to_streams") self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] @@ -210,7 +210,7 @@ def test_init_ZulipError_exception(self, mocker, initial_data, exception_text="X MODEL + "._register_desired_events", side_effect=ZulipError(exception_text) ) - mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data") mocker.patch(MODEL + "._subscribe_to_streams") self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] @@ -1283,7 +1283,7 @@ def test_success_get_messages( num_after=10, ): self.client.register.return_value = initial_data - mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data") mocker.patch(MODEL + "._subscribe_to_streams") self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] @@ -1406,7 +1406,7 @@ def test_get_message_false_first_anchor( # Initialize Model self.client.register.return_value = initial_data - mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data") mocker.patch(MODEL + "._subscribe_to_streams") self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] @@ -1434,7 +1434,7 @@ def test_fail_get_messages( ): # Initialize Model self.client.register.return_value = initial_data - mocker.patch(MODEL + "._update_users_data_from_initial_data", return_value=[]) + mocker.patch(MODEL + "._update_users_data_from_initial_data") mocker.patch(MODEL + "._subscribe_to_streams") self.classify_unread_counts = mocker.patch( MODULE + ".classify_unread_counts", return_value=[] diff --git a/zulipterminal/model.py b/zulipterminal/model.py index b00c5efce5..82d809a24c 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -173,7 +173,8 @@ def __init__(self, controller: Any) -> None: self.user_id_email_dict: Dict[int, str] = {} self._all_users_by_id: Dict[int, RealmUser] = {} self._cross_realm_bots_by_id: Dict[int, RealmUser] = {} - self.users: List[MinimalUserData] = self._update_users_data_from_initial_data() + self.users: List[MinimalUserData] = [] + self._update_users_data_from_initial_data() self.stream_dict: Dict[int, Any] = {} self.muted_streams: Set[int] = set() @@ -435,7 +436,7 @@ def _start_presence_updates(self) -> None: response = self._notify_server_of_presence() if response["result"] == "success": self.initial_data["presences"] = response["presences"] - self.users = self._update_users_data_from_initial_data() + self._update_users_data_from_initial_data() if hasattr(self.controller, "view"): view = self.controller.view view.users_view.update_user_list(user_list=self.users) @@ -1086,7 +1087,7 @@ def get_user_info(self, user_id: int) -> Optional[TidiedUserInfo]: return user_info - def _update_users_data_from_initial_data(self) -> List[MinimalUserData]: + def _update_users_data_from_initial_data(self) -> None: # Dict which stores the active/idle status of users (by email) presences = self.initial_data["presences"] @@ -1202,7 +1203,7 @@ def _update_users_data_from_initial_data(self) -> List[MinimalUserData]: self.user_dict[current_user["email"]] = current_user self.user_id_email_dict[self.user_id] = current_user["email"] - return user_list + self.users = user_list def user_name_from_id(self, user_id: int) -> str: """ From 3451c9b7f45d4b6ed0ae1adefe857e773d7be91f Mon Sep 17 00:00:00 2001 From: supascooopa Date: Mon, 24 Apr 2023 09:00:50 +0300 Subject: [PATCH 124/276] messages: Add stream access type markers to stream message headers. This improves the message UI by adding stream access type markers as prefixes to stream names, when rendering headers of stream messages in the messages view. This gives users an easier way to determine what type of stream the message belongs to. Tests and fixtures are updated accordingly. --- tests/conftest.py | 4 +++- tests/ui_tools/test_messages.py | 24 ++++++++++++++++++------ zulipterminal/ui_tools/messages.py | 5 ++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b42e955cbf..fe771d095c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -119,9 +119,11 @@ def msg_box( """ Mocked MessageBox with stream message """ + model_mock = mocker.patch("zulipterminal.model.Model") + model_mock.stream_access_type.return_value = "public" return MessageBox( messages_successful_response["messages"][0], - mocker.patch("zulipterminal.model.Model"), + model_mock, None, ) diff --git a/tests/ui_tools/test_messages.py b/tests/ui_tools/test_messages.py index db22da3d2b..8eafd2b951 100644 --- a/tests/ui_tools/test_messages.py +++ b/tests/ui_tools/test_messages.py @@ -11,6 +11,7 @@ from zulipterminal.config.symbols import ( QUOTED_TEXT_MARKER, STATUS_INACTIVE, + STREAM_MARKER_PUBLIC, STREAM_TOPIC_SEPARATOR, TIME_MENTION_MARKER, ) @@ -28,6 +29,7 @@ class TestMessageBox: def mock_external_classes(self, mocker, initial_index): self.model = mocker.MagicMock() self.model.index = initial_index + self.model.stream_access_type.return_value = "public" @pytest.mark.parametrize( "message_type, set_fields", @@ -893,19 +895,24 @@ def test_main_view_generates_PM_header( @pytest.mark.parametrize( "msg_narrow, msg_type, assert_header_bar, assert_search_bar", [ - ([], 0, f"PTEST {STREAM_TOPIC_SEPARATOR} ", "All messages"), + ( + [], + 0, + f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", + "All messages", + ), ([], 1, "You and ", "All messages"), ([], 2, "You and ", "All messages"), ( [["stream", "PTEST"]], 0, - f"PTEST {STREAM_TOPIC_SEPARATOR} ", + f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", ("bar", [("s#bd6", "PTEST")]), ), ( [["stream", "PTEST"], ["topic", "b"]], 0, - f"PTEST {STREAM_TOPIC_SEPARATOR}", + f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR}", ("bar", [("s#bd6", "PTEST"), ("s#bd6", ": topic narrow")]), ), ([["is", "private"]], 1, "You and ", "All direct messages"), @@ -925,7 +932,7 @@ def test_main_view_generates_PM_header( ( [["is", "starred"]], 0, - f"PTEST {STREAM_TOPIC_SEPARATOR} ", + f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", "Starred messages", ), ([["is", "starred"]], 1, "You and ", "Starred messages"), @@ -934,10 +941,15 @@ def test_main_view_generates_PM_header( ( [["search", "FOO"]], 0, - f"PTEST {STREAM_TOPIC_SEPARATOR} ", + f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", "All messages", ), - ([["is", "mentioned"]], 0, f"PTEST {STREAM_TOPIC_SEPARATOR} ", "Mentions"), + ( + [["is", "mentioned"]], + 0, + f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", + "Mentions", + ), ([["is", "mentioned"]], 1, "You and ", "Mentions"), ([["is", "mentioned"]], 2, "You and ", "Mentions"), ([["is", "mentioned"], ["search", "FOO"]], 1, "You and ", "Mentions"), diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index e73143ad66..bc07bdccb1 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -24,7 +24,7 @@ STREAM_TOPIC_SEPARATOR, TIME_MENTION_MARKER, ) -from zulipterminal.config.ui_mappings import STATE_ICON +from zulipterminal.config.ui_mappings import STATE_ICON, STREAM_ACCESS_TYPE from zulipterminal.helper import get_unused_fence from zulipterminal.server_url import near_message_url from zulipterminal.ui_tools.tables import render_table @@ -153,9 +153,12 @@ def stream_header(self) -> Any: assert self.stream_id is not None color = self.model.stream_dict[self.stream_id]["color"] bar_color = f"s{color}" + stream_access_type = self.model.stream_access_type(self.stream_id) + stream_icon = STREAM_ACCESS_TYPE[stream_access_type]["icon"] stream_title_markup = ( "bar", [ + (bar_color, f" {stream_icon} "), (bar_color, f"{self.stream_name} {STREAM_TOPIC_SEPARATOR} "), ("title", f" {self.topic_name}"), ], From 2781e34514be0e606a3f88ebf8fd409781d9f625 Mon Sep 17 00:00:00 2001 From: supascooopa Date: Thu, 6 Jul 2023 11:15:27 -0700 Subject: [PATCH 125/276] symbols/messages: Add direct message marker to direct message headers. The previous commit added a prefix marker for headers of stream messages; this commit does the same for headers of direct messages. No symbol was previously defined for direct messages, so one is added, and applied in a similar way as with stream messages. Tests updated. --- tests/ui_tools/test_messages.py | 65 ++++++++++++++++++++++++------ zulipterminal/config/symbols.py | 1 + zulipterminal/ui_tools/messages.py | 7 +++- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/tests/ui_tools/test_messages.py b/tests/ui_tools/test_messages.py index 8eafd2b951..c87cc07203 100644 --- a/tests/ui_tools/test_messages.py +++ b/tests/ui_tools/test_messages.py @@ -9,6 +9,7 @@ from zulipterminal.config.keys import keys_for_command from zulipterminal.config.symbols import ( + DIRECT_MESSAGE_MARKER, QUOTED_TEXT_MARKER, STATUS_INACTIVE, STREAM_MARKER_PUBLIC, @@ -901,8 +902,8 @@ def test_main_view_generates_PM_header( f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", "All messages", ), - ([], 1, "You and ", "All messages"), - ([], 2, "You and ", "All messages"), + ([], 1, f" {DIRECT_MESSAGE_MARKER} You and ", "All messages"), + ([], 2, f" {DIRECT_MESSAGE_MARKER} You and ", "All messages"), ( [["stream", "PTEST"]], 0, @@ -915,18 +916,28 @@ def test_main_view_generates_PM_header( f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR}", ("bar", [("s#bd6", "PTEST"), ("s#bd6", ": topic narrow")]), ), - ([["is", "private"]], 1, "You and ", "All direct messages"), - ([["is", "private"]], 2, "You and ", "All direct messages"), + ( + [["is", "private"]], + 1, + f" {DIRECT_MESSAGE_MARKER} You and ", + "All direct messages", + ), + ( + [["is", "private"]], + 2, + f" {DIRECT_MESSAGE_MARKER} You and ", + "All direct messages", + ), ( [["pm-with", "boo@zulip.com"]], 1, - "You and ", + f" {DIRECT_MESSAGE_MARKER} You and ", "Direct message conversation", ), ( [["pm-with", "boo@zulip.com, bar@zulip.com"]], 2, - "You and ", + f" {DIRECT_MESSAGE_MARKER} You and ", "Group direct message conversation", ), ( @@ -935,9 +946,24 @@ def test_main_view_generates_PM_header( f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", "Starred messages", ), - ([["is", "starred"]], 1, "You and ", "Starred messages"), - ([["is", "starred"]], 2, "You and ", "Starred messages"), - ([["is", "starred"], ["search", "FOO"]], 1, "You and ", "Starred messages"), + ( + [["is", "starred"]], + 1, + f" {DIRECT_MESSAGE_MARKER} You and ", + "Starred messages", + ), + ( + [["is", "starred"]], + 2, + f" {DIRECT_MESSAGE_MARKER} You and ", + "Starred messages", + ), + ( + [["is", "starred"], ["search", "FOO"]], + 1, + f" {DIRECT_MESSAGE_MARKER} You and ", + "Starred messages", + ), ( [["search", "FOO"]], 0, @@ -950,9 +976,24 @@ def test_main_view_generates_PM_header( f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", "Mentions", ), - ([["is", "mentioned"]], 1, "You and ", "Mentions"), - ([["is", "mentioned"]], 2, "You and ", "Mentions"), - ([["is", "mentioned"], ["search", "FOO"]], 1, "You and ", "Mentions"), + ( + [["is", "mentioned"]], + 1, + f" {DIRECT_MESSAGE_MARKER} You and ", + "Mentions", + ), + ( + [["is", "mentioned"]], + 2, + f" {DIRECT_MESSAGE_MARKER} You and ", + "Mentions", + ), + ( + [["is", "mentioned"], ["search", "FOO"]], + 1, + f" {DIRECT_MESSAGE_MARKER} You and ", + "Mentions", + ), ], ) def test_msg_generates_search_and_header_bar( diff --git a/zulipterminal/config/symbols.py b/zulipterminal/config/symbols.py index 4782f4c500..153e76c274 100644 --- a/zulipterminal/config/symbols.py +++ b/zulipterminal/config/symbols.py @@ -8,6 +8,7 @@ # Suffix comments indicate: unicode name, codepoint (unicode block, version if not v1.1) INVALID_MARKER = "✗" # BALLOT X, U+2717 (Dingbats) +DIRECT_MESSAGE_MARKER = "§" # SECTION SIGN, U+00A7 (Latin-1 supplement) STREAM_MARKER_PRIVATE = "P" STREAM_MARKER_PUBLIC = "#" STREAM_MARKER_WEB_PUBLIC = "⊚" # CIRCLED RING OPERATOR, U+229A (Mathematical operators) diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index bc07bdccb1..47600b4606 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -18,6 +18,7 @@ from zulipterminal.api_types import Message from zulipterminal.config.keys import is_command_key, primary_key_for_command from zulipterminal.config.symbols import ( + DIRECT_MESSAGE_MARKER, MESSAGE_CONTENT_MARKER, MESSAGE_HEADER_DIVIDER, QUOTED_TEXT_MARKER, @@ -177,7 +178,11 @@ def stream_header(self) -> Any: def private_header(self) -> Any: title_markup = ( "header", - [("general_narrow", "You and "), ("general_narrow", self.recipients_names)], + [ + ("general_narrow", f" {DIRECT_MESSAGE_MARKER} "), + ("general_narrow", "You and "), + ("general_narrow", self.recipients_names), + ], ) title = urwid.Text(title_markup) header = urwid.Columns( From 991f648de851627819372a7c22dd2f6dfeadb679 Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Fri, 7 Apr 2023 17:03:44 -0700 Subject: [PATCH 126/276] model: Add method to get stream topic from message id. This commit adds a stream_topic_from_message_id method which returns the topic of the currently focused message. This is done in preparation of the change in algorithm of the get_next_unread_topic function. Tests added. --- tests/model/test_model.py | 15 +++++++++++++++ zulipterminal/model.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 2bf150a2f6..c461e5b799 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -4043,6 +4043,21 @@ def test_get_next_unread_pm_no_unread(self, model): assert return_value is None assert model.last_unread_pm is None + @pytest.mark.parametrize( + "message_id, expected_value", + [ + case(537286, (205, "Test"), id="stream_message"), + case(537287, None, id="direct_message"), + case(537289, None, id="non-existent message"), + ], + ) + def test_stream_topic_from_message_id( + self, mocker, model, message_id, expected_value, empty_index + ): + model.index = empty_index + current_topic = model.stream_topic_from_message_id(message_id) + assert current_topic == expected_value + @pytest.mark.parametrize( "stream_id, expected_response", [ diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 82d809a24c..08ae6989ba 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -886,6 +886,21 @@ def is_muted_topic(self, stream_id: int, topic: str) -> bool: topic_to_search = (stream_name, topic) return topic_to_search in self._muted_topics + def stream_topic_from_message_id( + self, message_id: int + ) -> Optional[Tuple[int, str]]: + """ + Returns the stream and topic of a message of a given message id. + If the message is not a stream message or if it is not present in the index, + None is returned. + """ + message = self.index["messages"].get(message_id, None) + if message is not None and message["type"] == "stream": + stream_id = message["stream_id"] + topic = message["subject"] + return (stream_id, topic) + return None + def get_next_unread_topic(self) -> Optional[Tuple[int, str]]: unread_topics = sorted(self.unread_counts["unread_topics"].keys()) next_topic = False From 5e70c9afe68e26c69d26fdfdc0b5c1661523724a Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Sun, 26 Mar 2023 14:52:17 -0700 Subject: [PATCH 127/276] model/views: Fetch next unread topic using current message state. This commit changes the behavior of the get_next_unread_topic method to use the current message state (ie. the current topic) in calculating the next unread topic to be cycled to. If there is no current message available, ie. an empty narrow with no focus, the logic attempts to use the current narrow to determine the best course of action. This replaces the previous approach of using a _last_unread_topic stored in the model. Tests updated. --- tests/model/test_model.py | 64 +++++++++++++++++++++++++++++---- tests/ui/test_ui_tools.py | 2 ++ zulipterminal/model.py | 24 ++++++++++--- zulipterminal/ui_tools/views.py | 15 ++++++-- 4 files changed, 91 insertions(+), 14 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index c461e5b799..0432ecf192 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -72,7 +72,6 @@ def test_init( assert model.stream_dict == stream_dict assert model.recipients == frozenset() assert model.index == initial_index - assert model._last_unread_topic is None assert model.last_unread_pm is None model.get_messages.assert_called_once_with( num_before=30, num_after=10, anchor=None @@ -3900,7 +3899,7 @@ def test_is_muted_topic( assert return_value == is_muted @pytest.mark.parametrize( - "unread_topics, last_unread_topic, next_unread_topic", + "unread_topics, current_topic, next_unread_topic", [ case( {(1, "topic"), (2, "topic2")}, @@ -3929,7 +3928,7 @@ def test_is_muted_topic( case( {}, (1, "topic"), - None, + (1, "topic"), id="no_unreads_with_previous_topic_state", ), case({}, None, None, id="no_unreads_with_no_previous_topic_state"), @@ -3948,7 +3947,7 @@ def test_is_muted_topic( case( {(3, "topic3")}, (2, "topic2"), - None, + (2, "topic2"), id="unread_present_only_in_muted_stream", ), case( @@ -3993,16 +3992,36 @@ def test_is_muted_topic( (2, "topic2"), id="unread_present_after_previous_topic_muted", ), + case( + {(1, "topic1"), (2, "topic2"), (2, "topic2 muted")}, + (2, "topic1"), + (2, "topic2"), + id="unmuted_unread_present_in_same_stream_as_current_topic_not_in_unread_list", + ), + case( + {(1, "topic1"), (2, "topic2 muted"), (4, "topic4")}, + (2, "topic1"), + (4, "topic4"), + id="unmuted_unread_present_in_next_stream_as_current_topic_not_in_unread_list", + ), + case( + {(1, "topic1"), (2, "topic2 muted"), (3, "topic3")}, + (2, "topic1"), + (1, "topic1"), + id="unmuted_unread_not_present_in_next_stream_as_current_topic_not_in_unread_list", + ), ], ) def test_get_next_unread_topic( - self, model, unread_topics, last_unread_topic, next_unread_topic + self, mocker, model, unread_topics, current_topic, next_unread_topic ): # NOTE Not important how many unreads per topic, so just use '1' model.unread_counts = { "unread_topics": {stream_topic: 1 for stream_topic in unread_topics} } - model._last_unread_topic = last_unread_topic + + current_message_id = 10 # Arbitrary value due to mock below + model.stream_topic_from_message_id = mocker.Mock(return_value=current_topic) # Minimal extra streams for muted stream testing (should not exist otherwise) assert {3, 4} & set(model.stream_dict) == set() @@ -4020,7 +4039,38 @@ def test_get_next_unread_topic( ] } - unread_topic = model.get_next_unread_topic() + unread_topic = model.get_next_unread_topic(current_message=current_message_id) + + assert unread_topic == next_unread_topic + + @pytest.mark.parametrize( + "unread_topics, empty_narrow, narrow_stream_id, next_unread_topic", + [ + case( + {(1, "topic1"), (1, "topic2"), (2, "topic3")}, + [["stream", "Stream 1"], ["topic", "topic1.5"]], + 1, + (1, "topic2"), + ), + ], + ) + def test_get_next_unread_topic__empty_narrow( + self, + mocker, + model, + unread_topics, + empty_narrow, + narrow_stream_id, + next_unread_topic, + ): + # NOTE Not important how many unreads per topic, so just use '1' + model.unread_counts = { + "unread_topics": {stream_topic: 1 for stream_topic in unread_topics} + } + model.stream_id_from_name = mocker.Mock(return_value=narrow_stream_id) + model.narrow = empty_narrow + + unread_topic = model.get_next_unread_topic(current_message=None) assert unread_topic == next_unread_topic diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index c04c9c3e2a..b84c613f0e 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -887,6 +887,7 @@ def test_keypress_NEXT_UNREAD_TOPIC_stream( ): size = widget_size(mid_col_view) mocker.patch(MIDCOLVIEW + ".focus_position") + mocker.patch.object(self.view, "message_view") mid_col_view.model.stream_dict = {1: {"name": "stream"}} mid_col_view.model.get_next_unread_topic.return_value = (1, "topic") @@ -904,6 +905,7 @@ def test_keypress_NEXT_UNREAD_TOPIC_no_stream( ): size = widget_size(mid_col_view) mocker.patch(MIDCOLVIEW + ".focus_position") + mocker.patch.object(self.view, "message_view") mid_col_view.model.get_next_unread_topic.return_value = None return_value = mid_col_view.keypress(size, key) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 08ae6989ba..119e92e28d 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -2,6 +2,7 @@ Defines the `Model`, fetching and storing data retrieved from the Zulip server """ +import bisect import itertools import json import time @@ -111,7 +112,6 @@ def __init__(self, controller: Any) -> None: self.stream_id: Optional[int] = None self.recipients: FrozenSet[Any] = frozenset() self.index = initial_index - self._last_unread_topic = None self.last_unread_pm = None self.user_id = -1 @@ -901,11 +901,26 @@ def stream_topic_from_message_id( return (stream_id, topic) return None - def get_next_unread_topic(self) -> Optional[Tuple[int, str]]: + def get_next_unread_topic( + self, current_message: Optional[int] + ) -> Optional[Tuple[int, str]]: + if current_message: + current_topic = self.stream_topic_from_message_id(current_message) + else: + current_topic = ( + self.stream_id_from_name(self.narrow[0][1]), + self.narrow[1][1], + ) unread_topics = sorted(self.unread_counts["unread_topics"].keys()) next_topic = False - if self._last_unread_topic not in unread_topics: + if current_topic is None: next_topic = True + elif current_topic not in unread_topics: + # insert current_topic in list of unread_topics for the case where + # current_topic is not in unread_topics, and the next unmuted topic + # is to be returned. This does not modify the original unread topics + # data, and is just used to compute the next unmuted topic to be returned. + bisect.insort(unread_topics, current_topic) # loop over unread_topics list twice for the case that last_unread_topic was # the last valid unread_topic in unread_topics list. for unread_topic in unread_topics * 2: @@ -915,9 +930,8 @@ def get_next_unread_topic(self) -> Optional[Tuple[int, str]]: and not self.is_muted_stream(stream_id) and next_topic ): - self._last_unread_topic = unread_topic return unread_topic - if unread_topic == self._last_unread_topic: + if unread_topic == current_topic: next_topic = True return None diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 64eebeb255..94864e16dd 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -584,9 +584,20 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: elif is_command_key("NEXT_UNREAD_TOPIC", key): # narrow to next unread topic - stream_topic = self.model.get_next_unread_topic() - if stream_topic is None: + focus = self.view.message_view.focus + narrow = self.model.narrow + if focus: + current_msg_id = focus.original_widget.message["id"] + stream_topic = self.model.get_next_unread_topic( + current_message=current_msg_id + ) + if stream_topic is None: + return key + elif narrow[0][0] == "stream" and narrow[1][0] == "topic": + stream_topic = self.model.get_next_unread_topic(current_message=None) + else: return key + stream_id, topic = stream_topic self.controller.narrow_to_topic( stream_name=self.model.stream_dict[stream_id]["name"], From f268174368320bd335bf5dd30aa6f0fe12536708 Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Sun, 16 Apr 2023 03:01:10 -0700 Subject: [PATCH 128/276] refactor: model/views: Rename "get_next_unread_topic" method. This commit renames the "get_next_unread_topic" function to "next_unread_topic_from_message_id". This makes the function name clearer by indicating that the function argument is the context from which we get the next unread topic. Tests updated to rename the function. --- tests/model/test_model.py | 8 ++++---- tests/ui/test_ui_tools.py | 4 ++-- zulipterminal/model.py | 2 +- zulipterminal/ui_tools/views.py | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 0432ecf192..23e464efe7 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -4012,7 +4012,7 @@ def test_is_muted_topic( ), ], ) - def test_get_next_unread_topic( + def test_next_unread_topic_from_message( self, mocker, model, unread_topics, current_topic, next_unread_topic ): # NOTE Not important how many unreads per topic, so just use '1' @@ -4039,7 +4039,7 @@ def test_get_next_unread_topic( ] } - unread_topic = model.get_next_unread_topic(current_message=current_message_id) + unread_topic = model.next_unread_topic_from_message_id(current_message_id) assert unread_topic == next_unread_topic @@ -4054,7 +4054,7 @@ def test_get_next_unread_topic( ), ], ) - def test_get_next_unread_topic__empty_narrow( + def test_next_unread_topic_from_message__empty_narrow( self, mocker, model, @@ -4070,7 +4070,7 @@ def test_get_next_unread_topic__empty_narrow( model.stream_id_from_name = mocker.Mock(return_value=narrow_stream_id) model.narrow = empty_narrow - unread_topic = model.get_next_unread_topic(current_message=None) + unread_topic = model.next_unread_topic_from_message_id(None) assert unread_topic == next_unread_topic diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index b84c613f0e..324ee048b0 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -890,7 +890,7 @@ def test_keypress_NEXT_UNREAD_TOPIC_stream( mocker.patch.object(self.view, "message_view") mid_col_view.model.stream_dict = {1: {"name": "stream"}} - mid_col_view.model.get_next_unread_topic.return_value = (1, "topic") + mid_col_view.model.next_unread_topic_from_message_id.return_value = (1, "topic") return_value = mid_col_view.keypress(size, key) @@ -906,7 +906,7 @@ def test_keypress_NEXT_UNREAD_TOPIC_no_stream( size = widget_size(mid_col_view) mocker.patch(MIDCOLVIEW + ".focus_position") mocker.patch.object(self.view, "message_view") - mid_col_view.model.get_next_unread_topic.return_value = None + mid_col_view.model.next_unread_topic_from_message_id.return_value = None return_value = mid_col_view.keypress(size, key) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 119e92e28d..bfc4020be7 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -901,7 +901,7 @@ def stream_topic_from_message_id( return (stream_id, topic) return None - def get_next_unread_topic( + def next_unread_topic_from_message_id( self, current_message: Optional[int] ) -> Optional[Tuple[int, str]]: if current_message: diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 94864e16dd..6a1e4e6c0e 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -588,13 +588,13 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: narrow = self.model.narrow if focus: current_msg_id = focus.original_widget.message["id"] - stream_topic = self.model.get_next_unread_topic( - current_message=current_msg_id + stream_topic = self.model.next_unread_topic_from_message_id( + current_msg_id ) if stream_topic is None: return key elif narrow[0][0] == "stream" and narrow[1][0] == "topic": - stream_topic = self.model.get_next_unread_topic(current_message=None) + stream_topic = self.model.next_unread_topic_from_message_id(None) else: return key From fad3af0a6e46ab9d4348c3e7a9570af444a52d22 Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Sun, 16 Apr 2023 01:37:36 -0700 Subject: [PATCH 129/276] model: Change next_unread_topic to return None if topic stays same. The next_unread_topic_from_message_id function returns the next topic to be narrowed to. If the topic remains same, there is no need to call narrow to the same topic again. This commit fixes this, without any change in user-facing behavior. Test case updated. --- tests/model/test_model.py | 8 ++++---- zulipterminal/model.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 23e464efe7..6099c1ac7f 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -3919,16 +3919,16 @@ def test_is_muted_topic( (1, "topic"), id="unread_present_before_previous_topic", ), - case( # TODO Should be None? (2 other cases) + case( {(1, "topic")}, (1, "topic"), - (1, "topic"), + None, id="unread_still_present_in_topic", ), case( {}, (1, "topic"), - (1, "topic"), + None, id="no_unreads_with_previous_topic_state", ), case({}, None, None, id="no_unreads_with_no_previous_topic_state"), @@ -3947,7 +3947,7 @@ def test_is_muted_topic( case( {(3, "topic3")}, (2, "topic2"), - (2, "topic2"), + None, id="unread_present_only_in_muted_stream", ), case( diff --git a/zulipterminal/model.py b/zulipterminal/model.py index bfc4020be7..468eb3e173 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -930,6 +930,8 @@ def next_unread_topic_from_message_id( and not self.is_muted_stream(stream_id) and next_topic ): + if unread_topic == current_topic: + return None return unread_topic if unread_topic == current_topic: next_topic = True From 00645fd0a044907095f1375153c484c779e7f170 Mon Sep 17 00:00:00 2001 From: Vishwesh Pillai Date: Sun, 28 May 2023 15:54:10 +0530 Subject: [PATCH 130/276] model: Add in-stream wrap-around behavior to next unread topic behavior. This commit aims to introduce in-stream wrap-around behavior to the next_unread_topic_from_message_id function if there are unread messages still present in the current stream. Test case added. --- tests/model/test_model.py | 6 ++++++ zulipterminal/model.py | 29 +++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 6099c1ac7f..fe2ba5535e 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -4010,6 +4010,12 @@ def test_is_muted_topic( (1, "topic1"), id="unmuted_unread_not_present_in_next_stream_as_current_topic_not_in_unread_list", ), + case( + {(1, "topic1"), (1, "topic11"), (2, "topic2")}, + (1, "topic11"), + (1, "topic1"), + id="unread_present_in_same_stream_wrap_around", + ), ], ) def test_next_unread_topic_from_message( diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 468eb3e173..7073e4f055 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -913,6 +913,7 @@ def next_unread_topic_from_message_id( ) unread_topics = sorted(self.unread_counts["unread_topics"].keys()) next_topic = False + stream_start: Optional[Tuple[int, str]] = None if current_topic is None: next_topic = True elif current_topic not in unread_topics: @@ -925,14 +926,26 @@ def next_unread_topic_from_message_id( # the last valid unread_topic in unread_topics list. for unread_topic in unread_topics * 2: stream_id, topic_name = unread_topic - if ( - not self.is_muted_topic(stream_id, topic_name) - and not self.is_muted_stream(stream_id) - and next_topic - ): - if unread_topic == current_topic: - return None - return unread_topic + if not self.is_muted_topic( + stream_id, topic_name + ) and not self.is_muted_stream(stream_id): + if next_topic: + if unread_topic == current_topic: + return None + if ( + current_topic is not None + and unread_topic[0] != current_topic[0] + and stream_start != current_topic + ): + return stream_start + return unread_topic + + if ( + stream_start is None + and current_topic is not None + and unread_topic[0] == current_topic[0] + ): + stream_start = unread_topic if unread_topic == current_topic: next_topic = True return None From b11ea7b3d0efb2c11537436083bb353688aa156b Mon Sep 17 00:00:00 2001 From: akshayaj Date: Mon, 17 Apr 2023 17:46:44 -0400 Subject: [PATCH 131/276] keys/views: Add `=` key to toggle agreement with first message reaction. Hotkeys document updated. --- docs/hotkeys.md | 1 + zulipterminal/config/keys.py | 5 +++++ zulipterminal/ui_tools/views.py | 9 +++++++++ 3 files changed, 15 insertions(+) diff --git a/docs/hotkeys.md b/docs/hotkeys.md index c9677e8258..38ad92b34d 100644 --- a/docs/hotkeys.md +++ b/docs/hotkeys.md @@ -55,6 +55,7 @@ |Narrow to the stream of the current message|s| |Narrow to the topic of the current message|S| |Narrow to a topic/direct-chat, or stream/all-direct-messages|z| +|Toggle first emoji reaction on selected message|=| |Add/remove thumbs-up reaction to the current message|+| |Add/remove star status of the current message|ctrl + s / *| |Show/hide message information|i| diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py index 27c9f35428..f6f6c09c0c 100644 --- a/zulipterminal/config/keys.py +++ b/zulipterminal/config/keys.py @@ -178,6 +178,11 @@ class KeyBinding(TypedDict): 'Narrow to a topic/direct-chat, or stream/all-direct-messages', 'key_category': 'msg_actions', }, + 'REACTION_AGREEMENT': { + 'keys': ['='], + 'help_text': 'Toggle first emoji reaction on selected message', + 'key_category': 'msg_actions', + }, 'TOGGLE_TOPIC': { 'keys': ['t'], 'help_text': 'Toggle topics in a stream', diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 6a1e4e6c0e..16221144fe 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -236,6 +236,15 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: message = self.focus.original_widget.message self.model.toggle_message_star_status(message) + elif is_command_key("REACTION_AGREEMENT", key) and self.focus is not None: + message = self.focus.original_widget.message + message_reactions = message["reactions"] + if len(message_reactions) > 0: + for reaction in message_reactions: + emoji = reaction["emoji_name"] + self.model.toggle_message_reaction(message, emoji) + break + key = super().keypress(size, key) return key From 2977bedb6502e5ac0d15f775c78fc490d711e7a1 Mon Sep 17 00:00:00 2001 From: mounilKshah Date: Sat, 26 Aug 2023 20:53:25 +0530 Subject: [PATCH 132/276] refactor: tests: buttons: Inline IDs and extract SERVER_URL. This commit shifts test IDs for test__parse_narrow_link() to be inline with the test cases, and extracts SERVER_URL into the test function body. --- tests/ui_tools/test_buttons.py | 68 +++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/tests/ui_tools/test_buttons.py b/tests/ui_tools/test_buttons.py index b29e88af89..80f274991b 100644 --- a/tests/ui_tools/test_buttons.py +++ b/tests/ui_tools/test_buttons.py @@ -653,70 +653,80 @@ def test__decode_message_id( @pytest.mark.parametrize( "link, expected_parsed_link", [ - ( - SERVER_URL + "/#narrow/stream/1-Stream-1", + case( + "/#narrow/stream/1-Stream-1", ParsedNarrowLink( narrow="stream", stream=DecodedStream(stream_id=1, stream_name=None) ), + id="modern_stream_narrow_link", ), - ( - SERVER_URL + "/#narrow/stream/Stream.201", + case( + "/#narrow/stream/Stream.201", ParsedNarrowLink( narrow="stream", stream=DecodedStream(stream_id=None, stream_name="Stream 1"), ), + id="deprecated_stream_narrow_link", ), - ( - SERVER_URL + "/#narrow/stream/1-Stream-1/topic/foo.20bar", + case( + "/#narrow/stream/1-Stream-1/topic/foo.20bar", ParsedNarrowLink( narrow="stream:topic", topic_name="foo bar", stream=DecodedStream(stream_id=1, stream_name=None), ), + id="topic_narrow_link", ), - ( - SERVER_URL + "/#narrow/stream/1-Stream-1/near/1", + case( + "/#narrow/stream/1-Stream-1/near/1", ParsedNarrowLink( narrow="stream:near", message_id=1, stream=DecodedStream(stream_id=1, stream_name=None), ), + id="stream_near_narrow_link", ), - ( - SERVER_URL + "/#narrow/stream/1-Stream-1/topic/foo/near/1", + case( + "/#narrow/stream/1-Stream-1/topic/foo/near/1", ParsedNarrowLink( narrow="stream:topic:near", topic_name="foo", message_id=1, stream=DecodedStream(stream_id=1, stream_name=None), ), + id="topic_near_narrow_link", ), - (SERVER_URL + "/#narrow/foo", ParsedNarrowLink()), - (SERVER_URL + "/#narrow/stream/", ParsedNarrowLink()), - (SERVER_URL + "/#narrow/stream/1-Stream-1/topic/", ParsedNarrowLink()), - (SERVER_URL + "/#narrow/stream/1-Stream-1//near/", ParsedNarrowLink()), - ( - SERVER_URL + "/#narrow/stream/1-Stream-1/topic/foo/near/", + case( + "/#narrow/foo", ParsedNarrowLink(), + id="invalid_narrow_link_1", + ), + case( + "/#narrow/stream/", + ParsedNarrowLink(), + id="invalid_narrow_link_2", + ), + case( + "/#narrow/stream/1-Stream-1/topic/", + ParsedNarrowLink(), + id="invalid_narrow_link_3", + ), + case( + "/#narrow/stream/1-Stream-1//near/", + ParsedNarrowLink(), + id="invalid_narrow_link_4", + ), + case( + "/#narrow/stream/1-Stream-1/topic/foo/near/", + ParsedNarrowLink(), + id="invalid_narrow_link_5", ), - ], - ids=[ - "modern_stream_narrow_link", - "deprecated_stream_narrow_link", - "topic_narrow_link", - "stream_near_narrow_link", - "topic_near_narrow_link", - "invalid_narrow_link_1", - "invalid_narrow_link_2", - "invalid_narrow_link_3", - "invalid_narrow_link_4", - "invalid_narrow_link_5", ], ) def test__parse_narrow_link( self, link: str, expected_parsed_link: ParsedNarrowLink ) -> None: - return_value = MessageLinkButton._parse_narrow_link(link) + return_value = MessageLinkButton._parse_narrow_link(SERVER_URL + link) assert return_value == expected_parsed_link From 2e9ef5bd72da7a6764d8f195b11da4d14a80830f Mon Sep 17 00:00:00 2001 From: mounilKshah Date: Sat, 26 Aug 2023 21:04:05 +0530 Subject: [PATCH 133/276] buttons: Add support for old format for narrow links. This commit provides support for narrow links in message content containing 'subject' instead of 'topic', which may be present in messages before server version 2.1.0. Test cases added. Fixes #1422. --- tests/ui_tools/test_buttons.py | 33 +++++++++++++++++++++++++++++-- zulipterminal/ui_tools/buttons.py | 13 +++++++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/tests/ui_tools/test_buttons.py b/tests/ui_tools/test_buttons.py index 80f274991b..3102d70d4c 100644 --- a/tests/ui_tools/test_buttons.py +++ b/tests/ui_tools/test_buttons.py @@ -677,6 +677,15 @@ def test__decode_message_id( ), id="topic_narrow_link", ), + case( + "/#narrow/stream/1-Stream-1/subject/foo.20bar", + ParsedNarrowLink( + narrow="stream:topic", + topic_name="foo bar", + stream=DecodedStream(stream_id=1, stream_name=None), + ), + id="subject_narrow_link", + ), case( "/#narrow/stream/1-Stream-1/near/1", ParsedNarrowLink( @@ -696,6 +705,16 @@ def test__decode_message_id( ), id="topic_near_narrow_link", ), + case( + "/#narrow/stream/1-Stream-1/subject/foo/near/1", + ParsedNarrowLink( + narrow="stream:topic:near", + topic_name="foo", + message_id=1, + stream=DecodedStream(stream_id=1, stream_name=None), + ), + id="subject_near_narrow_link", + ), case( "/#narrow/foo", ParsedNarrowLink(), @@ -712,15 +731,25 @@ def test__decode_message_id( id="invalid_narrow_link_3", ), case( - "/#narrow/stream/1-Stream-1//near/", + "/#narrow/stream/1-Stream-1/subject/", ParsedNarrowLink(), id="invalid_narrow_link_4", ), case( - "/#narrow/stream/1-Stream-1/topic/foo/near/", + "/#narrow/stream/1-Stream-1//near/", ParsedNarrowLink(), id="invalid_narrow_link_5", ), + case( + "/#narrow/stream/1-Stream-1/topic/foo/near/", + ParsedNarrowLink(), + id="invalid_narrow_link_6", + ), + case( + "/#narrow/stream/1-Stream-1/subject/foo/near/", + ParsedNarrowLink(), + id="invalid_narrow_link_7", + ), ], ) def test__parse_narrow_link( diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index fe2116981c..3fd8b9772c 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -493,12 +493,17 @@ def _parse_narrow_link(cls, link: str) -> ParsedNarrowLink: """ # NOTE: The optional stream_id link version is deprecated. The extended # support is for old messages. + # NOTE: Support for narrow links with subject instead of topic is also added # We expect the fragment to be one of the following types: # a. narrow/stream/[{stream_id}-]{stream-name} # b. narrow/stream/[{stream_id}-]{stream-name}/near/{message_id} # c. narrow/stream/[{stream_id}-]{stream-name}/topic/ # {encoded.20topic.20name} - # d. narrow/stream/[{stream_id}-]{stream-name}/topic/ + # d. narrow/stream/[{stream_id}-]{stream-name}/subject/ + # {encoded.20topic.20name} + # e. narrow/stream/[{stream_id}-]{stream-name}/topic/ + # {encoded.20topic.20name}/near/{message_id} + # f. narrow/stream/[{stream_id}-]{stream-name}/subject/ # {encoded.20topic.20name}/near/{message_id} fragments = urlparse(link.rstrip("/")).fragment.split("/") len_fragments = len(fragments) @@ -509,7 +514,9 @@ def _parse_narrow_link(cls, link: str) -> ParsedNarrowLink: parsed_link = dict(narrow="stream", stream=stream_data) elif ( - len_fragments == 5 and fragments[1] == "stream" and fragments[3] == "topic" + len_fragments == 5 + and fragments[1] == "stream" + and (fragments[3] == "topic" or fragments[3] == "subject") ): stream_data = cls._decode_stream_data(fragments[2]) topic_name = hash_util_decode(fragments[4]) @@ -527,7 +534,7 @@ def _parse_narrow_link(cls, link: str) -> ParsedNarrowLink: elif ( len_fragments == 7 and fragments[1] == "stream" - and fragments[3] == "topic" + and (fragments[3] == "topic" or fragments[3] == "subject") and fragments[5] == "near" ): stream_data = cls._decode_stream_data(fragments[2]) From 691a248305361afffe60473c4a855ee15b70d3fd Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sat, 2 Sep 2023 23:06:43 -0700 Subject: [PATCH 134/276] refactor: tests: buttons: Separate stream & message id in parsing links. Minor change to reduce confusion between values of different ids in test cases and ensure they are kept distinct for testing purposes. --- tests/ui_tools/test_buttons.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/ui_tools/test_buttons.py b/tests/ui_tools/test_buttons.py index 3102d70d4c..3705bbaefe 100644 --- a/tests/ui_tools/test_buttons.py +++ b/tests/ui_tools/test_buttons.py @@ -687,30 +687,30 @@ def test__decode_message_id( id="subject_narrow_link", ), case( - "/#narrow/stream/1-Stream-1/near/1", + "/#narrow/stream/1-Stream-1/near/987", ParsedNarrowLink( narrow="stream:near", - message_id=1, + message_id=987, stream=DecodedStream(stream_id=1, stream_name=None), ), id="stream_near_narrow_link", ), case( - "/#narrow/stream/1-Stream-1/topic/foo/near/1", + "/#narrow/stream/1-Stream-1/topic/foo/near/789", ParsedNarrowLink( narrow="stream:topic:near", topic_name="foo", - message_id=1, + message_id=789, stream=DecodedStream(stream_id=1, stream_name=None), ), id="topic_near_narrow_link", ), case( - "/#narrow/stream/1-Stream-1/subject/foo/near/1", + "/#narrow/stream/1-Stream-1/subject/foo/near/654", ParsedNarrowLink( narrow="stream:topic:near", topic_name="foo", - message_id=1, + message_id=654, stream=DecodedStream(stream_id=1, stream_name=None), ), id="subject_near_narrow_link", From bd1b918cb58166ba5780b37dc210d51cbfeab860 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 8 Sep 2023 17:55:26 -0700 Subject: [PATCH 135/276] requirements[dev]: Upgrade typos from ~=1.14.9 to 1.16.11. This picks out O_WRONLY as a misspelling, but this version of typos allows configuration in pyproject.toml, so exclude it there. --- pyproject.toml | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7c1ec28f8..e225cdee70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,10 @@ precision = 1 skip_covered = true show_missing = true +[tool.typos.default.extend-identifiers] +# See crate-ci/typos#744 and crate-ci/typos#773 +"O_WRONLY" = "O_WRONLY" + [tool.isort] py_version = 37 # Use black for the default settings (with adjustments listed below) diff --git a/setup.py b/setup.py index 13c304fc00..14c6ef1500 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ def long_description(): "black~=23.0", "ruff==0.0.267", "codespell[toml]~=2.2.2", - "typos~=1.14.9", + "typos~=1.16.11", ] typing_deps = [ From d0af5bd652ed49594e92afc7643dab12ef915d05 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 8 Sep 2023 18:01:36 -0700 Subject: [PATCH 136/276] requirements[dev]: Upgrade codespell from ~=2.2.2 to ~=2.2.5. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 14c6ef1500..b8d27a53e3 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def long_description(): "isort~=5.11.0", "black~=23.0", "ruff==0.0.267", - "codespell[toml]~=2.2.2", + "codespell[toml]~=2.2.5", "typos~=1.16.11", ] From a37d0c47bc5a7e16d26aaa0c78f839a82e7f98a1 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 8 Sep 2023 18:22:51 -0700 Subject: [PATCH 137/276] pyproject/run-spellcheck: Recognize iterm & check docs/FAQ.md. --- pyproject.toml | 4 ++++ tools/run-spellcheck | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e225cdee70..a2920b838d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,10 @@ show_missing = true # See crate-ci/typos#744 and crate-ci/typos#773 "O_WRONLY" = "O_WRONLY" +[tool.typos.default.extend-words] +# Allow iterm terminal emulator (ideally only per-file) +"iterm" = "iterm" + [tool.isort] py_version = 37 # Use black for the default settings (with adjustments listed below) diff --git a/tools/run-spellcheck b/tools/run-spellcheck index fac125626d..2b42de31d9 100755 --- a/tools/run-spellcheck +++ b/tools/run-spellcheck @@ -32,7 +32,6 @@ EXCLUDE_FILES = [ "tests/ui_tools/test_boxes.py", # Word prefixes as part of autocomplete test "tests/config/test_themes.py", # Wrong words as part of theme syntax test "tests/model/test_model.py", # 2nd not recognized crate-ci/typos#466 - "docs/FAQ.md", # iterm2 is proper noun [term, item, interm] "tools/run-spellcheck", # Exclude ourself due to notes ] From 0485bafe7bf00e7e8ce80e18429eead624640b04 Mon Sep 17 00:00:00 2001 From: Sashank Ravipati Date: Sat, 16 Sep 2023 20:48:44 +0530 Subject: [PATCH 138/276] core: Prompt user before exiting using PopUpConfirmationView. --- zulipterminal/core.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/zulipterminal/core.py b/zulipterminal/core.py index 6ac1ee4740..f93d89e53d 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -619,10 +619,18 @@ def narrow_to_all_mentions(self) -> None: def deregister_client(self) -> None: queue_id = self.model.queue_id self.client.deregister(queue_id, 1.0) + sys.exit(0) def exit_handler(self, signum: int, frame: Any) -> None: - self.deregister_client() - sys.exit(0) + question = urwid.Text( + ("bold", " Please confirm that you wish to exit Zulip-Terminal "), + "center", + ) + popup_view = PopUpConfirmationView( + self, question, self.deregister_client, location="center" + ) + self.loop.widget = popup_view + self.loop.run() def _raise_exception(self, *args: Any, **kwargs: Any) -> Literal[True]: if self._exception_info is not None: From da56b0e2cfa50244fc26a57bb3de1f4fd1070fd3 Mon Sep 17 00:00:00 2001 From: Subhasish-Behera Date: Sun, 2 Apr 2023 02:23:02 +0530 Subject: [PATCH 139/276] ui/buttons: On exiting topics view, associate topic name with stream_id. This is achieved through a new method associate_stream_with_topic in the View, which saves to a new internal dict. This will support later restoring the topic position in the UI. Since the topic list may be reordered between saving and restoring the state, the current index in the topic list is insufficient; the name of the topic is used intead. --- zulipterminal/ui.py | 6 +++++- zulipterminal/ui_tools/buttons.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/zulipterminal/ui.py b/zulipterminal/ui.py index a8ffdf3165..2d84d81a87 100644 --- a/zulipterminal/ui.py +++ b/zulipterminal/ui.py @@ -5,7 +5,7 @@ import random import re import time -from typing import Any, List, Optional +from typing import Any, Dict, List, Optional import urwid @@ -44,12 +44,16 @@ def __init__(self, controller: Any) -> None: self.unpinned_streams = self.model.unpinned_streams self.write_box = WriteBox(self) self.search_box = MessageSearchBox(self.controller) + self.stream_topic_map: Dict[int, str] = {} self.message_view: Any = None self.displaying_selection_hint = False super().__init__(self.main_window()) + def associate_stream_with_topic(self, stream_id: int, topic_name: str) -> None: + self.stream_topic_map[stream_id] = topic_name + def left_column_view(self) -> Any: tab = TabView( f"{AUTOHIDE_TAB_LEFT_ARROW} STREAMS & TOPICS {AUTOHIDE_TAB_LEFT_ARROW}" diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index 3fd8b9772c..3e3ea245c0 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -348,6 +348,7 @@ def mark_muted(self) -> None: def keypress(self, size: urwid_Size, key: str) -> Optional[str]: if is_command_key("TOGGLE_TOPIC", key): # Exit topic view + self.view.associate_stream_with_topic(self.stream_id, self.topic_name) self.view.left_panel.show_stream_view() return super().keypress(size, key) From 0e513fc615439dc9b851bb84d7240a9424137df3 Mon Sep 17 00:00:00 2001 From: Subhasish-Behera Date: Tue, 8 Aug 2023 07:29:58 +0530 Subject: [PATCH 140/276] ui/views: On initializing TopicsView, set topic focus from saved state. Information regarding the previous position in the topic list for the matching stream is retrieved from the View and used to set the initial focus. A new accessor in the View, saved_topic_in_stream_id, returns the most recent (saved) topic name. In the absence of a previous value this returns None. A new internal TopicsView helper method returns the focus (index) which corresponds to the saved topic name for the related stream. If there is no saved state or no matching topic name, it returns the top index (0), which was the previous behavior. This is isolated as a method to facilitate testing. Combined, this restores any previous topic-name state by assigning to the focus position. Test added for the internal helper method _focus_position_for_topic_name. --- tests/ui/test_ui_tools.py | 29 +++++++++++++++++++++++++++++ zulipterminal/ui.py | 3 +++ zulipterminal/ui_tools/views.py | 11 +++++++++++ 3 files changed, 43 insertions(+) diff --git a/tests/ui/test_ui_tools.py b/tests/ui/test_ui_tools.py index 324ee048b0..18d2ef27b3 100644 --- a/tests/ui/test_ui_tools.py +++ b/tests/ui/test_ui_tools.py @@ -1,6 +1,8 @@ from collections import OrderedDict import pytest +import urwid +from pytest import param as case from urwid import Divider from zulipterminal.config.keys import keys_for_command, primary_key_for_command @@ -614,6 +616,33 @@ def test_init(self, mocker, topic_view): ] ) + @pytest.mark.parametrize( + "stream_id, saved_topic_state, expected_focus_index", + [ + case(1, None, 0, id="initial_condition_no_topic_is_stored"), + case(2, "Topic 3", 2, id="topic_is_stored_and_present_in_topic_list"), + case(3, "Topic 4", 0, id="topic_is_stored_but_not_present_in_topic_list"), + ], + ) + def test__focus_position_for_topic_name( + self, mocker, stream_id, saved_topic_state, topic_view, expected_focus_index + ): + topic_view.stream_button.stream_id = stream_id + topic_view.list_box = mocker.MagicMock(spec=urwid.ListBox) + topic_view.list_box.body = [ + mocker.Mock(topic_name="Topic 1"), + mocker.Mock(topic_name="Topic 2"), + mocker.Mock(topic_name="Topic 3"), + ] + topic_view.log = urwid.SimpleFocusListWalker(topic_view.list_box.body) + mocker.patch.object( + topic_view.view, "saved_topic_in_stream_id", return_value=saved_topic_state + ) + + new_focus_index = topic_view._focus_position_for_topic_name() + + assert new_focus_index == expected_focus_index + @pytest.mark.parametrize( "new_text, expected_log", [ diff --git a/zulipterminal/ui.py b/zulipterminal/ui.py index 2d84d81a87..3507f72b6c 100644 --- a/zulipterminal/ui.py +++ b/zulipterminal/ui.py @@ -54,6 +54,9 @@ def __init__(self, controller: Any) -> None: def associate_stream_with_topic(self, stream_id: int, topic_name: str) -> None: self.stream_topic_map[stream_id] = topic_name + def saved_topic_in_stream_id(self, stream_id: int) -> Optional[str]: + return self.stream_topic_map.get(stream_id, None) + def left_column_view(self) -> Any: tab = TabView( f"{AUTOHIDE_TAB_LEFT_ARROW} STREAMS & TOPICS {AUTOHIDE_TAB_LEFT_ARROW}" diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 16221144fe..ef59d19c32 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -428,6 +428,7 @@ def __init__( urwid.Divider(SECTION_DIVIDER_LINE), ] ) + self.list_box.focus_position = self._focus_position_for_topic_name() super().__init__( self.list_box, header=self.header_list, @@ -435,6 +436,16 @@ def __init__( self.search_lock = threading.Lock() self.empty_search = False + def _focus_position_for_topic_name(self) -> int: + saved_topic_state = self.view.saved_topic_in_stream_id( + self.stream_button.stream_id + ) + if saved_topic_state is not None: + for index, topic in enumerate(self.log): + if topic.topic_name == saved_topic_state: + return index + return 0 + @asynch def update_topics(self, search_box: Any, new_text: str) -> None: if not self.view.controller.is_in_editor_mode(): From 8db080dc7f3c90556c8fab4834203dba60599885 Mon Sep 17 00:00:00 2001 From: vishwesh Date: Sat, 16 Sep 2023 11:25:08 -0700 Subject: [PATCH 141/276] refactor: model/helper: Extract unread_topics sorting into helper.py. This commit introduces the sort_unread_topics function to the next_unread_topic_from_message_id method to sort unread_topics instead of sorting it in the model. This replaces the use of bisect since the sorting behavior changes due to the change in the sort_unread_topics function. The caveat for this replacement is that the sort is performed again. This commit serves as a preparatory commit for the change in sorting behavior in the next commit. Test added. --- tests/helper/test_helper.py | 34 ++++++++++++++++++++++++++++++++++ zulipterminal/helper.py | 6 ++++++ zulipterminal/model.py | 8 +++++--- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/tests/helper/test_helper.py b/tests/helper/test_helper.py index 3c23343d6d..b38cc859a1 100644 --- a/tests/helper/test_helper.py +++ b/tests/helper/test_helper.py @@ -19,6 +19,7 @@ open_media, powerset, process_media, + sort_unread_topics, ) @@ -244,6 +245,39 @@ def test_powerset( assert powerset(iterable, map_func) == expected_powerset +@pytest.mark.parametrize( + "unread_topics, expected_value", + [ + case({}, [], id="no_unread_topics"), + case( + {(99, "topic1"): 1}, + [(99, "topic1")], + id="single_unread_topic", + ), + case( + {(999, "topic3"): 1, (1000, "topic2"): 1, (1, "topic1"): 1}, + [(1, "topic1"), (999, "topic3"), (1000, "topic2")], + id="multiple_unread_topics", + ), + case( + { + (999, "topic3"): 1, + (1000, "topic2"): 1, + (1000, "topic4"): 3, + (1, "topic1"): 1, + }, + [(1, "topic1"), (999, "topic3"), (1000, "topic2"), (1000, "topic4")], + id="multiple_unread_topics_in_same_stream", + ), + ], +) +def test_sort_unread_topics( + unread_topics: Dict[Tuple[int, str], int], + expected_value: List[Tuple[int, str]], +) -> None: + assert sort_unread_topics(unread_topics) == expected_value + + @pytest.mark.parametrize( "muted_streams, muted_topics, vary_in_unreads", [ diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index ce77c52a8c..5e30c1c55e 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -165,6 +165,12 @@ def wrapper(*args: ParamT.args, **kwargs: ParamT.kwargs) -> None: return wrapper +def sort_unread_topics( + unread_topics: Dict[Tuple[int, str], int] +) -> List[Tuple[int, str]]: + return sorted(unread_topics.keys()) + + def _set_count_in_model( new_count: int, changed_messages: List[Message], unread_counts: UnreadCounts ) -> None: diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 7073e4f055..8270598c2c 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -2,7 +2,6 @@ Defines the `Model`, fetching and storing data retrieved from the Zulip server """ -import bisect import itertools import json import time @@ -73,6 +72,7 @@ initial_index, notify_if_message_sent_outside_narrow, set_count, + sort_unread_topics, ) from zulipterminal.platform_code import notify from zulipterminal.ui_tools.utils import create_msg_box_list @@ -911,7 +911,7 @@ def next_unread_topic_from_message_id( self.stream_id_from_name(self.narrow[0][1]), self.narrow[1][1], ) - unread_topics = sorted(self.unread_counts["unread_topics"].keys()) + unread_topics = sort_unread_topics(self.unread_counts["unread_topics"]) next_topic = False stream_start: Optional[Tuple[int, str]] = None if current_topic is None: @@ -921,7 +921,9 @@ def next_unread_topic_from_message_id( # current_topic is not in unread_topics, and the next unmuted topic # is to be returned. This does not modify the original unread topics # data, and is just used to compute the next unmuted topic to be returned. - bisect.insort(unread_topics, current_topic) + unread_topics = sort_unread_topics( + {**self.unread_counts["unread_topics"], current_topic: 0} + ) # loop over unread_topics list twice for the case that last_unread_topic was # the last valid unread_topic in unread_topics list. for unread_topic in unread_topics * 2: From 7654df77eec6eae87d8192cdad3b02b6dd91ddf7 Mon Sep 17 00:00:00 2001 From: vishwesh Date: Thu, 7 Sep 2023 17:50:59 -0700 Subject: [PATCH 142/276] model/helper: Use stream panel order in sort_unread_topics. This commit changes the sort_unread_topics method in helper.py to use the left stream panel ordering to sort the unread data instead of implicitly using the stream id as key. Tests updated & extended. --- tests/helper/test_helper.py | 8 +++++--- tests/model/test_model.py | 29 +++++++++++++++++++++++++++++ zulipterminal/helper.py | 12 ++++++++++-- zulipterminal/model.py | 11 +++++++++-- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/tests/helper/test_helper.py b/tests/helper/test_helper.py index b38cc859a1..8a79359ea4 100644 --- a/tests/helper/test_helper.py +++ b/tests/helper/test_helper.py @@ -256,7 +256,7 @@ def test_powerset( ), case( {(999, "topic3"): 1, (1000, "topic2"): 1, (1, "topic1"): 1}, - [(1, "topic1"), (999, "topic3"), (1000, "topic2")], + [(1000, "topic2"), (1, "topic1"), (999, "topic3")], id="multiple_unread_topics", ), case( @@ -266,7 +266,7 @@ def test_powerset( (1000, "topic4"): 3, (1, "topic1"): 1, }, - [(1, "topic1"), (999, "topic3"), (1000, "topic2"), (1000, "topic4")], + [(1000, "topic2"), (1000, "topic4"), (1, "topic1"), (999, "topic3")], id="multiple_unread_topics_in_same_stream", ), ], @@ -274,8 +274,10 @@ def test_powerset( def test_sort_unread_topics( unread_topics: Dict[Tuple[int, str], int], expected_value: List[Tuple[int, str]], + streams: List[Dict[str, Any]], ) -> None: - assert sort_unread_topics(unread_topics) == expected_value + stream_list = [stream["id"] for stream in streams] + assert sort_unread_topics(unread_topics, stream_list) == expected_value @pytest.mark.parametrize( diff --git a/tests/model/test_model.py b/tests/model/test_model.py index fe2ba5535e..915621f8fc 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -4016,6 +4016,12 @@ def test_is_muted_topic( (1, "topic1"), id="unread_present_in_same_stream_wrap_around", ), + case( + {(1, "topic1"), (5, "topic5"), (2, "topic2")}, + (2, "topic2"), + (5, "topic5"), + id="streams_sorted_according_to_left_panel", + ), ], ) def test_next_unread_topic_from_message( @@ -4035,6 +4041,20 @@ def test_next_unread_topic_from_message( model.stream_dict[4] = {"name": "Stream 4"} model.muted_streams = {3} + # Extra stream for stream sort testing (should not exist otherwise) + assert 5 not in model.stream_dict + model.stream_dict[5] = {"name": "First Stream"} + + model.pinned_streams = [ + {"name": "First Stream", "id": 5}, + {"name": "Stream 1", "id": 1}, + {"name": "Stream 2", "id": 2}, + ] + model.unpinned_streams = [ + {"name": "Stream 3", "id": 3}, + {"name": "Stream 4", "id": 4}, + ] + # date data unimportant (if present) model._muted_topics = { stream_topic: None @@ -4073,6 +4093,15 @@ def test_next_unread_topic_from_message__empty_narrow( model.unread_counts = { "unread_topics": {stream_topic: 1 for stream_topic in unread_topics} } + model.pinned_streams = [ + {"name": "Stream 1", "id": 1}, + {"name": "Stream 2", "id": 2}, + ] + model.unpinned_streams = [ + {"name": "Stream 3", "id": 3}, + {"name": "Stream 4", "id": 4}, + ] + model.stream_id_from_name = mocker.Mock(return_value=narrow_stream_id) model.narrow = empty_narrow diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index 5e30c1c55e..52e407628a 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -166,9 +166,17 @@ def wrapper(*args: ParamT.args, **kwargs: ParamT.kwargs) -> None: def sort_unread_topics( - unread_topics: Dict[Tuple[int, str], int] + unread_topics: Dict[Tuple[int, str], int], stream_list: List[int] ) -> List[Tuple[int, str]]: - return sorted(unread_topics.keys()) + return sorted( + unread_topics.keys(), + key=lambda stream_topic: ( + stream_list.index(stream_topic[0]) + if stream_topic[0] in stream_list + else len(stream_list), + stream_topic[1], + ), + ) def _set_count_in_model( diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 8270598c2c..9c6fce61b5 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -911,7 +911,13 @@ def next_unread_topic_from_message_id( self.stream_id_from_name(self.narrow[0][1]), self.narrow[1][1], ) - unread_topics = sort_unread_topics(self.unread_counts["unread_topics"]) + left_panel_stream_list = [ + stream["id"] for stream in (self.pinned_streams + self.unpinned_streams) + ] + unread_topics = sort_unread_topics( + self.unread_counts["unread_topics"], + left_panel_stream_list, + ) next_topic = False stream_start: Optional[Tuple[int, str]] = None if current_topic is None: @@ -922,7 +928,8 @@ def next_unread_topic_from_message_id( # is to be returned. This does not modify the original unread topics # data, and is just used to compute the next unmuted topic to be returned. unread_topics = sort_unread_topics( - {**self.unread_counts["unread_topics"], current_topic: 0} + {**self.unread_counts["unread_topics"], current_topic: 0}, + left_panel_stream_list, ) # loop over unread_topics list twice for the case that last_unread_topic was # the last valid unread_topic in unread_topics list. From 99f3bdd829c495934d1638fdb630c50b5e1d588a Mon Sep 17 00:00:00 2001 From: supascooopa Date: Thu, 17 Aug 2023 14:52:54 +0300 Subject: [PATCH 143/276] symbols: Add symbols (markers) for use with common narrows. The 'Mentioned messages' and 'Starred messages' symbols are set to ASCII characters, similar to those in the Zulip web app. The selected 'All messages' symbol is the closest match found after some testing, and appears to be available fairly widely. An appropriate descriptive comment is added, as with other non-ASCII symbols, for later reference. --- zulipterminal/config/symbols.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/zulipterminal/config/symbols.py b/zulipterminal/config/symbols.py index 153e76c274..f52dd85f7b 100644 --- a/zulipterminal/config/symbols.py +++ b/zulipterminal/config/symbols.py @@ -8,10 +8,17 @@ # Suffix comments indicate: unicode name, codepoint (unicode block, version if not v1.1) INVALID_MARKER = "✗" # BALLOT X, U+2717 (Dingbats) + +ALL_MESSAGES_MARKER = "≡" # IDENTICAL TO, U+2261 (Mathematical operators) +MENTIONED_MESSAGES_MARKER = "@" +STARRED_MESSAGES_MARKER = "*" + DIRECT_MESSAGE_MARKER = "§" # SECTION SIGN, U+00A7 (Latin-1 supplement) + STREAM_MARKER_PRIVATE = "P" STREAM_MARKER_PUBLIC = "#" STREAM_MARKER_WEB_PUBLIC = "⊚" # CIRCLED RING OPERATOR, U+229A (Mathematical operators) + STREAM_TOPIC_SEPARATOR = "▶" # BLACK RIGHT-POINTING TRIANGLE, U+25B6 (Geometric shapes) # Range of block options for consideration: '█', '▓', '▒', '░' From 023e2ea7ccbc6092e9e8e547a0dd1fb97b30ec65 Mon Sep 17 00:00:00 2001 From: supascooopa Date: Wed, 6 Sep 2023 21:39:20 +0300 Subject: [PATCH 144/276] buttons/ui_sizes: Add prefix symbols to narrowing buttons in top-left. This commit adds prefix symbols to the main buttons in the top left corner of the UI. This aims to make the main buttons stand out more and approximate the designs used in the web app. This change also leads to a cleaner design, since these top buttons are now indented and aligned similarly to the panels beneath them, eg. the list of streams. The symbol used as a prefix in headers of direct messages is reused for the 'Direct messages' button, with other buttons using symbols added in the previous commit. The left part of the UI is increased in width to accommodate the new additions. Note that the "title" style is used to make the icons bolder, though this should be decoupled in future. --- zulipterminal/config/ui_sizes.py | 2 +- zulipterminal/ui_tools/buttons.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/zulipterminal/config/ui_sizes.py b/zulipterminal/config/ui_sizes.py index 3a2e021e74..cdc95a7433 100644 --- a/zulipterminal/config/ui_sizes.py +++ b/zulipterminal/config/ui_sizes.py @@ -3,7 +3,7 @@ """ TAB_WIDTH = 3 -LEFT_WIDTH = 29 +LEFT_WIDTH = 31 RIGHT_WIDTH = 23 # These affect popup width-scaling, dependent upon window width diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index 3e3ea245c0..748c86c2f0 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -13,7 +13,14 @@ from zulipterminal.api_types import RESOLVED_TOPIC_PREFIX, EditPropagateMode, Message from zulipterminal.config.keys import is_command_key, primary_key_for_command from zulipterminal.config.regexes import REGEX_INTERNAL_LINK_STREAM_ID -from zulipterminal.config.symbols import CHECK_MARK, MUTE_MARKER +from zulipterminal.config.symbols import ( + ALL_MESSAGES_MARKER, + CHECK_MARK, + DIRECT_MESSAGE_MARKER, + MENTIONED_MESSAGES_MARKER, + MUTE_MARKER, + STARRED_MESSAGES_MARKER, +) from zulipterminal.config.ui_mappings import EDIT_MODE_CAPTIONS, STREAM_ACCESS_TYPE from zulipterminal.helper import StreamData, hash_util_decode, process_media from zulipterminal.urwid_types import urwid_MarkupTuple, urwid_Size @@ -122,6 +129,7 @@ def __init__(self, *, controller: Any, count: int) -> None: super().__init__( controller=controller, + prefix_markup=("title", ALL_MESSAGES_MARKER), label_markup=(None, button_text), suffix_markup=("unread_count", ""), show_function=controller.narrow_to_all_messages, @@ -136,6 +144,7 @@ def __init__(self, *, controller: Any, count: int) -> None: super().__init__( controller=controller, label_markup=(None, button_text), + prefix_markup=("title", DIRECT_MESSAGE_MARKER), suffix_markup=("unread_count", ""), show_function=controller.narrow_to_all_pm, count=count, @@ -148,6 +157,7 @@ def __init__(self, *, controller: Any, count: int) -> None: super().__init__( controller=controller, + prefix_markup=("title", MENTIONED_MESSAGES_MARKER), label_markup=(None, button_text), suffix_markup=("unread_count", ""), show_function=controller.narrow_to_all_mentions, @@ -161,6 +171,7 @@ def __init__(self, *, controller: Any, count: int) -> None: super().__init__( controller=controller, + prefix_markup=("title", STARRED_MESSAGES_MARKER), label_markup=(None, button_text), suffix_markup=("starred_count", ""), show_function=controller.narrow_to_all_starred, From 2d25ba97b9049e0b8fa5950e7d5c4eae96e5a4f9 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 3 Oct 2023 22:19:47 -0700 Subject: [PATCH 145/276] requirements: Pin pygments at ~=2.15.1 instead of >=2.14.0. Pygments 2.16.0 introduced a style to support a combination of bold and italic styling in pygments/pygments#2444. Both of our gruvbox themes and the light native theme gain a 'bold strong' style via pygments as a result, which urwid fails to parse and blocks the application from loading. Longer-term we should improve the pygments to urwid translation logic to allow these styles to work and an upgrade to later pygments versions, but for now this allows these themes to continue working as before. Fixes #1431. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b8d27a53e3..7b416d844d 100644 --- a/setup.py +++ b/setup.py @@ -100,7 +100,7 @@ def long_description(): "urwid_readline>=0.13", "beautifulsoup4>=4.11.1", "lxml>=4.9.2", - "pygments>=2.14.0", + "pygments~=2.15.1", "typing_extensions~=4.5.0", "python-dateutil>=2.8.2", "pytz>=2022.7.1", From 89cadf3390a3cb2fdd76ce3ad72fc48496cab6a0 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 24 Oct 2023 21:10:32 -0700 Subject: [PATCH 146/276] refactor: tests: run: Use fixture for common platform output string. This extracts the mocking to occur automatically given a platform string, and provide the expected output text, which is tested in various tests. --- tests/cli/test_run.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index ea2b7752f1..a285f77702 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -153,6 +153,15 @@ def test_main_help(capsys: CaptureFixture[str], options: str) -> None: assert captured.err == "" +@pytest.fixture +def platform_mocker(mocker: MockerFixture) -> Callable[[str], List[str]]: + def factory(platform: str) -> List[str]: + mocker.patch(MODULE + ".detected_platform", return_value=platform) + return [f"Detected platform: {platform}"] + + return factory + + @pytest.fixture def minimal_zuliprc(tmp_path: Path) -> str: zuliprc_path = tmp_path / "zuliprc" @@ -165,6 +174,7 @@ def minimal_zuliprc(tmp_path: Path) -> str: def test_valid_zuliprc_but_no_connection( capsys: CaptureFixture[str], mocker: MockerFixture, + platform_mocker: Callable[[str], List[str]], minimal_zuliprc: str, server_connection_error: str = "some_error", platform: str = "some_platform", @@ -173,7 +183,7 @@ def test_valid_zuliprc_but_no_connection( CONTROLLER + ".__init__", side_effect=ServerConnectionFailure(server_connection_error), ) - mocker.patch(MODULE + ".detected_platform", return_value=platform) + expected_platform_output = platform_mocker(platform) with pytest.raises(SystemExit) as e: main(["-c", minimal_zuliprc]) @@ -183,8 +193,7 @@ def test_valid_zuliprc_but_no_connection( captured = capsys.readouterr() lines = captured.out.strip().split("\n") - expected_lines = [ - f"Detected platform: {platform}", + expected_lines = expected_platform_output + [ "Loading with:", " theme 'zt_dark' specified from default config.", " autohide setting 'no_autohide' specified from default config.", @@ -209,6 +218,7 @@ def test_valid_zuliprc_but_no_connection( def test_warning_regarding_incomplete_theme( capsys: CaptureFixture[str], mocker: MockerFixture, + platform_mocker: Callable[[str], List[str]], minimal_zuliprc: str, bad_theme: str, expected_complete_incomplete_themes: Tuple[List[str], List[str]], @@ -228,6 +238,8 @@ def test_warning_regarding_incomplete_theme( ) mocker.patch(MODULE + ".generate_theme") + expected_platform_output = platform_mocker(platform) + with pytest.raises(SystemExit) as e: main(["-c", minimal_zuliprc, "-t", bad_theme]) @@ -236,8 +248,7 @@ def test_warning_regarding_incomplete_theme( captured = capsys.readouterr() lines = captured.out.strip().split("\n") - expected_lines = [ - f"Detected platform: {platform}", + expected_lines = expected_platform_output + [ "Loading with:", f" theme '{bad_theme}' specified on command line.", "\x1b[93m WARNING: Incomplete theme; results may vary!", @@ -430,14 +441,13 @@ def func(config: Dict[str, str]) -> str: def test_successful_main_function_with_config( capsys: CaptureFixture[str], mocker: MockerFixture, + platform_mocker: Callable[[str], List[str]], parameterized_zuliprc: Callable[[Dict[str, str]], str], config_key: str, config_value: str, footlinks_output: str, platform: str = "some_platform", ) -> None: - mocker.patch(MODULE + ".detected_platform", return_value=platform) - config = { "theme": "default", "autohide": "autohide", @@ -446,16 +456,18 @@ def test_successful_main_function_with_config( } config[config_key] = config_value zuliprc = parameterized_zuliprc(config) + mocker.patch(CONTROLLER + ".__init__", return_value=None) mocker.patch(CONTROLLER + ".main", return_value=None) + expected_platform_output = platform_mocker(platform) + with pytest.raises(SystemExit): main(["-c", zuliprc]) captured = capsys.readouterr() lines = captured.out.strip().split("\n") - expected_lines = [ - f"Detected platform: {platform}", + expected_lines = expected_platform_output + [ "Loading with:", " theme 'zt_dark' specified in zuliprc file (by alias 'default').", " autohide setting 'autohide' specified in zuliprc file.", @@ -484,6 +496,7 @@ def test_successful_main_function_with_config( def test_main_error_with_invalid_zuliprc_options( capsys: CaptureFixture[str], mocker: MockerFixture, + platform_mocker: Callable[[str], List[str]], parameterized_zuliprc: Callable[[Dict[str, str]], str], zulip_config: Dict[str, str], error_message: str, @@ -494,6 +507,8 @@ def test_main_error_with_invalid_zuliprc_options( mocker.patch(MODULE + ".detected_platform", return_value=platform) mocker.patch(CONTROLLER + ".main", return_value=None) + expected_platform_output = platform_mocker(platform) + with pytest.raises(SystemExit) as e: main(["-c", zuliprc]) @@ -502,7 +517,7 @@ def test_main_error_with_invalid_zuliprc_options( captured = capsys.readouterr() lines = captured.out.strip() expected_lines = "\n".join( - [f"Detected platform: {platform}", f"\033[91m{error_message}\033[0m"] + expected_platform_output + [f"\033[91m{error_message}\033[0m"] ) assert lines == expected_lines From 17e0e6d9a40544af8ee8bf66493b14adcd6f473a Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 24 Oct 2023 21:09:46 -0700 Subject: [PATCH 147/276] platform_code: Add wrappers to detect python runtime. --- zulipterminal/platform_code.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/zulipterminal/platform_code.py b/zulipterminal/platform_code.py index 89e25bedc9..d44355e4d0 100644 --- a/zulipterminal/platform_code.py +++ b/zulipterminal/platform_code.py @@ -4,10 +4,26 @@ import platform import subprocess +from typing import Tuple from typing_extensions import Literal +# PYTHON DETECTION +def detected_python() -> Tuple[str, str, str]: + return ( + platform.python_version(), + platform.python_implementation(), + platform.python_branch(), + ) + + +def detected_python_in_full() -> str: + version, implementation, branch = detected_python() + branch_text = f"[{branch}]" if branch else "" + return f"{version} ({implementation}) {branch_text}" + + # PLATFORM DETECTION SupportedPlatforms = Literal["Linux", "MacOS", "WSL"] AllPlatforms = Literal[SupportedPlatforms, "unsupported"] From ebe8dd26c63297b274c42eb2024170a5f481c391 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 24 Oct 2023 21:08:57 -0700 Subject: [PATCH 148/276] run: Output python runtime on startup. Tests and fixture updated. --- tests/cli/test_run.py | 27 ++++++++++++++++----------- zulipterminal/cli/run.py | 8 ++++++-- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index a285f77702..f1fde1cc73 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -154,10 +154,11 @@ def test_main_help(capsys: CaptureFixture[str], options: str) -> None: @pytest.fixture -def platform_mocker(mocker: MockerFixture) -> Callable[[str], List[str]]: - def factory(platform: str) -> List[str]: +def platform_mocker(mocker: MockerFixture) -> Callable[[str, str], List[str]]: + def factory(platform: str, python: str) -> List[str]: mocker.patch(MODULE + ".detected_platform", return_value=platform) - return [f"Detected platform: {platform}"] + mocker.patch(MODULE + ".detected_python_in_full", return_value=python) + return ["Detected:", f" - platform: {platform}", f" - python: {python}"] return factory @@ -174,16 +175,17 @@ def minimal_zuliprc(tmp_path: Path) -> str: def test_valid_zuliprc_but_no_connection( capsys: CaptureFixture[str], mocker: MockerFixture, - platform_mocker: Callable[[str], List[str]], + platform_mocker: Callable[[str, str], List[str]], minimal_zuliprc: str, server_connection_error: str = "some_error", platform: str = "some_platform", + python: str = "3.99 (Zython) [cool]", ) -> None: mocker.patch( CONTROLLER + ".__init__", side_effect=ServerConnectionFailure(server_connection_error), ) - expected_platform_output = platform_mocker(platform) + expected_platform_output = platform_mocker(platform, python) with pytest.raises(SystemExit) as e: main(["-c", minimal_zuliprc]) @@ -218,13 +220,14 @@ def test_valid_zuliprc_but_no_connection( def test_warning_regarding_incomplete_theme( capsys: CaptureFixture[str], mocker: MockerFixture, - platform_mocker: Callable[[str], List[str]], + platform_mocker: Callable[[str, str], List[str]], minimal_zuliprc: str, bad_theme: str, expected_complete_incomplete_themes: Tuple[List[str], List[str]], expected_warning: str, server_connection_error: str = "sce", platform: str = "some_platform", + python: str = "3.99 (Zython) [cool]", ) -> None: mocker.patch( CONTROLLER + ".__init__", @@ -238,7 +241,7 @@ def test_warning_regarding_incomplete_theme( ) mocker.patch(MODULE + ".generate_theme") - expected_platform_output = platform_mocker(platform) + expected_platform_output = platform_mocker(platform, python) with pytest.raises(SystemExit) as e: main(["-c", minimal_zuliprc, "-t", bad_theme]) @@ -441,12 +444,13 @@ def func(config: Dict[str, str]) -> str: def test_successful_main_function_with_config( capsys: CaptureFixture[str], mocker: MockerFixture, - platform_mocker: Callable[[str], List[str]], + platform_mocker: Callable[[str, str], List[str]], parameterized_zuliprc: Callable[[Dict[str, str]], str], config_key: str, config_value: str, footlinks_output: str, platform: str = "some_platform", + python: str = "3.99 (Zython) [cool]", ) -> None: config = { "theme": "default", @@ -460,7 +464,7 @@ def test_successful_main_function_with_config( mocker.patch(CONTROLLER + ".__init__", return_value=None) mocker.patch(CONTROLLER + ".main", return_value=None) - expected_platform_output = platform_mocker(platform) + expected_platform_output = platform_mocker(platform, python) with pytest.raises(SystemExit): main(["-c", zuliprc]) @@ -496,18 +500,19 @@ def test_successful_main_function_with_config( def test_main_error_with_invalid_zuliprc_options( capsys: CaptureFixture[str], mocker: MockerFixture, - platform_mocker: Callable[[str], List[str]], + platform_mocker: Callable[[str, str], List[str]], parameterized_zuliprc: Callable[[Dict[str, str]], str], zulip_config: Dict[str, str], error_message: str, platform: str = "some_platform", + python: str = "3.99 (Zython) [cool]", ) -> None: zuliprc = parameterized_zuliprc(zulip_config) mocker.patch(CONTROLLER + ".__init__", return_value=None) mocker.patch(MODULE + ".detected_platform", return_value=platform) mocker.patch(CONTROLLER + ".main", return_value=None) - expected_platform_output = platform_mocker(platform) + expected_platform_output = platform_mocker(platform, python) with pytest.raises(SystemExit) as e: main(["-c", zuliprc]) diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index 937f2c754c..87794e96f6 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -26,7 +26,7 @@ ) from zulipterminal.core import Controller from zulipterminal.model import ServerConnectionFailure -from zulipterminal.platform_code import detected_platform +from zulipterminal.platform_code import detected_platform, detected_python_in_full from zulipterminal.version import ZT_VERSION @@ -430,7 +430,11 @@ def main(options: Optional[List[str]] = None) -> None: else: zuliprc_path = "~/zuliprc" - print(f"Detected platform: {detected_platform()}") + print( + "Detected:" + f"\n - platform: {detected_platform()}" + f"\n - python: {detected_python_in_full()}" + ) try: zterm = parse_zuliprc(zuliprc_path) From 047662a5548523a929270e0c440c229e65096d55 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 2 Nov 2023 21:27:49 -0700 Subject: [PATCH 149/276] lint-and-test: Revert previous change to use latest python on MacOS. Using '3.x' automatically selected a high single version of python on MacOS. However, it bumped the python version over the version that we support, breaking with no warning - and also using different python versions between PRs. This could be useful to have with a manual trigger, or if it did not break regular builds. This is an effective revert of d8bae8894544e4cdaafdd1b5aea6682e42b0f854. --- .github/workflows/lint-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 1660bcc7e2..f7fceb7895 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -206,7 +206,7 @@ jobs: - {PYTHON: 'pypy-3.7', OS: ubuntu-latest, NAME: "PyPy 3.7 (ubuntu)", EXPECT: "Linux"} - {PYTHON: 'pypy-3.8', OS: ubuntu-latest, NAME: "PyPy 3.8 (ubuntu)", EXPECT: "Linux"} - {PYTHON: 'pypy-3.9', OS: ubuntu-latest, NAME: "PyPy 3.9 (ubuntu)", EXPECT: "Linux"} - - {PYTHON: '3.x', OS: macos-latest, NAME: "CPython 3.x [latest] (macos)", EXPECT: "MacOS"} + - {PYTHON: '3.11', OS: macos-latest, NAME: "CPython 3.11 (macos)", EXPECT: "MacOS"} env: EXPECT: ${{ matrix.env.EXPECT }} runs-on: ${{ matrix.env.OS }} From 720fea625b4f6176b19cf326eb002cc5b2a1ec55 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 2 Nov 2023 15:41:24 -0700 Subject: [PATCH 150/276] lint-and-test/platform_code: Validate python environment detection. Note that a side-effect of the change to use a specific python version on MacOS in a recent commit is that this platform does not need to be excluded manually. --- .github/workflows/lint-and-test.yml | 3 +++ zulipterminal/platform_code.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index f7fceb7895..a2f9b449c2 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -209,6 +209,7 @@ jobs: - {PYTHON: '3.11', OS: macos-latest, NAME: "CPython 3.11 (macos)", EXPECT: "MacOS"} env: EXPECT: ${{ matrix.env.EXPECT }} + PYTHON: ${{ matrix.env.PYTHON }} runs-on: ${{ matrix.env.OS }} name: Install & test - ${{ matrix.env.NAME}} steps: @@ -230,6 +231,8 @@ jobs: run: sudo apt install libxml2-dev libxslt1-dev - name: Ensure regular package installs from checkout run: pip install . + - name: Check we detect the python environment correctly + run: python -c "from zulipterminal.platform_code import detected_python_short; import os; e, d = os.environ['PYTHON'], detected_python_short(); assert d == e, f'{d} != {e}'" - name: Check we detect the platform correctly run: python -c "from zulipterminal.platform_code import detected_platform; import os; e, d = os.environ['EXPECT'], detected_platform(); assert d == e, f'{d} != {e}'" - name: Install test dependencies diff --git a/zulipterminal/platform_code.py b/zulipterminal/platform_code.py index d44355e4d0..2741e8ec38 100644 --- a/zulipterminal/platform_code.py +++ b/zulipterminal/platform_code.py @@ -24,6 +24,17 @@ def detected_python_in_full() -> str: return f"{version} ({implementation}) {branch_text}" +def detected_python_short() -> str: + """Concise output for comparison in CI (CPython implied in version)""" + version, implementation, _ = detected_python() + short_version = version[: version.rfind(".")] + if implementation == "CPython": + return short_version + if implementation == "PyPy": + return f"pypy-{short_version}" + raise NotImplementedError + + # PLATFORM DETECTION SupportedPlatforms = Literal["Linux", "MacOS", "WSL"] AllPlatforms = Literal[SupportedPlatforms, "unsupported"] From 203d5cc769e494ebd7d7562d5b3dacc864b50661 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 2 Nov 2023 22:54:18 -0700 Subject: [PATCH 151/276] tests: popups: Add test for categories in About popup. --- tests/ui_tools/test_popups.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py index c378ba9eb0..2ea422b3d7 100644 --- a/tests/ui_tools/test_popups.py +++ b/tests/ui_tools/test_popups.py @@ -187,7 +187,6 @@ def mock_external_classes(self, mocker: MockerFixture) -> None: mocker.patch.object( self.controller, "maximum_popup_dimensions", return_value=(64, 64) ) - mocker.patch(LISTWALKER, return_value=[]) server_version, server_feature_level = MINIMUM_SUPPORTED_SERVER_VERSION self.about_view = AboutView( @@ -248,6 +247,20 @@ def test_feature_level_content( 1 if server_feature_level else 0 ) + def test_categories(self) -> None: + categories = [ + widget.text + for widget in self.about_view.log + if isinstance(widget, Text) + and len(widget.attrib) + and "popup_category" in widget.attrib[0][0] + ] + assert categories == [ + "Application", + "Server", + "Application Configuration", + ] + class TestUserInfoView: @pytest.fixture(autouse=True) From abdc4541ee39f44603b5ae40be8fc752591144b6 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 2 Nov 2023 17:04:59 -0700 Subject: [PATCH 152/276] views: Add Detected Environment section to About popup. This includes the established Platform (PLATFORM), and the recent addition of the python version and implementation. Test updated. --- tests/ui_tools/test_popups.py | 1 + zulipterminal/ui_tools/views.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py index 2ea422b3d7..e5253973eb 100644 --- a/tests/ui_tools/test_popups.py +++ b/tests/ui_tools/test_popups.py @@ -259,6 +259,7 @@ def test_categories(self) -> None: "Application", "Server", "Application Configuration", + "Detected environment", ] diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index ef59d19c32..463478b24e 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -41,6 +41,7 @@ match_stream, match_user, ) +from zulipterminal.platform_code import PLATFORM, detected_python_in_full from zulipterminal.server_url import near_message_url from zulipterminal.ui_tools.boxes import PanelSearchBox from zulipterminal.ui_tools.buttons import ( @@ -1104,6 +1105,10 @@ def __init__( ("Notifications", "enabled" if notify_enabled else "disabled"), ], ), + ( + "Detected environment", + [("Platform", PLATFORM), ("Python", detected_python_in_full())], + ), ] popup_width, column_widths = self.calculate_table_widths(contents, len(title)) From b5819c723e0e930072d5270211be92534ac71d3c Mon Sep 17 00:00:00 2001 From: Sashank Ravipati Date: Sun, 1 Oct 2023 17:11:45 +0530 Subject: [PATCH 153/276] run/core: Add control of exit confirmation via configuration file. New setting documented in README. Tests updated. --- README.md | 3 +++ tests/cli/test_run.py | 3 +++ tests/core/test_core.py | 2 ++ zulipterminal/cli/run.py | 6 ++++++ zulipterminal/core.py | 14 ++++++++++++-- 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c59e35103d..5763df618c 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,9 @@ theme=zt_dark ## Autohide: set to 'autohide' to hide the left & right panels except when they're focused autohide=no_autohide +## Exit Confirmation: set to 'disabled' to exit directly with no warning popup first +exit_confirmation=enabled + ## Footlinks: set to 'disabled' to hide footlinks; 'enabled' will show the first 3 per message ## For more flexibility, comment-out this value, and un-comment maximum-footlinks below footlinks=enabled diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index f1fde1cc73..7b4fe029b4 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -199,6 +199,7 @@ def test_valid_zuliprc_but_no_connection( "Loading with:", " theme 'zt_dark' specified from default config.", " autohide setting 'no_autohide' specified from default config.", + " exit confirmation setting 'enabled' specified from default config.", " maximum footlinks value '3' specified from default config.", " color depth setting '256' specified from default config.", " notify setting 'disabled' specified from default config.", @@ -257,6 +258,7 @@ def test_warning_regarding_incomplete_theme( "\x1b[93m WARNING: Incomplete theme; results may vary!", f" {expected_warning}\x1b[0m", " autohide setting 'no_autohide' specified from default config.", + " exit confirmation setting 'enabled' specified from default config.", " maximum footlinks value '3' specified from default config.", " color depth setting '256' specified from default config.", " notify setting 'disabled' specified from default config.", @@ -475,6 +477,7 @@ def test_successful_main_function_with_config( "Loading with:", " theme 'zt_dark' specified in zuliprc file (by alias 'default').", " autohide setting 'autohide' specified in zuliprc file.", + " exit confirmation setting 'enabled' specified from default config.", f" maximum footlinks value {footlinks_output}", " color depth setting '256' specified in zuliprc file.", " notify setting 'enabled' specified in zuliprc file.", diff --git a/tests/core/test_core.py b/tests/core/test_core.py index 9f13a4061a..033f126fd7 100644 --- a/tests/core/test_core.py +++ b/tests/core/test_core.py @@ -48,6 +48,7 @@ def controller(self, mocker: MockerFixture) -> Controller: self.in_explore_mode = False self.autohide = True # FIXME Add tests for no-autohide self.notify_enabled = False + self.exit_confirmation = True self.maximum_footlinks = 3 result = Controller( config_file=self.config_file, @@ -60,6 +61,7 @@ def controller(self, mocker: MockerFixture) -> Controller: **dict( autohide=self.autohide, notify=self.notify_enabled, + exit_confirmation=self.exit_confirmation, ), ) result.view.message_view = mocker.Mock() # set in View.__init__ diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index 87794e96f6..a3ccd9d18e 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -61,6 +61,7 @@ class SettingData(NamedTuple): VALID_BOOLEAN_SETTINGS: Dict[str, Tuple[str, str]] = { "autohide": ("autohide", "no_autohide"), "notify": ("enabled", "disabled"), + "exit_confirmation": ("enabled", "disabled"), } COLOR_DEPTH_ARGS_TO_DEPTHS: Dict[str, int] = { @@ -78,10 +79,14 @@ class SettingData(NamedTuple): "footlinks": "enabled", "color-depth": "256", "maximum-footlinks": "3", + "exit_confirmation": "enabled", } assert DEFAULT_SETTINGS["autohide"] in VALID_BOOLEAN_SETTINGS["autohide"] assert DEFAULT_SETTINGS["notify"] in VALID_BOOLEAN_SETTINGS["notify"] assert DEFAULT_SETTINGS["color-depth"] in COLOR_DEPTH_ARGS_TO_DEPTHS +assert ( + DEFAULT_SETTINGS["exit_confirmation"] in VALID_BOOLEAN_SETTINGS["exit_confirmation"] +) def in_color(color: str, text: str) -> str: @@ -537,6 +542,7 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None: incomplete_theme_text += " (all themes are incomplete)" print(in_color("yellow", incomplete_theme_text)) print_setting("autohide setting", zterm["autohide"]) + print_setting("exit confirmation setting", zterm["exit_confirmation"]) if zterm["footlinks"].source == ConfigSource.ZULIPRC: print_setting( "maximum footlinks value", diff --git a/zulipterminal/core.py b/zulipterminal/core.py index f93d89e53d..2799210ab9 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -70,12 +70,14 @@ def __init__( in_explore_mode: bool, autohide: bool, notify: bool, + exit_confirmation: bool, ) -> None: self.theme_name = theme_name self.theme = theme self.color_depth = color_depth self.in_explore_mode = in_explore_mode self.autohide = autohide + self.exit_confirmation = exit_confirmation self.notify_enabled = notify self.maximum_footlinks = maximum_footlinks @@ -107,7 +109,12 @@ def __init__( self._exception_pipe = self.loop.watch_pipe(self._raise_exception) # Register new ^C handler - signal.signal(signal.SIGINT, self.exit_handler) + signal.signal( + signal.SIGINT, + self.prompting_exit_handler + if self.exit_confirmation + else self.no_prompt_exit_handler, + ) def raise_exception_in_main_thread( self, exc_info: ExceptionInfo, *, critical: bool @@ -621,7 +628,10 @@ def deregister_client(self) -> None: self.client.deregister(queue_id, 1.0) sys.exit(0) - def exit_handler(self, signum: int, frame: Any) -> None: + def no_prompt_exit_handler(self, signum: int, frame: Any) -> None: + self.deregister_client() + + def prompting_exit_handler(self, signum: int, frame: Any) -> None: question = urwid.Text( ("bold", " Please confirm that you wish to exit Zulip-Terminal "), "center", From 35cd630b462ffd8271a725705a99c28b1396d9b8 Mon Sep 17 00:00:00 2001 From: rsashank Date: Tue, 7 Nov 2023 05:27:58 +0530 Subject: [PATCH 154/276] views/core: Add Exit confirmation setting to AboutView popup. Tests updated. --- tests/ui_tools/test_popups.py | 2 ++ zulipterminal/core.py | 1 + zulipterminal/ui_tools/views.py | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py index e5253973eb..be9ee49eec 100644 --- a/tests/ui_tools/test_popups.py +++ b/tests/ui_tools/test_popups.py @@ -200,6 +200,7 @@ def mock_external_classes(self, mocker: MockerFixture) -> None: notify_enabled=False, autohide_enabled=False, maximum_footlinks=3, + exit_confirmation_enabled=False, ) @pytest.mark.parametrize( @@ -241,6 +242,7 @@ def test_feature_level_content( notify_enabled=False, autohide_enabled=False, maximum_footlinks=3, + exit_confirmation_enabled=False, ) assert len(about_view.feature_level_content) == ( diff --git a/zulipterminal/core.py b/zulipterminal/core.py index 2799210ab9..ddc7e89b62 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -307,6 +307,7 @@ def show_about(self) -> None: notify_enabled=self.notify_enabled, autohide_enabled=self.autohide, maximum_footlinks=self.maximum_footlinks, + exit_confirmation_enabled=self.exit_confirmation, ), "area:help", ) diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 463478b24e..fcb116480d 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -1086,6 +1086,7 @@ def __init__( autohide_enabled: bool, maximum_footlinks: int, notify_enabled: bool, + exit_confirmation_enabled: bool, ) -> None: self.feature_level_content = ( [("Feature level", str(server_feature_level))] @@ -1103,6 +1104,10 @@ def __init__( ("Maximum footlinks", str(maximum_footlinks)), ("Color depth", str(color_depth)), ("Notifications", "enabled" if notify_enabled else "disabled"), + ( + "Exit confirmation", + "enabled" if exit_confirmation_enabled else "disabled", + ), ], ), ( From a1760cf1b59db0439b95166c3635c21fb4f308f8 Mon Sep 17 00:00:00 2001 From: laomuon Date: Tue, 7 Nov 2023 17:48:04 +0100 Subject: [PATCH 155/276] FAQ: Add Alacritty to list of successfully run terminal emulators. --- docs/FAQ.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/FAQ.md b/docs/FAQ.md index b7501883e9..b1474440e9 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -106,6 +106,7 @@ We have reports of success on the following terminal emulators: * Terminator (https://github.com/gnome-terminator/terminator) * iterm2 (https://iterm2.com/) **Mac only** * mosh (https://mosh.org/) +* Alaccrity (https://github.com/alacritty/alacritty) Issues have been reported with the following: From 856e6ff74422327ea84e794b85fbbcf7cceebefc Mon Sep 17 00:00:00 2001 From: supascooopa Date: Wed, 1 Nov 2023 09:15:32 +0200 Subject: [PATCH 156/276] messages: Show prefix symbols before the title of the current narrow. As previously done to other parts of the UI, this commit adds prefix symbols to the narrow title (before the message search input area). This is applied to all supported narrows: - all messages - all direct messages - mentions - starred messages - single stream - single topic - direct message conversations (1:1 and group) The spacing around the prefixes is laid out to be similar to that for message headings, so that the icons line up cleanly. This is intended to make the UI more similar to the web app UI and unify the overall look. Tests adjusted accordingly. --- tests/ui_tools/test_messages.py | 52 +++++++++++++++++++----------- zulipterminal/ui_tools/messages.py | 25 +++++++------- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/tests/ui_tools/test_messages.py b/tests/ui_tools/test_messages.py index c87cc07203..cc98c8dd37 100644 --- a/tests/ui_tools/test_messages.py +++ b/tests/ui_tools/test_messages.py @@ -9,8 +9,11 @@ from zulipterminal.config.keys import keys_for_command from zulipterminal.config.symbols import ( + ALL_MESSAGES_MARKER, DIRECT_MESSAGE_MARKER, + MENTIONED_MESSAGES_MARKER, QUOTED_TEXT_MARKER, + STARRED_MESSAGES_MARKER, STATUS_INACTIVE, STREAM_MARKER_PUBLIC, STREAM_TOPIC_SEPARATOR, @@ -900,99 +903,112 @@ def test_main_view_generates_PM_header( [], 0, f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", - "All messages", + f" {ALL_MESSAGES_MARKER} All messages", + ), + ( + [], + 1, + f" {DIRECT_MESSAGE_MARKER} You and ", + f" {ALL_MESSAGES_MARKER} All messages", + ), + ( + [], + 2, + f" {DIRECT_MESSAGE_MARKER} You and ", + f" {ALL_MESSAGES_MARKER} All messages", ), - ([], 1, f" {DIRECT_MESSAGE_MARKER} You and ", "All messages"), - ([], 2, f" {DIRECT_MESSAGE_MARKER} You and ", "All messages"), ( [["stream", "PTEST"]], 0, f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", - ("bar", [("s#bd6", "PTEST")]), + ("bar", ("s#bd6", f" {STREAM_MARKER_PUBLIC} PTEST")), ), ( [["stream", "PTEST"], ["topic", "b"]], 0, f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR}", - ("bar", [("s#bd6", "PTEST"), ("s#bd6", ": topic narrow")]), + ( + "bar", + ("s#bd6", f" {STREAM_MARKER_PUBLIC} PTEST: topic narrow"), + ), ), ( [["is", "private"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - "All direct messages", + f" {DIRECT_MESSAGE_MARKER} All direct messages", ), ( [["is", "private"]], 2, f" {DIRECT_MESSAGE_MARKER} You and ", - "All direct messages", + f" {DIRECT_MESSAGE_MARKER} All direct messages", ), ( [["pm-with", "boo@zulip.com"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - "Direct message conversation", + f" {DIRECT_MESSAGE_MARKER} Direct message conversation", ), ( [["pm-with", "boo@zulip.com, bar@zulip.com"]], 2, f" {DIRECT_MESSAGE_MARKER} You and ", - "Group direct message conversation", + f" {DIRECT_MESSAGE_MARKER} Group direct message conversation", ), ( [["is", "starred"]], 0, f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", - "Starred messages", + f" {STARRED_MESSAGES_MARKER} Starred messages", ), ( [["is", "starred"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - "Starred messages", + f" {STARRED_MESSAGES_MARKER} Starred messages", ), ( [["is", "starred"]], 2, f" {DIRECT_MESSAGE_MARKER} You and ", - "Starred messages", + f" {STARRED_MESSAGES_MARKER} Starred messages", ), ( [["is", "starred"], ["search", "FOO"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - "Starred messages", + f" {STARRED_MESSAGES_MARKER} Starred messages", ), ( [["search", "FOO"]], 0, f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", - "All messages", + f" {ALL_MESSAGES_MARKER} All messages", ), ( [["is", "mentioned"]], 0, f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", - "Mentions", + f" {MENTIONED_MESSAGES_MARKER} Mentions", ), ( [["is", "mentioned"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - "Mentions", + f" {MENTIONED_MESSAGES_MARKER} Mentions", ), ( [["is", "mentioned"]], 2, f" {DIRECT_MESSAGE_MARKER} You and ", - "Mentions", + f" {MENTIONED_MESSAGES_MARKER} Mentions", ), ( [["is", "mentioned"], ["search", "FOO"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - "Mentions", + f" {MENTIONED_MESSAGES_MARKER} Mentions", ), ], ) diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index 47600b4606..ce120296e7 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -18,10 +18,13 @@ from zulipterminal.api_types import Message from zulipterminal.config.keys import is_command_key, primary_key_for_command from zulipterminal.config.symbols import ( + ALL_MESSAGES_MARKER, DIRECT_MESSAGE_MARKER, + MENTIONED_MESSAGES_MARKER, MESSAGE_CONTENT_MARKER, MESSAGE_HEADER_DIVIDER, QUOTED_TEXT_MARKER, + STARRED_MESSAGES_MARKER, STREAM_TOPIC_SEPARATOR, TIME_MENTION_MARKER, ) @@ -211,34 +214,34 @@ def top_search_bar(self) -> Any: else: self.model.controller.view.search_box.text_box.set_edit_text("") if curr_narrow == []: - text_to_fill = "All messages" + text_to_fill = f" {ALL_MESSAGES_MARKER} All messages" elif len(curr_narrow) == 1 and curr_narrow[0][1] == "private": - text_to_fill = "All direct messages" + text_to_fill = f" {DIRECT_MESSAGE_MARKER} All direct messages" elif len(curr_narrow) == 1 and curr_narrow[0][1] == "starred": - text_to_fill = "Starred messages" + text_to_fill = f" {STARRED_MESSAGES_MARKER} Starred messages" elif len(curr_narrow) == 1 and curr_narrow[0][1] == "mentioned": - text_to_fill = "Mentions" + text_to_fill = f" {MENTIONED_MESSAGES_MARKER} Mentions" elif self.message["type"] == "stream": assert self.stream_id is not None + bar_color = self.model.stream_dict[self.stream_id]["color"] bar_color = f"s{bar_color}" + stream_access_type = self.model.stream_access_type(self.stream_id) + stream_icon = STREAM_ACCESS_TYPE[stream_access_type]["icon"] if len(curr_narrow) == 2 and curr_narrow[1][0] == "topic": text_to_fill = ( "bar", # type: ignore[assignment] - [ - (bar_color, self.stream_name), - (bar_color, ": topic narrow"), - ], + (bar_color, f" {stream_icon} {self.stream_name}: topic narrow"), ) else: text_to_fill = ( "bar", # type: ignore[assignment] - [(bar_color, self.stream_name)], + (bar_color, f" {stream_icon} {self.stream_name}"), ) elif len(curr_narrow) == 1 and len(curr_narrow[0][1].split(",")) > 1: - text_to_fill = "Group direct message conversation" + text_to_fill = f" {DIRECT_MESSAGE_MARKER} Group direct message conversation" else: - text_to_fill = "Direct message conversation" + text_to_fill = f" {DIRECT_MESSAGE_MARKER} Direct message conversation" if is_search_narrow: title_markup = ( From 972a30c2e0cf750b57f93cdab403d621f6f74144 Mon Sep 17 00:00:00 2001 From: supascooopa Date: Thu, 30 Nov 2023 20:48:37 +0200 Subject: [PATCH 157/276] boxes/messages: Extend narrow title colored background area. The colored background area of stream message headings are already visually balanced with a leading and trailing space. The narrow title prefixes added in the previous commit contain a leading space to align the prefixes with those in message headings; this commit adds a trailing space, which balances the colored area in a similar way to the stream message headings and avoids an abrupt end to the narrow text. The existing spacing between the narrow title and message search text is maintained by removing one of the two spaces between the `Search [/]:` and the narrow title with a general background color, and substituting it with an additional space at the end of each narrow title. Tests adjusted accordingly. --- tests/ui_tools/test_messages.py | 36 +++++++++++++++--------------- zulipterminal/ui_tools/boxes.py | 2 +- zulipterminal/ui_tools/messages.py | 18 ++++++++------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/tests/ui_tools/test_messages.py b/tests/ui_tools/test_messages.py index cc98c8dd37..f56da14abe 100644 --- a/tests/ui_tools/test_messages.py +++ b/tests/ui_tools/test_messages.py @@ -903,25 +903,25 @@ def test_main_view_generates_PM_header( [], 0, f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", - f" {ALL_MESSAGES_MARKER} All messages", + f" {ALL_MESSAGES_MARKER} All messages ", ), ( [], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {ALL_MESSAGES_MARKER} All messages", + f" {ALL_MESSAGES_MARKER} All messages ", ), ( [], 2, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {ALL_MESSAGES_MARKER} All messages", + f" {ALL_MESSAGES_MARKER} All messages ", ), ( [["stream", "PTEST"]], 0, f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", - ("bar", ("s#bd6", f" {STREAM_MARKER_PUBLIC} PTEST")), + ("bar", ("s#bd6", f" {STREAM_MARKER_PUBLIC} PTEST ")), ), ( [["stream", "PTEST"], ["topic", "b"]], @@ -929,86 +929,86 @@ def test_main_view_generates_PM_header( f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR}", ( "bar", - ("s#bd6", f" {STREAM_MARKER_PUBLIC} PTEST: topic narrow"), + ("s#bd6", f" {STREAM_MARKER_PUBLIC} PTEST: topic narrow "), ), ), ( [["is", "private"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {DIRECT_MESSAGE_MARKER} All direct messages", + f" {DIRECT_MESSAGE_MARKER} All direct messages ", ), ( [["is", "private"]], 2, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {DIRECT_MESSAGE_MARKER} All direct messages", + f" {DIRECT_MESSAGE_MARKER} All direct messages ", ), ( [["pm-with", "boo@zulip.com"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {DIRECT_MESSAGE_MARKER} Direct message conversation", + f" {DIRECT_MESSAGE_MARKER} Direct message conversation ", ), ( [["pm-with", "boo@zulip.com, bar@zulip.com"]], 2, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {DIRECT_MESSAGE_MARKER} Group direct message conversation", + f" {DIRECT_MESSAGE_MARKER} Group direct message conversation ", ), ( [["is", "starred"]], 0, f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", - f" {STARRED_MESSAGES_MARKER} Starred messages", + f" {STARRED_MESSAGES_MARKER} Starred messages ", ), ( [["is", "starred"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {STARRED_MESSAGES_MARKER} Starred messages", + f" {STARRED_MESSAGES_MARKER} Starred messages ", ), ( [["is", "starred"]], 2, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {STARRED_MESSAGES_MARKER} Starred messages", + f" {STARRED_MESSAGES_MARKER} Starred messages ", ), ( [["is", "starred"], ["search", "FOO"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {STARRED_MESSAGES_MARKER} Starred messages", + f" {STARRED_MESSAGES_MARKER} Starred messages ", ), ( [["search", "FOO"]], 0, f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", - f" {ALL_MESSAGES_MARKER} All messages", + f" {ALL_MESSAGES_MARKER} All messages ", ), ( [["is", "mentioned"]], 0, f" {STREAM_MARKER_PUBLIC} PTEST {STREAM_TOPIC_SEPARATOR} ", - f" {MENTIONED_MESSAGES_MARKER} Mentions", + f" {MENTIONED_MESSAGES_MARKER} Mentions ", ), ( [["is", "mentioned"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {MENTIONED_MESSAGES_MARKER} Mentions", + f" {MENTIONED_MESSAGES_MARKER} Mentions ", ), ( [["is", "mentioned"]], 2, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {MENTIONED_MESSAGES_MARKER} Mentions", + f" {MENTIONED_MESSAGES_MARKER} Mentions ", ), ( [["is", "mentioned"], ["search", "FOO"]], 1, f" {DIRECT_MESSAGE_MARKER} You and ", - f" {MENTIONED_MESSAGES_MARKER} Mentions", + f" {MENTIONED_MESSAGES_MARKER} Mentions ", ), ], ) diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index 36128f444a..db5f396715 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -928,7 +928,7 @@ def main_view(self) -> Any: self.search_bar = urwid.Columns( [ ("pack", self.conversation_focus), - ("pack", urwid.Text(" ")), + ("pack", urwid.Text(" ")), self.text_box, ] ) diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index ce120296e7..15983062fa 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -214,13 +214,13 @@ def top_search_bar(self) -> Any: else: self.model.controller.view.search_box.text_box.set_edit_text("") if curr_narrow == []: - text_to_fill = f" {ALL_MESSAGES_MARKER} All messages" + text_to_fill = f" {ALL_MESSAGES_MARKER} All messages " elif len(curr_narrow) == 1 and curr_narrow[0][1] == "private": - text_to_fill = f" {DIRECT_MESSAGE_MARKER} All direct messages" + text_to_fill = f" {DIRECT_MESSAGE_MARKER} All direct messages " elif len(curr_narrow) == 1 and curr_narrow[0][1] == "starred": - text_to_fill = f" {STARRED_MESSAGES_MARKER} Starred messages" + text_to_fill = f" {STARRED_MESSAGES_MARKER} Starred messages " elif len(curr_narrow) == 1 and curr_narrow[0][1] == "mentioned": - text_to_fill = f" {MENTIONED_MESSAGES_MARKER} Mentions" + text_to_fill = f" {MENTIONED_MESSAGES_MARKER} Mentions " elif self.message["type"] == "stream": assert self.stream_id is not None @@ -231,17 +231,19 @@ def top_search_bar(self) -> Any: if len(curr_narrow) == 2 and curr_narrow[1][0] == "topic": text_to_fill = ( "bar", # type: ignore[assignment] - (bar_color, f" {stream_icon} {self.stream_name}: topic narrow"), + (bar_color, f" {stream_icon} {self.stream_name}: topic narrow "), ) else: text_to_fill = ( "bar", # type: ignore[assignment] - (bar_color, f" {stream_icon} {self.stream_name}"), + (bar_color, f" {stream_icon} {self.stream_name} "), ) elif len(curr_narrow) == 1 and len(curr_narrow[0][1].split(",")) > 1: - text_to_fill = f" {DIRECT_MESSAGE_MARKER} Group direct message conversation" + text_to_fill = ( + f" {DIRECT_MESSAGE_MARKER} Group direct message conversation " + ) else: - text_to_fill = f" {DIRECT_MESSAGE_MARKER} Direct message conversation" + text_to_fill = f" {DIRECT_MESSAGE_MARKER} Direct message conversation " if is_search_narrow: title_markup = ( From eee60796aa7b3d72d0e2ffe950853958e6832aa7 Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Sat, 9 Dec 2023 17:39:19 -0800 Subject: [PATCH 158/276] mailmap: Add entry for Vishwesh Pillai. --- .mailmap | 1 + 1 file changed, 1 insertion(+) diff --git a/.mailmap b/.mailmap index 518228ec3e..3304f0a9f0 100644 --- a/.mailmap +++ b/.mailmap @@ -15,3 +15,4 @@ Zeeshan Equbal <54993043+zee-bit@users.noreply.github. Zeeshan Equbal Mounil K. Shah Mounil K. Shah +Vishwesh Pillai From 585722a206e8c8d501ecfd4d9b2ea31235d0f51d Mon Sep 17 00:00:00 2001 From: Naman Agrawal Date: Tue, 12 Dec 2023 16:52:01 +0530 Subject: [PATCH 159/276] messages: Highlight @topic mentions using new rendered content. Add class name topic-mention to existing user-mention and user-group-mention to highlight @topic mentions in a similar way. Test case added. Fixes #1418. --- tests/ui_tools/test_messages.py | 5 +++++ zulipterminal/ui_tools/messages.py | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/ui_tools/test_messages.py b/tests/ui_tools/test_messages.py index f56da14abe..d4d9e806b4 100644 --- a/tests/ui_tools/test_messages.py +++ b/tests/ui_tools/test_messages.py @@ -114,6 +114,11 @@ def test_private_message_to_self(self, mocker): [("msg_mention", "@A Group")], id="group-mention", ), + case( + '@topic', + [("msg_mention", "@topic")], + id="topic-mention", + ), case("some code", [("pygments:w", "some code")], id="inline-code"), case( '
' diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index 15983062fa..1a9bb9db02 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -442,9 +442,10 @@ def soup2markup( markup.append(("msg_math", tag_text)) elif tag == "span" and ( - {"user-group-mention", "user-mention"} & set(tag_classes) + {"user-group-mention", "user-mention", "topic-mention"} + & set(tag_classes) ): - # USER MENTIONS & USER-GROUP MENTIONS + # USER, USER-GROUP & TOPIC MENTIONS markup.append(("msg_mention", tag_text)) elif tag == "a": # LINKS From 1c30f074a1cc2cfaeca79499936c208c36227986 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 13 Dec 2023 17:28:25 -0800 Subject: [PATCH 160/276] views: Expose Stream info popup table data to allow testing. Minimal tests added for this popup, for: - basic section titles/structure - text expected for the stream email (in the stream details section) This could be generalized to other popups that use the same helper functions, though as always we should avoid testing data exactly as it is in the code. --- tests/ui_tools/test_popups.py | 18 ++++++++++++++++++ zulipterminal/ui_tools/views.py | 7 ++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py index be9ee49eec..1a3520a9a4 100644 --- a/tests/ui_tools/test_popups.py +++ b/tests/ui_tools/test_popups.py @@ -1379,6 +1379,24 @@ def test_popup_height( # + 3(checkboxes) + [2-5](fields, depending upon server_feature_level) assert stream_info_view.height == expected_height + def test_stream_info_content__sections(self) -> None: + assert len(self.stream_info_view._stream_info_content) == 2 + + stream_details, stream_settings = self.stream_info_view._stream_info_content + assert stream_details[0] == "Stream Details" + assert stream_settings[0] == "Stream settings" + + def test_stream_info_content__email_copy_text( + self, general_stream: Dict[str, Any] + ) -> None: + stream_details, _ = self.stream_info_view._stream_info_content + stream_details_data = stream_details[1] + + assert ( + "Stream email", + "Press 'c' to copy Stream email address", + ) in stream_details_data + @pytest.mark.parametrize("key", keys_for_command("COPY_STREAM_EMAIL")) def test_keypress_copy_stream_email( self, key: str, widget_size: Callable[[Widget], urwid_Size] diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index fcb116480d..46f6246b0e 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -1393,7 +1393,8 @@ def __init__(self, controller: Any, stream_id: int) -> None: ) desc = urwid.Text(self.markup_desc) - stream_info_content = [ + # NOTE: This is treated as a member to make it easier to test + self._stream_info_content = [ ( "Stream Details", [ @@ -1420,7 +1421,7 @@ def __init__(self, controller: Any, stream_id: int) -> None: ] # type: PopUpViewTableContent popup_width, column_widths = self.calculate_table_widths( - stream_info_content, len(title) + self._stream_info_content, len(title) ) muted_setting = urwid.CheckBox( @@ -1473,7 +1474,7 @@ def __init__(self, controller: Any, stream_id: int) -> None: ), ] self.widgets = self.make_table_with_categories( - stream_info_content, column_widths + self._stream_info_content, column_widths ) # Stream description. From a24d23a26e59b01ab939c78825f53dd8a606ccf6 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 13 Dec 2023 17:50:39 -0800 Subject: [PATCH 161/276] bugfix: views: Stream email is not in subscription objects from ZFL226. This change avoids a crash related to the removal of the email_address field from subscription objects at feature level 226 (ZFL226) onwards. The minimal fix here is to indicate the email is unavailable, if this situation is detected. The change at ZFL226 (Zulip 7.5 and 8.x) enables fetching of the email address of a stream on demand, which we can use later to dynamically fetch the data and insert it into the popup. Since that call could fail, this error handling is likely to remain useful. Test generalized to include previous and new behavior. --- tests/ui_tools/test_popups.py | 31 ++++++++++++++++++++++++------- zulipterminal/ui_tools/views.py | 18 +++++++++++------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py index 1a3520a9a4..e0caf548ba 100644 --- a/tests/ui_tools/test_popups.py +++ b/tests/ui_tools/test_popups.py @@ -1386,16 +1386,33 @@ def test_stream_info_content__sections(self) -> None: assert stream_details[0] == "Stream Details" assert stream_settings[0] == "Stream settings" + @pytest.mark.parametrize( + "stream_email_present, expected_copy_text", + [ + (False, "< Stream email is unavailable >"), + (True, "Press 'c' to copy Stream email address"), + ], + ) def test_stream_info_content__email_copy_text( - self, general_stream: Dict[str, Any] + self, + general_stream: Dict[str, Any], + stream_email_present: bool, + expected_copy_text: str, ) -> None: - stream_details, _ = self.stream_info_view._stream_info_content + if not stream_email_present: + del general_stream["email_address"] + + model = self.controller.model + stream_id = general_stream["stream_id"] + model.stream_dict = {stream_id: general_stream} + + # Custom, to enable variation of stream data before creation + stream_info_view = StreamInfoView(self.controller, stream_id) + + stream_details, _ = stream_info_view._stream_info_content stream_details_data = stream_details[1] - assert ( - "Stream email", - "Press 'c' to copy Stream email address", - ) in stream_details_data + assert ("Stream email", expected_copy_text) in stream_details_data @pytest.mark.parametrize("key", keys_for_command("COPY_STREAM_EMAIL")) def test_keypress_copy_stream_email( @@ -1406,7 +1423,7 @@ def test_keypress_copy_stream_email( self.stream_info_view.keypress(size, key) self.controller.copy_to_clipboard.assert_called_once_with( - self.stream_info_view.stream_email, "Stream email" + self.stream_info_view._stream_email, "Stream email" ) @pytest.mark.parametrize( diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 46f6246b0e..fcdfc83901 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -1377,8 +1377,15 @@ def __init__(self, controller: Any, stream_id: int) -> None: else "Not Public to Users" ) member_keys = ", ".join(map(repr, keys_for_command("STREAM_MEMBERS"))) - self.stream_email = stream["email_address"] - email_keys = ", ".join(map(repr, keys_for_command("COPY_STREAM_EMAIL"))) + + # FIXME: This field was removed from the subscription data in Zulip 7.5 / ZFL226 + # We should use the new /streams/{stream_id}/email_address endpoint instead + self._stream_email = stream.get("email_address", None) + if self._stream_email is None: + stream_copy_text = "< Stream email is unavailable >" + else: + email_keys = ", ".join(map(repr, keys_for_command("COPY_STREAM_EMAIL"))) + stream_copy_text = f"Press {email_keys} to copy Stream email address" weekly_traffic = stream["stream_weekly_traffic"] weekly_msg_count = ( @@ -1409,10 +1416,7 @@ def __init__(self, controller: Any, stream_id: int) -> None: "Stream Members", f"{total_members} (Press {member_keys} to view list)", ), - ( - "Stream email", - f"Press {email_keys} to copy Stream email address", - ), + ("Stream email", stream_copy_text), ("History of Stream", f"{availability_of_history}"), ("Posting Policy", f"{stream_policy}"), ], @@ -1503,7 +1507,7 @@ def keypress(self, size: urwid_Size, key: str) -> str: if is_command_key("STREAM_MEMBERS", key): self.controller.show_stream_members(stream_id=self.stream_id) elif is_command_key("COPY_STREAM_EMAIL", key): - self.controller.copy_to_clipboard(self.stream_email, "Stream email") + self.controller.copy_to_clipboard(self._stream_email, "Stream email") return super().keypress(size, key) From 1bc817580ffd50f348ce45a768e18423b6cbac81 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Wed, 13 Dec 2023 18:09:13 -0800 Subject: [PATCH 162/276] views: Skip copying email in Stream Info popup, if it is absent. While the copying shortcut key is not shown explicitly, it could still previously be used. The copied content would be unimportant, but could unnecessarily overwrite something else on the clipboard, so we skip copying if the email is absent. Test updated. --- tests/ui_tools/test_popups.py | 18 ++++++++++++++---- zulipterminal/ui_tools/views.py | 4 +++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py index e0caf548ba..2c5ce92d84 100644 --- a/tests/ui_tools/test_popups.py +++ b/tests/ui_tools/test_popups.py @@ -1414,17 +1414,27 @@ def test_stream_info_content__email_copy_text( assert ("Stream email", expected_copy_text) in stream_details_data + @pytest.mark.parametrize("normalized_email_address", ("user@example.com", None)) @pytest.mark.parametrize("key", keys_for_command("COPY_STREAM_EMAIL")) def test_keypress_copy_stream_email( - self, key: str, widget_size: Callable[[Widget], urwid_Size] + self, + key: str, + normalized_email_address: Optional[str], + widget_size: Callable[[Widget], urwid_Size], ) -> None: size = widget_size(self.stream_info_view) + # This patches inside the object, which is fragile but tests the logic + # Note that the assert uses the same variable + self.stream_info_view._stream_email = normalized_email_address self.stream_info_view.keypress(size, key) - self.controller.copy_to_clipboard.assert_called_once_with( - self.stream_info_view._stream_email, "Stream email" - ) + if normalized_email_address is not None: + self.controller.copy_to_clipboard.assert_called_once_with( + self.stream_info_view._stream_email, "Stream email" + ) + else: + self.controller.copy_to_clipboard.assert_not_called() @pytest.mark.parametrize( "rendered_description, expected_markup", diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index fcdfc83901..1d63b5eb92 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -1506,7 +1506,9 @@ def toggle_visual_notification(self, button: Any, new_state: bool) -> None: def keypress(self, size: urwid_Size, key: str) -> str: if is_command_key("STREAM_MEMBERS", key): self.controller.show_stream_members(stream_id=self.stream_id) - elif is_command_key("COPY_STREAM_EMAIL", key): + elif ( + is_command_key("COPY_STREAM_EMAIL", key) and self._stream_email is not None + ): self.controller.copy_to_clipboard(self._stream_email, "Stream email") return super().keypress(size, key) From d9efc567d79ba82bbcc84541f6ebc7859a4c2c36 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Sun, 7 Jan 2024 18:54:33 -0800 Subject: [PATCH 163/276] requirements: Pin lxml at ~=4.9.2 instead of >=4.9.2. This works around a CI failure with PyPy 3.7, which occurs with the latest 5.0.1 release. The exact error is E TypeError: inheritance from PyVarObject types like 'str' not currently supported Limited time was spent debugging this, since Python 3.7 support will be dropped soon. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7b416d844d..d509d3e496 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def long_description(): "zulip>=0.8.2", "urwid_readline>=0.13", "beautifulsoup4>=4.11.1", - "lxml>=4.9.2", + "lxml~=4.9.2", "pygments~=2.15.1", "typing_extensions~=4.5.0", "python-dateutil>=2.8.2", From 8f0f980ca978516e68ac875c60b58b15a3894e5f Mon Sep 17 00:00:00 2001 From: jrijul1201 Date: Tue, 19 Dec 2023 13:27:49 +0530 Subject: [PATCH 164/276] config/themes: Convert pygments styles to urwid compatible styles. Pygments 2.16.0 introduced a style to support a combination of bold and italic styling in pygments/pygments#2444. Both of our gruvbox themes and the light native theme gain a 'bold italic' style via pygments as a result, which urwid fails to parse and blocks the application from loading. This work would represent a clean fix for #1431, which was temporarily fixed by pinning pygments at ~=2.15.1 in #1433. This minimally handles spaces and the shortened `italic` in pygments styles, sufficient to resolve the current style issue. Note that other pygments styles in themes that we do not yet use may need additional translation or adjustment. Tests added. Fixes part of #1434. --- tests/config/test_themes.py | 66 +++++++++++++++++++++++++++++++++- zulipterminal/config/themes.py | 24 ++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/tests/config/test_themes.py b/tests/config/test_themes.py index b06e35df39..b95c4e33cb 100644 --- a/tests/config/test_themes.py +++ b/tests/config/test_themes.py @@ -5,13 +5,14 @@ import pytest from pygments.styles.material import MaterialStyle from pygments.styles.perldoc import PerldocStyle -from pygments.token import STANDARD_TYPES +from pygments.token import STANDARD_TYPES, _TokenType from pytest import param as case from pytest_mock import MockerFixture from zulipterminal.config.regexes import REGEX_COLOR_VALID_FORMATS from zulipterminal.config.themes import ( REQUIRED_STYLES, + STYLE_TRANSLATIONS, THEMES, InvalidThemeColorCode, MissingThemeAttributeError, @@ -20,6 +21,7 @@ complete_and_incomplete_themes, generate_pygments_styles, generate_theme, + generate_urwid_compatible_pygments_styles, parse_themefile, valid_16_color_codes, validate_colors, @@ -402,3 +404,65 @@ class Color3(Enum): + "- GRAY_244 = dark_gra\n" + "- LIGHT2 = whit" ) + + +@pytest.mark.parametrize( + "pygments_styles, expected_styles, style_translations", + [ + case( + {}, + {}, + STYLE_TRANSLATIONS, + id="empty_input", + ), + case( + { + "token1": "style1", + "token2": "style2", + }, + { + "token1": "style1", + "token2": "style2", + }, + {}, + id="empty_translations", + ), + case( + { + "token1": "bold italic", # pygments/pygments#2444 + "token2": "italic bold", # + order shouldn't matter + "token3": "italic #abc", # + italic should work with color + }, + { + "token1": "bold,italics", + "token2": "italics,bold", + "token3": "italics,#abc", + }, + STYLE_TRANSLATIONS, + id="default_translations", + ), + case( + { + "token1": "style italic", + "token2": "#abc", + }, + { + "token1": "newstyle italic", + "token2": "#abc", + }, + {"style": "newstyle"}, + id="custom_translations", + ), + ], +) +def test_generate_urwid_compatible_pygments_styles( + pygments_styles: Dict[_TokenType, str], # NOTE: placeholder string values used + expected_styles: Dict[_TokenType, str], # in parametrized dict keys + style_translations: Dict[str, str], +) -> None: + generated_styles = generate_urwid_compatible_pygments_styles( + pygments_styles, style_translations + ) + + assert set(expected_styles) == set(generated_styles) # keys unchanged + assert sorted(expected_styles.items()) == sorted(generated_styles.items()) diff --git a/zulipterminal/config/themes.py b/zulipterminal/config/themes.py index c9e5174a40..7c6dfe56da 100644 --- a/zulipterminal/config/themes.py +++ b/zulipterminal/config/themes.py @@ -3,7 +3,7 @@ """ from typing import Any, Dict, List, Optional, Tuple, Union -from pygments.token import STANDARD_TYPES +from pygments.token import STANDARD_TYPES, _TokenType from zulipterminal.config.color import term16 from zulipterminal.themes import gruvbox_dark, gruvbox_light, zt_blue, zt_dark, zt_light @@ -128,6 +128,13 @@ "white", ] +# These are style_translations for translating pygments styles into +# urwid-compatible styles +STYLE_TRANSLATIONS = { + " ": ",", + "italic": "italics", +} + class ThemeError(Exception): pass @@ -255,6 +262,19 @@ def parse_themefile( return urwid_theme +def generate_urwid_compatible_pygments_styles( + pygments_styles: Dict[_TokenType, str], + style_translations: Dict[str, str] = STYLE_TRANSLATIONS, +) -> Dict[_TokenType, str]: + urwid_compatible_styles = {} + for token, style in pygments_styles.items(): + updated_style = style + for old_value, new_value in style_translations.items(): + updated_style = updated_style.replace(old_value, new_value) + urwid_compatible_styles[token] = updated_style + return urwid_compatible_styles + + def generate_pygments_styles(pygments: Dict[str, Any]) -> ThemeSpec: """ This function adds pygments styles for use in syntax @@ -278,6 +298,8 @@ def generate_pygments_styles(pygments: Dict[str, Any]) -> ThemeSpec: term16_bg = term16.background_color theme_styles_from_pygments: ThemeSpec = [] + pygments_styles = generate_urwid_compatible_pygments_styles(pygments_styles) + for token, css_class in STANDARD_TYPES.items(): if css_class in pygments_overrides: pygments_styles[token] = pygments_overrides[css_class] From 646a04e4f7c7deddf4e3d7115316d485db5aa38a Mon Sep 17 00:00:00 2001 From: jrijul1201 Date: Thu, 21 Dec 2023 15:52:30 +0530 Subject: [PATCH 165/276] requirements: Unpin pygments from ~=2.15.1 to >=2.14.0,<2.18.0. The previous commit fixes the issue #1431 that was temporarily fixed by pinning pygments at ~=2.15.1 in #1433. Subsequent versions of pygments have since been released, but an upper limit of 2.18.0 is included, since that unreleased version depends on Python3.8 and could make ZT uninstallable on older pythons. Fixes part of #1434. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d509d3e496..39c9ff8410 100644 --- a/setup.py +++ b/setup.py @@ -100,7 +100,7 @@ def long_description(): "urwid_readline>=0.13", "beautifulsoup4>=4.11.1", "lxml~=4.9.2", - "pygments~=2.15.1", + "pygments>=2.14.0,<2.18.0", "typing_extensions~=4.5.0", "python-dateutil>=2.8.2", "pytz>=2022.7.1", From dd89cfc017d3e598afb83cffa4ef82967ba16dd1 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 16 Jan 2024 13:38:05 -0800 Subject: [PATCH 166/276] setup: Restructure dev options to add testing_minimal option. This represents a minimal pytest install that should be sufficient for linting requirements, where pytest symbols are analyzed. For running pytest itself, continue to use the testing option, ie. `pip install .[testing]` vs `pip install .[testing_minimal]` Note that the minimal version includes both pytest itself and pytest-mock, whose objects are used in tests (via `pytest_mock`). --- setup.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 39c9ff8410..afe0d9bb6a 100644 --- a/setup.py +++ b/setup.py @@ -17,12 +17,17 @@ def long_description(): return "\n".join(source.splitlines()[1:]) -testing_deps = [ +testing_minimal_deps = [ "pytest~=7.2.0", - "pytest-cov~=4.0.0", "pytest-mock~=3.10.0", ] +testing_plugin_deps = [ + "pytest-cov~=4.0.0", +] + +testing_deps = testing_minimal_deps + testing_plugin_deps + linting_deps = [ "isort~=5.11.0", "black~=23.0", @@ -90,6 +95,7 @@ def long_description(): extras_require={ "dev": testing_deps + linting_deps + typing_deps + dev_helper_deps, "testing": testing_deps, + "testing_minimal": testing_minimal_deps, "linting": linting_deps, "typing": typing_deps, }, From 8196a09283520e0c7a126a71eff0a6ecccab9e84 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 16 Jan 2024 13:32:39 -0800 Subject: [PATCH 167/276] lint-and-test: Use testing_minimal instead of full pytest install. In one case we were installing the `testing` option fully, which we shouldn't need for linting. In another we were manually installing pytest using pip at its latest version, which could vary subtly from the version we use otherwise. This change ensures the version of pytest we use is consistent and somewhat more minimal when installed in CI. --- .github/workflows/lint-and-test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index a2f9b449c2..96236a517b 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -26,8 +26,8 @@ jobs: python-version: 3.7 cache: 'pip' cache-dependency-path: 'setup.py' - - name: Install with type-checking tools, stubs & test libraries - run: pip install .[typing,testing] + - name: Install with type-checking tools, stubs & minimal test libraries + run: pip install .[typing,testing_minimal] - name: Run mypy run: ./tools/run-mypy @@ -60,9 +60,9 @@ jobs: python-version: 3.7 cache: 'pip' cache-dependency-path: 'setup.py' - - name: Install with linting tools + - name: Install with linting tools & minimal test libraries # NOTE: Install pytest so that isort recognizes it as a known library - run: pip install .[linting] && pip install pytest + run: pip install .[linting,minimal_testing] - name: Run isort run: ./tools/run-isort-check From 87816fb4bf47446fa84fee6912c19de8364d8f46 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 23 Jan 2024 15:39:31 -0800 Subject: [PATCH 168/276] refactor: lint-and-test: Improve job name consistency. --- .github/workflows/lint-and-test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 96236a517b..25f6f73279 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -68,7 +68,7 @@ jobs: black: runs-on: ubuntu-latest - name: Lint - Code formatting (black) + name: Lint - Code format (black) steps: - uses: actions/checkout@v3 with: @@ -85,7 +85,7 @@ jobs: spellcheck: runs-on: ubuntu-latest - name: Lint - Spellcheck + name: Lint - Spellcheck (codespell, typos) steps: - uses: actions/checkout@v3 with: @@ -102,7 +102,7 @@ jobs: hotkeys: runs-on: ubuntu-latest - name: Lint - Hotkeys linting & docs sync check + name: Lint - Hotkeys & docs sync (lint-hotkeys) steps: - uses: actions/checkout@v3 with: @@ -119,7 +119,7 @@ jobs: docstrings: runs-on: ubuntu-latest - name: Lint - Docstrings linting & docs sync check + name: Lint - Docstrings & docs sync (lint-docstring) steps: - uses: actions/checkout@v3 with: From 1c4d2345cc37852704b78db23507bdf793c04a81 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 16 Jan 2024 16:56:57 -0800 Subject: [PATCH 169/276] setup: Extract gitlint into minimal install option for CI. --- setup.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index afe0d9bb6a..1862573293 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,10 @@ def long_description(): "typos~=1.16.11", ] +gitlint_deps = [ + "gitlint~=0.18.0", +] + typing_deps = [ "lxml-stubs", "mypy~=1.3.0", @@ -47,10 +51,9 @@ def long_description(): "types-requests", ] -dev_helper_deps = [ +helper_deps = [ "pudb==2022.1.1", "snakeviz>=2.1.1", - "gitlint~=0.18.0", ] setup( @@ -93,10 +96,11 @@ def long_description(): ], }, extras_require={ - "dev": testing_deps + linting_deps + typing_deps + dev_helper_deps, + "dev": testing_deps + linting_deps + typing_deps + helper_deps + gitlint_deps, "testing": testing_deps, "testing_minimal": testing_minimal_deps, "linting": linting_deps, + "gitlint": gitlint_deps, "typing": typing_deps, }, tests_require=testing_deps, From 0cc2bdeb19fd5067db255154c379fde0db743a77 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 16 Jan 2024 12:54:04 -0800 Subject: [PATCH 170/276] lint-and-test: Check commit style for commits on PR branches. This requires an approach similar to the isolated-commits job, since the checkout action by default only fetches the latest state of the code. This job is also similar in that since it applies to a branch, it is limited to pull requests. This uses the new gitlint install option, to minimize necessary package installs using the current method. --- .github/workflows/lint-and-test.yml | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 25f6f73279..5a06a8d149 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -134,6 +134,35 @@ jobs: - name: Run lint-docstring run: ./tools/lint-docstring + gitlint: + runs-on: ubuntu-latest + name: Lint - Commit text format (gitlint) + steps: + - name: 'PR commits +1' + if: github.event_name == 'pull_request' + run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}" + - name: 'Checkout PR branch and all PR commits' + if: github.event_name == 'pull_request' + uses: actions/checkout@v3 + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: ${{ env.PR_FETCH_DEPTH }} + - uses: actions/setup-python@v4 + if: github.event_name == 'pull_request' + with: + python-version: 3.7 + cache: 'pip' + cache-dependency-path: 'setup.py' + - name: Install with gitlint + if: github.event_name == 'pull_request' + run: pip install .[gitlint] + - name: Run gitlint + if: github.event_name == 'pull_request' + run: + git fetch https://github.com/zulip/zulip-terminal main; + gitlint --commits FETCH_HEAD..${{ github.event.pull_request.head.sha }} + base_pytest: runs-on: ubuntu-latest name: Install & test - CPython 3.7 (ubuntu), codecov @@ -172,6 +201,7 @@ jobs: - hotkeys - docstrings - base_pytest + - gitlint steps: - uses: actions/checkout@v3 if: github.event_name == 'pull_request' From 21b589099ecb209bda361d2c99e827bafaa22382 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 23 Jan 2024 15:09:37 -0800 Subject: [PATCH 171/276] README: Explicitly add gitlint to development environment setup. This is still mentioned later for now, but should ensure that more users receive feedback on their commit messages via gitlint. Given that CI now requires gitlint to pass, this should ensure that they are aware that problems exist earlier, when writing their commit messages. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 5763df618c..9fd63f4e49 100644 --- a/README.md +++ b/README.md @@ -460,6 +460,12 @@ $ pipenv install --dev $ pipenv run pip3 install -e '.[dev]' ``` +4. Connect the gitlint commit-message hook + +``` +$ pipenv run gitlint install-hook +``` + #### Pip 1. Manually create & activate a virtual environment; any method should work, @@ -473,12 +479,18 @@ $ pipenv run pip3 install -e '.[dev]' $ pip3 install -e '.[dev]' ``` +3. Connect the gitlint commit-message hook +``` +$ gitlint install-hook +``` + #### make/pip This is the newest and simplest approach, if you have `make` installed: 1. `make` (sets up an installed virtual environment in `zt_venv` in the current directory) 2. `source zt_venv/bin/activate` (activates the venv; this assumes a bash-like shell) +3. `gitlint install-hook` (connect the gitlint commit-message hook) ### Development tasks From ee7564c9651f2c8cc6514a72e020827cfecbf938 Mon Sep 17 00:00:00 2001 From: rsashank Date: Fri, 22 Dec 2023 21:36:56 +0530 Subject: [PATCH 172/276] bugfix: views: Avoid crash starting stream compose from empty narrow. Avoids previously encountered crash triggered by attempting to compose to a stream using the hotkey, when stream_id is None. Fixes #1453. --- zulipterminal/ui_tools/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 1d63b5eb92..a5a0221f41 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -591,7 +591,10 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: if self.footer.focus is None: stream_id = self.model.stream_id stream_dict = self.model.stream_dict - self.footer.stream_box_view(caption=stream_dict[stream_id]["name"]) + if stream_id is None: + self.footer.stream_box_view(0) + else: + self.footer.stream_box_view(caption=stream_dict[stream_id]["name"]) self.set_focus("footer") self.footer.focus_position = 0 return key From 158fdf4aec7e5736378d4f557e12eb7cdb0bfb7e Mon Sep 17 00:00:00 2001 From: Sagnik Mandal Date: Sat, 3 Feb 2024 00:25:09 +0530 Subject: [PATCH 173/276] refactor: boxes/api_types: Use milliseconds for typing notifications. From ZFL 204, the server provides typing notification parameters in milliseconds; this change ensures that the default values are also in milliseconds (instead of seconds). --- zulipterminal/api_types.py | 8 ++++---- zulipterminal/ui_tools/boxes.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 8acf4d05e2..5c1dd9a7f8 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -50,10 +50,10 @@ # # NOTE: `to` field could be email until ZFL 11/3.0; ids were possible from 2.0+ -# Timing parameters for when notifications should occur -TYPING_STARTED_WAIT_PERIOD: Final = 10 -TYPING_STOPPED_WAIT_PERIOD: Final = 5 -TYPING_STARTED_EXPIRY_PERIOD: Final = 15 # TODO: Needs implementation in ZT +# Timing parameters for when notifications should occur (in milliseconds) +TYPING_STARTED_WAIT_PERIOD: Final = 10000 +TYPING_STOPPED_WAIT_PERIOD: Final = 5000 +TYPING_STARTED_EXPIRY_PERIOD: Final = 15000 # TODO: Needs implementation in ZT TypingStatusChange = Literal["start", "stop"] diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index db5f396715..aa6c6df9b7 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -237,8 +237,8 @@ def private_box_view( ] self.focus_position = self.FOCUS_CONTAINER_MESSAGE - start_period_delta = timedelta(seconds=TYPING_STARTED_WAIT_PERIOD) - stop_period_delta = timedelta(seconds=TYPING_STOPPED_WAIT_PERIOD) + start_period_delta = timedelta(milliseconds=TYPING_STARTED_WAIT_PERIOD) + stop_period_delta = timedelta(milliseconds=TYPING_STOPPED_WAIT_PERIOD) def on_type_send_status(edit: object, new_edit_text: str) -> None: if new_edit_text and self.typing_recipient_user_ids: From d6b20d22191a1d47dfcd4c498b4905861d225c75 Mon Sep 17 00:00:00 2001 From: Sagnik Mandal Date: Sat, 3 Feb 2024 00:49:40 +0530 Subject: [PATCH 174/276] refactor: boxes/model: Store typing notification durations in model. Stores the default typing notification parameters in the model. Code which previously accessed these values directly now queries the model instead. A test has been added to verify the new model values. --- tests/model/test_model.py | 12 ++++++++++++ tests/ui_tools/test_boxes.py | 8 ++++++++ zulipterminal/model.py | 12 ++++++++++++ zulipterminal/ui_tools/boxes.py | 16 +++++++--------- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 915621f8fc..61bdb09c43 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -14,6 +14,9 @@ MAX_MESSAGE_LENGTH, MAX_STREAM_NAME_LENGTH, MAX_TOPIC_NAME_LENGTH, + TYPING_STARTED_EXPIRY_PERIOD, + TYPING_STARTED_WAIT_PERIOD, + TYPING_STOPPED_WAIT_PERIOD, Model, ServerConnectionFailure, UserSettings, @@ -1391,6 +1394,15 @@ def test__store_content_length_restrictions( assert model.max_topic_length == MAX_TOPIC_NAME_LENGTH assert model.max_message_length == MAX_MESSAGE_LENGTH + def test__store_typing_duration_settings__default_values(self, model, initial_data): + model.initial_data = initial_data + + model._store_typing_duration_settings() + + assert model.typing_started_wait_period == TYPING_STARTED_WAIT_PERIOD + assert model.typing_stopped_wait_period == TYPING_STOPPED_WAIT_PERIOD + assert model.typing_started_expiry_period == TYPING_STARTED_EXPIRY_PERIOD + def test_get_message_false_first_anchor( self, mocker, diff --git a/tests/ui_tools/test_boxes.py b/tests/ui_tools/test_boxes.py index 869cc981b9..dbdd0829e7 100644 --- a/tests/ui_tools/test_boxes.py +++ b/tests/ui_tools/test_boxes.py @@ -7,6 +7,11 @@ from pytest_mock import MockerFixture from urwid import Widget +from zulipterminal.api_types import ( + TYPING_STARTED_EXPIRY_PERIOD, + TYPING_STARTED_WAIT_PERIOD, + TYPING_STOPPED_WAIT_PERIOD, +) from zulipterminal.config.keys import keys_for_command, primary_key_for_command from zulipterminal.config.symbols import ( INVALID_MARKER, @@ -50,6 +55,9 @@ def write_box( write_box.model.max_stream_name_length = 60 write_box.model.max_topic_length = 60 write_box.model.max_message_length = 10000 + write_box.model.typing_started_wait_period = TYPING_STARTED_WAIT_PERIOD + write_box.model.typing_stopped_wait_period = TYPING_STOPPED_WAIT_PERIOD + write_box.model.typing_started_expiry_period = TYPING_STARTED_EXPIRY_PERIOD write_box.model.user_group_names = [ groups["name"] for groups in user_groups_fixture ] diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 9c6fce61b5..08f3ae0e46 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -34,6 +34,9 @@ MAX_MESSAGE_LENGTH, MAX_STREAM_NAME_LENGTH, MAX_TOPIC_NAME_LENGTH, + TYPING_STARTED_EXPIRY_PERIOD, + TYPING_STARTED_WAIT_PERIOD, + TYPING_STOPPED_WAIT_PERIOD, Composition, CustomFieldValue, DirectTypingNotification, @@ -214,6 +217,7 @@ def __init__(self, controller: Any) -> None: self._draft: Optional[Composition] = None self._store_content_length_restrictions() + self._store_typing_duration_settings() self.active_emoji_data, self.all_emoji_names = self.generate_all_emoji_data( self.initial_data["realm_emoji"] @@ -791,6 +795,14 @@ def _store_content_length_restrictions(self) -> None: "max_message_length", MAX_MESSAGE_LENGTH ) + def _store_typing_duration_settings(self) -> None: + """ + Store typing duration fields in model + """ + self.typing_started_wait_period = TYPING_STARTED_WAIT_PERIOD + self.typing_stopped_wait_period = TYPING_STOPPED_WAIT_PERIOD + self.typing_started_expiry_period = TYPING_STARTED_EXPIRY_PERIOD + @staticmethod def modernize_message_response(message: Message) -> Message: """ diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index aa6c6df9b7..6867d5a0e8 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -13,13 +13,7 @@ from typing_extensions import Literal from urwid_readline import ReadlineEdit -from zulipterminal.api_types import ( - TYPING_STARTED_WAIT_PERIOD, - TYPING_STOPPED_WAIT_PERIOD, - Composition, - PrivateComposition, - StreamComposition, -) +from zulipterminal.api_types import Composition, PrivateComposition, StreamComposition from zulipterminal.config.keys import ( is_command_key, keys_for_command, @@ -237,8 +231,12 @@ def private_box_view( ] self.focus_position = self.FOCUS_CONTAINER_MESSAGE - start_period_delta = timedelta(milliseconds=TYPING_STARTED_WAIT_PERIOD) - stop_period_delta = timedelta(milliseconds=TYPING_STOPPED_WAIT_PERIOD) + start_period_delta = timedelta( + milliseconds=self.model.typing_started_wait_period + ) + stop_period_delta = timedelta( + milliseconds=self.model.typing_stopped_wait_period + ) def on_type_send_status(edit: object, new_edit_text: str) -> None: if new_edit_text and self.typing_recipient_user_ids: From eeffc8ce7341d216c51d74eb4338312bca2233a3 Mon Sep 17 00:00:00 2001 From: Sagnik Mandal Date: Sat, 3 Feb 2024 01:12:51 +0530 Subject: [PATCH 175/276] api_types/model: Use server provided typing notification durations. From ZFL 204, the typing notification durations were made server-configurable; this change uses those values when provided, or else uses the default values. A test has been added to check this behaviour. Fixes #1445. --- tests/model/test_model.py | 30 ++++++++++++++++++++++++++++++ zulipterminal/api_types.py | 2 ++ zulipterminal/model.py | 19 +++++++++++++++---- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 61bdb09c43..51c71eb753 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -1403,6 +1403,36 @@ def test__store_typing_duration_settings__default_values(self, model, initial_da assert model.typing_stopped_wait_period == TYPING_STOPPED_WAIT_PERIOD assert model.typing_started_expiry_period == TYPING_STARTED_EXPIRY_PERIOD + def test__store_typing_duration_settings__with_values( + self, + model, + initial_data, + feature_level=204, + typing_started_wait=7500, + typing_stopped_wait=3000, + typing_started_expiry=10000, + ): + # Ensure inputs are not the defaults, to avoid the test accidentally passing + assert typing_started_wait != TYPING_STARTED_WAIT_PERIOD + assert typing_stopped_wait != TYPING_STOPPED_WAIT_PERIOD + assert typing_started_expiry != TYPING_STARTED_EXPIRY_PERIOD + + to_vary_in_initial_data = { + "server_typing_started_wait_period_milliseconds": typing_started_wait, + "server_typing_stopped_wait_period_milliseconds": typing_stopped_wait, + "server_typing_started_expiry_period_milliseconds": typing_started_expiry, + } + + initial_data.update(to_vary_in_initial_data) + model.initial_data = initial_data + model.server_feature_level = feature_level + + model._store_typing_duration_settings() + + assert model.typing_started_wait_period == typing_started_wait + assert model.typing_stopped_wait_period == typing_stopped_wait + assert model.typing_started_expiry_period == typing_started_expiry + def test_get_message_false_first_anchor( self, mocker, diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 5c1dd9a7f8..9bf3b5b54a 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -50,6 +50,8 @@ # # NOTE: `to` field could be email until ZFL 11/3.0; ids were possible from 2.0+ +# In ZFL 204, these values were made server-configurable +# Before this feature level, these values were fixed as follows: # Timing parameters for when notifications should occur (in milliseconds) TYPING_STARTED_WAIT_PERIOD: Final = 10000 TYPING_STOPPED_WAIT_PERIOD: Final = 5000 diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 08f3ae0e46..22988c609b 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -797,11 +797,22 @@ def _store_content_length_restrictions(self) -> None: def _store_typing_duration_settings(self) -> None: """ - Store typing duration fields in model + Store typing duration fields in model. + In ZFL 204, these values were made server-configurable. + Uses default values if not received from server. """ - self.typing_started_wait_period = TYPING_STARTED_WAIT_PERIOD - self.typing_stopped_wait_period = TYPING_STOPPED_WAIT_PERIOD - self.typing_started_expiry_period = TYPING_STARTED_EXPIRY_PERIOD + self.typing_started_wait_period = self.initial_data.get( + "server_typing_started_wait_period_milliseconds", + TYPING_STARTED_WAIT_PERIOD, + ) + self.typing_stopped_wait_period = self.initial_data.get( + "server_typing_stopped_wait_period_milliseconds", + TYPING_STOPPED_WAIT_PERIOD, + ) + self.typing_started_expiry_period = self.initial_data.get( + "server_typing_started_expiry_period_milliseconds", + TYPING_STARTED_EXPIRY_PERIOD, + ) @staticmethod def modernize_message_response(message: Message) -> Message: From fe1689c9d962c26c5f5b6f1151a0a6a1985cb82c Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 5 Feb 2024 15:22:31 -0800 Subject: [PATCH 176/276] lint-and-test/codeql-analysis: Upgrade setup-python Action to v5. --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/lint-and-test.yml | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ce6d89f2f0..6db9ca8fdf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,7 +25,7 @@ jobs: with: persist-credentials: false - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.9' - name: Install dependencies @@ -44,7 +44,7 @@ jobs: # Override the default behavior so that the action doesn't attempt # to auto-install Python dependencies setup-python-dependencies: false - + # Override language selection by uncommenting this and choosing your languages # with: # languages: go, javascript, csharp, python, cpp, java diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 5a06a8d149..e676ce3bb7 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v3 with: persist-credentials: false - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.7 cache: 'pip' @@ -38,7 +38,7 @@ jobs: - uses: actions/checkout@v3 with: persist-credentials: false - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.7 cache: 'pip' @@ -55,7 +55,7 @@ jobs: - uses: actions/checkout@v3 with: persist-credentials: false - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.7 cache: 'pip' @@ -73,7 +73,7 @@ jobs: - uses: actions/checkout@v3 with: persist-credentials: false - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.7 cache: 'pip' @@ -90,7 +90,7 @@ jobs: - uses: actions/checkout@v3 with: persist-credentials: false - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.7 cache: 'pip' @@ -107,7 +107,7 @@ jobs: - uses: actions/checkout@v3 with: persist-credentials: false - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.7 cache: 'pip' @@ -124,7 +124,7 @@ jobs: - uses: actions/checkout@v3 with: persist-credentials: false - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.7 cache: 'pip' @@ -148,7 +148,7 @@ jobs: persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} fetch-depth: ${{ env.PR_FETCH_DEPTH }} - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 if: github.event_name == 'pull_request' with: python-version: 3.7 @@ -171,7 +171,7 @@ jobs: with: persist-credentials: false - name: Install Python version - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.7 cache: 'pip' @@ -209,7 +209,7 @@ jobs: persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 if: github.event_name == 'pull_request' with: python-version: 3.7 @@ -247,7 +247,7 @@ jobs: with: persist-credentials: false - name: Install Python version ${{ matrix.env.PYTHON }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.env.PYTHON }} cache: 'pip' From c751938c52296b64c10e40949fac2692dd566156 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 5 Feb 2024 15:49:06 -0800 Subject: [PATCH 177/276] lint-and-test/codeql-analysis: Upgrade checkout Action to v4. --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/lint-and-test.yml | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 6db9ca8fdf..9c87d921a3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false - name: Set up Python diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index e676ce3bb7..d3f76cc456 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest name: Lint - Type consistency (mypy) steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest name: Lint - PEP8 & more (ruff) steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 @@ -52,7 +52,7 @@ jobs: runs-on: ubuntu-latest name: Lint - Import order (isort) steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest name: Lint - Code format (black) steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 @@ -87,7 +87,7 @@ jobs: runs-on: ubuntu-latest name: Lint - Spellcheck (codespell, typos) steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 @@ -104,7 +104,7 @@ jobs: runs-on: ubuntu-latest name: Lint - Hotkeys & docs sync (lint-hotkeys) steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 @@ -121,7 +121,7 @@ jobs: runs-on: ubuntu-latest name: Lint - Docstrings & docs sync (lint-docstring) steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - uses: actions/setup-python@v5 @@ -143,7 +143,7 @@ jobs: run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}" - name: 'Checkout PR branch and all PR commits' if: github.event_name == 'pull_request' - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} @@ -167,7 +167,7 @@ jobs: runs-on: ubuntu-latest name: Install & test - CPython 3.7 (ubuntu), codecov steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - name: Install Python version @@ -203,7 +203,7 @@ jobs: - base_pytest - gitlint steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 if: github.event_name == 'pull_request' with: persist-credentials: false @@ -243,7 +243,7 @@ jobs: runs-on: ${{ matrix.env.OS }} name: Install & test - ${{ matrix.env.NAME}} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: persist-credentials: false - name: Install Python version ${{ matrix.env.PYTHON }} From bf5192b139a22863c8265208e4d9b0f0282d58ea Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 5 Feb 2024 17:08:41 -0800 Subject: [PATCH 178/276] lint-and-test/codeql-analysis: Limit GitHub token workflow permissions. --- .github/workflows/codeql-analysis.yml | 7 +++++++ .github/workflows/lint-and-test.yml | 3 +++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9c87d921a3..7f33189d53 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,9 +14,16 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + contents: read + jobs: analyse: name: Analyse + permissions: + actions: read + contents: read + security-events: write runs-on: ubuntu-latest steps: diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index d3f76cc456..e6c80fb0c2 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -13,6 +13,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + contents: read + jobs: mypy: runs-on: ubuntu-latest From 8b6cdf80192076decb129ff9f9ad6f4fde0d7969 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 5 Feb 2024 16:21:00 -0800 Subject: [PATCH 179/276] codeql-analysis: Upgrade CodeQL action for upcoming deprecation of v2. The need for CODEQL_PYTHON is deprecated in this version, so remove it. --- .github/workflows/codeql-analysis.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7f33189d53..275b4c7979 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,13 +39,9 @@ jobs: run: | python -m pip install --upgrade pip pip3 install . - - # Set the `CODEQL-PYTHON` environment variable to the Python executable - # that includes the dependencies - echo "CODEQL_PYTHON=$(which python)" >> $GITHUB_ENV # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: python # Override the default behavior so that the action doesn't attempt @@ -56,4 +52,4 @@ jobs: # with: # languages: go, javascript, csharp, python, cpp, java - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 7c675e8b8361742a5a23b3f9b63743fec2393083 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 5 Feb 2024 18:13:46 -0800 Subject: [PATCH 180/276] codeql-analysis: Ignore CodeQL analysis in private repositories. This is not a frequent problem, but causes CI to fail since CodeQL only runs in public repositories. --- .github/workflows/codeql-analysis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 275b4c7979..44c3d17276 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,6 +20,7 @@ permissions: jobs: analyse: name: Analyse + if: ${{!github.event.repository.private}} permissions: actions: read contents: read From 1518c77b6d4c8cb6909812038d4311fd7f492589 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Mon, 5 Feb 2024 18:59:14 -0800 Subject: [PATCH 181/276] lint-and-test: Add support for testing PyPy 3.10. --- .github/workflows/lint-and-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index e6c80fb0c2..3ab1761508 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -239,6 +239,7 @@ jobs: - {PYTHON: 'pypy-3.7', OS: ubuntu-latest, NAME: "PyPy 3.7 (ubuntu)", EXPECT: "Linux"} - {PYTHON: 'pypy-3.8', OS: ubuntu-latest, NAME: "PyPy 3.8 (ubuntu)", EXPECT: "Linux"} - {PYTHON: 'pypy-3.9', OS: ubuntu-latest, NAME: "PyPy 3.9 (ubuntu)", EXPECT: "Linux"} + - {PYTHON: 'pypy-3.10', OS: ubuntu-latest, NAME: "PyPy 3.10 (ubuntu)", EXPECT: "Linux"} - {PYTHON: '3.11', OS: macos-latest, NAME: "CPython 3.11 (macos)", EXPECT: "MacOS"} env: EXPECT: ${{ matrix.env.EXPECT }} From f70a4a4a3c86d2f27db2dbe98abe8b8a6e5bc833 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 25 Jan 2024 19:37:08 -0800 Subject: [PATCH 182/276] setup: Hyphenate testing-minimal extra instead of using underscore. This appeared to work previously when used in CI, but installation gives a WARNING regarding not finding the extra. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1862573293..43321c63f9 100644 --- a/setup.py +++ b/setup.py @@ -98,7 +98,7 @@ def long_description(): extras_require={ "dev": testing_deps + linting_deps + typing_deps + helper_deps + gitlint_deps, "testing": testing_deps, - "testing_minimal": testing_minimal_deps, + "testing-minimal": testing_minimal_deps, # extra must be hyphenated "linting": linting_deps, "gitlint": gitlint_deps, "typing": typing_deps, From a158558eca105b5c778bc0bf18a0bb5d20671329 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 25 Jan 2024 16:07:59 -0800 Subject: [PATCH 183/276] refactor: run: Use distinct variable names to satisfy mypy. --- zulipterminal/cli/run.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py index a3ccd9d18e..46d13e8992 100755 --- a/zulipterminal/cli/run.py +++ b/zulipterminal/cli/run.py @@ -513,11 +513,11 @@ def main(options: Optional[List[str]] = None) -> None: ) # Validate remaining settings - for setting, valid_values in valid_remaining_settings.items(): - if zterm[setting].value not in valid_values: + for setting, valid_remaining_values in valid_remaining_settings.items(): + if zterm[setting].value not in valid_remaining_values: helper_text = ( ["Valid values are:"] - + [f" {option}" for option in valid_values] + + [f" {option}" for option in valid_remaining_values] + [f"Specify the {setting} option in zuliprc file."] ) exit_with_error( @@ -563,8 +563,8 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None: # Translate valid strings for boolean values into True/False boolean_settings: Dict[str, bool] = dict() - for setting, valid_values in VALID_BOOLEAN_SETTINGS.items(): - boolean_settings[setting] = zterm[setting].value == valid_values[0] + for setting, valid_boolean_values in VALID_BOOLEAN_SETTINGS.items(): + boolean_settings[setting] = zterm[setting].value == valid_boolean_values[0] Controller( config_file=zuliprc_path, From bd27f1660923e474e06de6ea31ee4ca9f5c0f01c Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 25 Jan 2024 16:09:44 -0800 Subject: [PATCH 184/276] requirements[dev]: Upgrade mypy from ~=1.3.0 to ~=1.4.0. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 43321c63f9..c728bed023 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def long_description(): typing_deps = [ "lxml-stubs", - "mypy~=1.3.0", + "mypy~=1.4.0", "types-beautifulsoup4", "types-pygments", "types-python-dateutil", From 4145d14ed0ec7138bb2834d017170054184e66d4 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 25 Jan 2024 23:29:02 -0800 Subject: [PATCH 185/276] refactor: lint-and-test: Extract variable for python linting version. --- .github/workflows/lint-and-test.yml | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 3ab1761508..452ecf9f80 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -16,6 +16,9 @@ concurrency: permissions: contents: read +env: + LINTING_PYTHON_VERSION: 3.7 + jobs: mypy: runs-on: ubuntu-latest @@ -26,7 +29,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: ${{ env.LINTING_PYTHON_VERSION }} cache: 'pip' cache-dependency-path: 'setup.py' - name: Install with type-checking tools, stubs & minimal test libraries @@ -43,7 +46,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: ${{ env.LINTING_PYTHON_VERSION }} cache: 'pip' cache-dependency-path: 'setup.py' - name: Install with linting tools @@ -60,7 +63,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: ${{ env.LINTING_PYTHON_VERSION }} cache: 'pip' cache-dependency-path: 'setup.py' - name: Install with linting tools & minimal test libraries @@ -78,7 +81,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: ${{ env.LINTING_PYTHON_VERSION }} cache: 'pip' cache-dependency-path: 'setup.py' - name: Install with linting tools @@ -95,7 +98,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: ${{ env.LINTING_PYTHON_VERSION }} cache: 'pip' cache-dependency-path: 'setup.py' - name: Install with linting tools @@ -112,7 +115,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: ${{ env.LINTING_PYTHON_VERSION }} cache: 'pip' cache-dependency-path: 'setup.py' - name: Minimal install @@ -129,7 +132,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: ${{ env.LINTING_PYTHON_VERSION }} cache: 'pip' cache-dependency-path: 'setup.py' - name: Minimal install @@ -154,7 +157,7 @@ jobs: - uses: actions/setup-python@v5 if: github.event_name == 'pull_request' with: - python-version: 3.7 + python-version: ${{ env.LINTING_PYTHON_VERSION }} cache: 'pip' cache-dependency-path: 'setup.py' - name: Install with gitlint @@ -215,7 +218,7 @@ jobs: - uses: actions/setup-python@v5 if: github.event_name == 'pull_request' with: - python-version: 3.7 + python-version: ${{ env.LINTING_PYTHON_VERSION }} cache: 'pip' cache-dependency-path: 'setup.py' - name: Run check-branch From d50660b49b883620e8607101aa374edfea810d63 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 6 Feb 2024 17:53:04 -0800 Subject: [PATCH 186/276] lint-and-test: Use Python 3.8 for linting in CI. Python 3.7 is now end of life. Core libraries will increasingly drop support, but we can certainly push development requirements more rapidly, and newer mypy already requires 3.8. --- .github/workflows/lint-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml index 452ecf9f80..d4799c4def 100644 --- a/.github/workflows/lint-and-test.yml +++ b/.github/workflows/lint-and-test.yml @@ -17,7 +17,7 @@ permissions: contents: read env: - LINTING_PYTHON_VERSION: 3.7 + LINTING_PYTHON_VERSION: 3.8 jobs: mypy: From 5f6ebfeb9c8b3d16923967bbd02b06e8ed014f74 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 25 Jan 2024 16:36:38 -0800 Subject: [PATCH 187/276] requirements[dev]: Upgrade mypy from ~=1.4.0 to ~=1.5.0. The strict-concatenate option is now deprecated, so replaced by extra-checks, in pyproject.toml. --- pyproject.toml | 3 ++- setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a2920b838d..f01695735c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,8 @@ warn_unused_ignores = true warn_return_any = false no_implicit_reexport = true # NOTE: Disabled explicitly for tests/ in run-mypy strict_equality = true -strict_concatenate = true + +extra_checks = true enable_error_code = [ "redundant-expr", diff --git a/setup.py b/setup.py index c728bed023..a3c7d1a73d 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def long_description(): typing_deps = [ "lxml-stubs", - "mypy~=1.4.0", + "mypy~=1.5.0", "types-beautifulsoup4", "types-pygments", "types-python-dateutil", From 81b49b8f8b8baf4a981a1cc12a2627a2b2d5f7a3 Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 6 Feb 2024 23:39:54 -0800 Subject: [PATCH 188/276] requirements[dev]: Upgrade mypy from ~=1.5.0 to ~=1.6.0. This requires an update of a type ignore in convert-unicode-emoji-data, which is necessary due to the imported file being conditionally present. CI and local Python 3.9 require different type ignore specifiers, so this also adds 'unused-ignore', to avoid errors when the other type ignore is apparently not used on the other platform. --- setup.py | 2 +- tools/convert-unicode-emoji-data | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index a3c7d1a73d..918789e781 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def long_description(): typing_deps = [ "lxml-stubs", - "mypy~=1.5.0", + "mypy~=1.6.0", "types-beautifulsoup4", "types-pygments", "types-python-dateutil", diff --git a/tools/convert-unicode-emoji-data b/tools/convert-unicode-emoji-data index 74acc1e685..2e7b307520 100755 --- a/tools/convert-unicode-emoji-data +++ b/tools/convert-unicode-emoji-data @@ -8,7 +8,7 @@ from pathlib import Path, PurePath try: # Ignored for type-checking, as it is a temporary file, deleted at the end of file - from zulipterminal.unicode_emoji_dict import ( # type: ignore [import] + from zulipterminal.unicode_emoji_dict import ( # type: ignore [import-not-found,import-untyped,unused-ignore] EMOJI_NAME_MAPS, ) except ModuleNotFoundError: From da84485cfc011c13e4177cc56442eeb04d83302d Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 6 Feb 2024 23:55:22 -0800 Subject: [PATCH 189/276] requirements[dev]: Upgrade mypy from ~=1.6.0 to ~=1.8.0. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 918789e781..168f33dd33 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def long_description(): typing_deps = [ "lxml-stubs", - "mypy~=1.6.0", + "mypy~=1.8.0", "types-beautifulsoup4", "types-pygments", "types-python-dateutil", From e463d6cacc3d913b94e356904e9a8c5ab96c478c Mon Sep 17 00:00:00 2001 From: LY <51789698+Young-Lord@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:06:53 +0800 Subject: [PATCH 190/276] bugfix: messages: Fix crash when editing message. When `realm_message_content_edit_limit_seconds` is None, which now means no time limit for editing messages, ZT crashes with a `TypeError`. This was diagnosed in Zulip cloud, but may be triggered for any version of the server from feature level 138, which was released first in Zulip server 6.0. Prior to this feature level, no limit on editing messages was represented by a numerical value of zero; a direct numerical comparison was therefore possible, but now causes an error. Fixes #1467. --- zulipterminal/ui_tools/messages.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zulipterminal/ui_tools/messages.py b/zulipterminal/ui_tools/messages.py index 1a9bb9db02..3ddfa10a70 100644 --- a/zulipterminal/ui_tools/messages.py +++ b/zulipterminal/ui_tools/messages.py @@ -1045,12 +1045,12 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: # the time limit. A limit of 0 signifies no limit # on message body editing. msg_body_edit_enabled = True - if self.model.initial_data["realm_message_content_edit_limit_seconds"] > 0: + edit_time_limit = self.model.initial_data[ + "realm_message_content_edit_limit_seconds" + ] + if edit_time_limit is not None and edit_time_limit > 0: if self.message["sender_id"] == self.model.user_id: time_since_msg_sent = time() - self.message["timestamp"] - edit_time_limit = self.model.initial_data[ - "realm_message_content_edit_limit_seconds" - ] # Don't allow editing message body if time-limit exceeded. if time_since_msg_sent >= edit_time_limit: if self.message["type"] == "private": From ba5e27a320b3ef383531e999f758913270e2f98f Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Thu, 8 Feb 2024 18:17:18 -0800 Subject: [PATCH 191/276] tests: messages: Add test cases for crash bugfix from ZFL 138. Confirmed as failing, if prior to the contributed bugfix in the previous commit. --- tests/ui_tools/test_messages.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/ui_tools/test_messages.py b/tests/ui_tools/test_messages.py index d4d9e806b4..8deb4ebf3c 100644 --- a/tests/ui_tools/test_messages.py +++ b/tests/ui_tools/test_messages.py @@ -1301,7 +1301,16 @@ def test_keypress_STREAM_MESSAGE( {"stream": True, "private": True}, {"stream": True, "private": True}, {"stream": None, "private": None}, - id="no_msg_body_edit_limit", + id="no_msg_body_edit_limit:ZFL<138", + ), + case( + {"sender_id": 1, "timestamp": 1, "subject": "test"}, + True, + None, + {"stream": True, "private": True}, + {"stream": True, "private": True}, + {"stream": None, "private": None}, + id="no_msg_body_edit_limit:ZFL>=138", ), case( {"sender_id": 1, "timestamp": 1, "subject": "(no topic)"}, @@ -1352,7 +1361,16 @@ def test_keypress_STREAM_MESSAGE( {"stream": True, "private": True}, {"stream": True, "private": True}, {"stream": None, "private": None}, - id="no_msg_body_edit_limit_with_no_topic", + id="no_msg_body_edit_limit_with_no_topic:ZFL<138", + ), + case( + {"sender_id": 1, "timestamp": 45, "subject": "(no topic)"}, + True, + None, + {"stream": True, "private": True}, + {"stream": True, "private": True}, + {"stream": None, "private": None}, + id="no_msg_body_edit_limit_with_no_topic:ZFL>=138", ), ], ) From b71aec23f78c487506e86bbd5a89b90c5b3d61ec Mon Sep 17 00:00:00 2001 From: rsashank Date: Wed, 10 Jan 2024 11:40:53 +0530 Subject: [PATCH 192/276] refactor: model/api_types/views/version: Simplify feature-level typing. When the server originally did not provide `zulip_feature_level`, this was previously represented by a `None` value in `model.server_feature_level`. This replaces the current potential `None` value with a zero value, allowing a pure integer representation in the model. This enables simplification of conditional statements when handling different server versions. The feature-level is represented by various strings in the repository, including: - server_feature_level - zulip_feature_level - ZFL - feature_level Tests updated, including test ids. Additional test case added for realm retention. Fixes #1364. Co-authored-by: Lucas.C.B --- tests/conftest.py | 4 ++-- tests/model/test_model.py | 41 +++++++++++++++++++-------------- tests/ui_tools/test_popups.py | 12 +++++----- zulipterminal/api_types.py | 1 - zulipterminal/model.py | 14 +++++------ zulipterminal/ui_tools/views.py | 2 +- zulipterminal/version.py | 2 +- 7 files changed, 41 insertions(+), 35 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index fe771d095c..ad92c4cc71 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -559,7 +559,7 @@ def messages_successful_response( params=SUPPORTED_SERVER_VERSIONS, ids=(lambda param: "server_version:{}-server_feature_level:{}".format(*param)), ) -def zulip_version(request: Any) -> Tuple[str, Optional[int]]: +def zulip_version(request: Any) -> Tuple[str, int]: """ Fixture to test different components based on the server version and the feature level. @@ -1449,7 +1449,7 @@ def stream_dict(streams_fixture: List[Dict[str, Any]]) -> Dict[int, Any]: }, ], ids=[ - "zulip_feature_level:None", + "zulip_feature_level:0", "zulip_feature_level:1", ], ) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 51c71eb753..83ce9de168 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -81,7 +81,7 @@ def test_init( ) assert model.initial_data == initial_data assert model.server_version == initial_data["zulip_version"] - assert model.server_feature_level == initial_data.get("zulip_feature_level") + assert model.server_feature_level == initial_data.get("zulip_feature_level", 0) assert model.user_id == user_profile["user_id"] assert model.user_full_name == user_profile["full_name"] assert model.user_email == user_profile["email"] @@ -157,7 +157,7 @@ def test_user_settings_expected_contents(self, model): ( [["Stream 1", "muted stream muted topic"]], {("Stream 1", "muted stream muted topic"): None}, - None, + 0, ), ( [["Stream 2", "muted topic", 1530129122]], @@ -166,7 +166,7 @@ def test_user_settings_expected_contents(self, model): ), ], ids=[ - "zulip_feature_level:None", + "zulip_feature_level:0", "zulip_feature_level:1", ], ) @@ -276,12 +276,19 @@ def test_register_initial_desired_events(self, mocker, initial_data): "expect_msg_retention_text", ], [ + case( + {1: {}}, + None, + 0, + {1: "Indefinite [Organization default]"}, + id="ZFL=0_no_stream_retention_realm_retention=None", + ), case( {1: {}}, None, 10, {1: "Indefinite [Organization default]"}, - id="ZFL=None_no_stream_retention_realm_retention=None", + id="ZFL=10_no_stream_retention_realm_retention=None", ), case( {1: {}, 2: {}}, @@ -973,7 +980,7 @@ def test_update_stream_message( @pytest.mark.parametrize( "ZFL, expect_API_notify_args", [ - (None, False), + (0, False), (8, False), (9, True), (152, True), @@ -1351,7 +1358,7 @@ def test_modernize_message_response( @pytest.mark.parametrize( "feature_level, to_vary_in_initial_data", [ - (None, {}), + (0, {}), (27, {}), (52, {}), ( @@ -1364,7 +1371,7 @@ def test_modernize_message_response( ), ], ids=[ - "Zulip_2.1.x_ZFL_None_no_restrictions", + "Zulip_2.1.x_ZFL_0_no_restrictions", "Zulip_3.1.x_ZFL_27_no_restrictions", "Zulip_4.0.x_ZFL_52_no_restrictions", "Zulip_4.0.x_ZFL_53_with_restrictions", @@ -2777,7 +2784,7 @@ def test__handle_reaction_event_for_msg_in_index( params=[ ("op", 32), # At server feature level 32, event uses standard field ("operation", 31), - ("operation", None), + ("operation", 0), ] ) def update_message_flags_operation(self, request): @@ -3297,11 +3304,11 @@ def test__handle_typing_event( ), ], ids=[ - "remove_18_in_home_view:already_unmuted:ZFLNone", - "remove_19_in_home_view:muted:ZFLNone", - "add_19_in_home_view:already_muted:ZFLNone", - "add_30_in_home_view:unmuted:ZFLNone", - "remove_30_is_muted:already_unmutedZFL139", + "remove_18_in_home_view:already_unmuted:ZFL0", + "remove_19_in_home_view:muted:ZFL0", + "add_19_in_home_view:already_muted:ZFL0", + "add_30_in_home_view:unmuted:ZFL0", + "remove_30_is_muted:already_unmuted:ZFL139", "remove_19_is_muted:muted:ZFL139", "add_15_is_muted:already_muted:ZFL139", "add_30_is_muted:unmuted:ZFL139", @@ -3469,7 +3476,7 @@ def test__handle_subscription_event_visual_notifications( [ ( {"op": "peer_add", "stream_id": 99, "user_id": 12}, - None, + 0, 99, [1001, 11, 12], ), @@ -3491,7 +3498,7 @@ def test__handle_subscription_event_visual_notifications( 99, [1001, 11, 12], ), - ({"op": "peer_remove", "stream_id": 2, "user_id": 12}, None, 2, [1001, 11]), + ({"op": "peer_remove", "stream_id": 2, "user_id": 12}, 0, 2, [1001, 11]), ({"op": "peer_remove", "stream_id": 2, "user_id": 12}, 34, 2, [1001, 11]), ( {"op": "peer_remove", "stream_ids": [2], "user_ids": [12]}, @@ -3507,11 +3514,11 @@ def test__handle_subscription_event_visual_notifications( ), ], ids=[ - "user_subscribed_to_stream:ZFLNone", + "user_subscribed_to_stream:ZFL0", "user_subscribed_to_stream:ZFL34", "user_subscribed_to_stream:ZFL34_should_be_35", "user_subscribed_to_stream:ZFL35", - "user_unsubscribed_from_stream:ZFLNone", + "user_unsubscribed_from_stream:ZFL0", "user_unsubscribed_from_stream:ZFL34", "user_unsubscribed_from_stream:ZFL34_should_be_35", "user_unsubscribed_from_stream:ZFL35", diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py index 2c5ce92d84..cb26395896 100644 --- a/tests/ui_tools/test_popups.py +++ b/tests/ui_tools/test_popups.py @@ -222,7 +222,7 @@ def test_keypress_exit_popup_invalid_key( assert not self.controller.exit_popup.called def test_feature_level_content( - self, mocker: MockerFixture, zulip_version: Tuple[str, Optional[int]] + self, mocker: MockerFixture, zulip_version: Tuple[str, int] ) -> None: self.controller = mocker.Mock() mocker.patch.object( @@ -1282,16 +1282,16 @@ def test_keypress_stream_members( case( {"date_created": None, "is_announcement_only": True}, "74 [Organization default]", - None, + 0, 17, - id="ZFL=None_no_date_created__no_retention_days__admins_only", + id="ZFL=0_no_date_created__no_retention_days__admins_only", ), case( {"date_created": None, "is_announcement_only": False}, "74 [Organization default]", - None, + 0, 16, - id="ZFL=None_no_date_created__no_retention_days__anyone_can_type", + id="ZFL=0_no_date_created__no_retention_days__anyone_can_type", ), case( {"date_created": None, "stream_post_policy": 1}, @@ -1363,7 +1363,7 @@ def test_popup_height( general_stream: Dict[str, Any], to_vary_in_stream_data: Dict[str, Optional[int]], cached_message_retention_text: str, - server_feature_level: Optional[int], + server_feature_level: int, expected_height: int, ) -> None: model = self.controller.model diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 9bf3b5b54a..119770e174 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -655,7 +655,6 @@ class ServerSettings(TypedDict): # Added in Zulip 2.1.0 external_authentication_methods: List[ExternalAuthenticationMethod] - # TODO Refactor ZFL to default to zero zulip_feature_level: NotRequired[int] # New in Zulip 3.0, ZFL 1 zulip_version: str zulip_merge_base: NotRequired[str] # New in Zulip 5.0, ZFL 88 diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 22988c609b..b7d5287ac3 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -170,7 +170,7 @@ def __init__(self, controller: Any) -> None: self._fetch_initial_data() self.server_version = self.initial_data["zulip_version"] - self.server_feature_level = self.initial_data.get("zulip_feature_level") + self.server_feature_level: int = self.initial_data.get("zulip_feature_level", 0) self.user_dict: Dict[str, MinimalUserData] = {} self.user_id_email_dict: Dict[int, str] = {} @@ -189,8 +189,8 @@ def __init__(self, controller: Any) -> None: # NOTE: The date_created field of stream has been added in feature # level 30, server version 4. For consistency we add this field - # on server iterations even before this with value of None. - if self.server_feature_level is None or self.server_feature_level < 30: + # on earlier server iterations with the value of None. + if self.server_feature_level < 30: for stream in self.stream_dict.values(): stream["date_created"] = None @@ -203,7 +203,7 @@ def __init__(self, controller: Any) -> None: assert set(map(len, muted_topics)) in (set(), {2}, {3}) self._muted_topics: Dict[Tuple[str, str], Optional[int]] = { (stream_name, topic): ( - None if self.server_feature_level is None else date_muted[0] + None if self.server_feature_level == 0 else date_muted[0] ) for stream_name, topic, *date_muted in muted_topics } @@ -259,7 +259,7 @@ def normalize_and_cache_message_retention_text(self) -> None: # sream_id in model.cached_retention_text. This will be displayed in the UI. self.cached_retention_text: Dict[int, str] = {} realm_message_retention_days = self.initial_data["realm_message_retention_days"] - if self.server_feature_level is None or self.server_feature_level < 17: + if self.server_feature_level < 17: for stream in self.stream_dict.values(): stream["message_retention_days"] = None @@ -590,7 +590,7 @@ def update_stream_message( if content is not None: request["content"] = content - if self.server_feature_level is not None and self.server_feature_level >= 9: + if self.server_feature_level >= 9: request["send_notification_to_old_thread"] = notify_old request["send_notification_to_new_thread"] = notify_new @@ -1822,7 +1822,7 @@ def _handle_update_message_flags_event(self, event: Event) -> None: Handle change to message flags (eg. starred, read) """ assert event["type"] == "update_message_flags" - if self.server_feature_level is None or self.server_feature_level < 32: + if self.server_feature_level < 32: operation = event["operation"] else: operation = event["op"] diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index a5a0221f41..455d6a8136 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -1083,7 +1083,7 @@ def __init__( *, zt_version: str, server_version: str, - server_feature_level: Optional[int], + server_feature_level: int, theme_name: str, color_depth: int, autohide_enabled: bool, diff --git a/zulipterminal/version.py b/zulipterminal/version.py index 38597dcf19..2dee75ef36 100644 --- a/zulipterminal/version.py +++ b/zulipterminal/version.py @@ -5,7 +5,7 @@ ZT_VERSION = "0.7.0+git" SUPPORTED_SERVER_VERSIONS = [ - ("2.1", None), + ("2.1", 0), ("3.0", 25), ] From 764fb720c3256f4824f9ed4547177b3960dcb6ac Mon Sep 17 00:00:00 2001 From: Sushmey Date: Thu, 22 Feb 2024 17:06:17 +0530 Subject: [PATCH 193/276] bugfix: core: Resolve macOS crash on opening message in web browser. This occurs due to an attribute error in the existing code, since a class in the standard library webbrowser module didn't have a 'name' attribute until Python 3.11 under macOS. While this attribute was not officially documented until Python 3.11, many if not all other browser drivers provided a name attribute before this point, and the existing use of the attribute did not cause issues when our original implementation was tested. This bugfix substitutes a default value in the absence of the name, rather than testing against the specific platform/python combination, which will protect against similar issues arising in future. Test updated. Fixes #1471. --- tests/core/test_core.py | 26 ++++++++++++++++++++------ zulipterminal/core.py | 11 +++++++---- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/tests/core/test_core.py b/tests/core/test_core.py index 033f126fd7..be05d490cc 100644 --- a/tests/core/test_core.py +++ b/tests/core/test_core.py @@ -406,15 +406,24 @@ def test_copy_to_clipboard_exception( assert popup.call_args_list[0][0][1] == "area:error" @pytest.mark.parametrize( - "url", + "url, webbrowser_name, expected_webbrowser_name", [ - "https://chat.zulip.org/#narrow/stream/test", - "https://chat.zulip.org/user_uploads/sent/abcd/efg.png", - "https://github.com/", + ("https://chat.zulip.org/#narrow/stream/test", "chrome", "chrome"), + ( + "https://chat.zulip.org/user_uploads/sent/abcd/efg.png", + "mozilla", + "mozilla", + ), + ("https://github.com/", None, "default browser"), ], ) def test_open_in_browser_success( - self, mocker: MockerFixture, controller: Controller, url: str + self, + mocker: MockerFixture, + controller: Controller, + url: str, + webbrowser_name: Optional[str], + expected_webbrowser_name: str, ) -> None: # Set DISPLAY environ to be able to run test in CI os.environ["DISPLAY"] = ":0" @@ -422,11 +431,16 @@ def test_open_in_browser_success( mock_get = mocker.patch(MODULE + ".webbrowser.get") mock_open = mock_get.return_value.open + if webbrowser_name is None: + del mock_get.return_value.name + else: + mock_get.return_value.name = webbrowser_name + controller.open_in_browser(url) mock_open.assert_called_once_with(url) mocked_report_success.assert_called_once_with( - [f"The link was successfully opened using {mock_get.return_value.name}"] + [f"The link was successfully opened using {expected_webbrowser_name}"] ) def test_open_in_browser_fail__no_browser_controller( diff --git a/zulipterminal/core.py b/zulipterminal/core.py index ddc7e89b62..1e113545e8 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -414,11 +414,14 @@ def open_in_browser(self, url: str) -> None: # Suppress stdout and stderr when opening browser with suppress_output(): browser_controller.open(url) + + # MacOS using Python version < 3.11 has no "name" attribute + # - https://github.com/python/cpython/issues/82828 + # - https://github.com/python/cpython/issues/87590 + # Use a default value if missing, for macOS, and potential later issues + browser_name = getattr(browser_controller, "name", "default browser") self.report_success( - [ - "The link was successfully opened using " - f"{browser_controller.name}" - ] + [f"The link was successfully opened using {browser_name}"] ) except webbrowser.Error as e: # Set a footer text if no runnable browser is located From 72fe01eef642242964fa316af8286cbf37d0dad5 Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:07:09 +0530 Subject: [PATCH 194/276] keys: Add function to convert an urwid key into display format. Generates a user-friendly version of Urwid's command keys. Currently this applies capitalization of special keys and custom mapping of PgUp/PgDn keys. Tests added. Co-authored-by: Parth Shah --- tests/config/test_keys.py | 27 +++++++++++++++++++++++++++ zulipterminal/config/keys.py | 22 ++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/tests/config/test_keys.py b/tests/config/test_keys.py index c0807962d3..b09fd8aac0 100644 --- a/tests/config/test_keys.py +++ b/tests/config/test_keys.py @@ -112,3 +112,30 @@ def test_updated_urwid_command_map() -> None: assert key in keys.keys_for_command(zt_cmd) except KeyError: pass + + +@pytest.mark.parametrize( + "urwid_key, display_key", + [ + ("a", "a"), + ("B", "B"), + (":", ":"), + ("enter", "Enter"), + ("meta c", "Meta c"), + ("ctrl D", "Ctrl D"), + ("page up", "PgUp"), + ("ctrl page up", "Ctrl PgUp"), + ], + ids=[ + "lowercase_alphabet_key", + "uppercase_alphabet_key", + "symbol_key", + "special_key", + "lowercase_alphabet_key_with_modifier_key", + "uppercase_alphabet_key_with_modifier_key", + "mapped_key", + "mapped_key_with_modifier_key", + ], +) +def test_display_key_for_urwid_key(urwid_key: str, display_key: str) -> None: + assert keys.display_key_for_urwid_key(urwid_key) == display_key diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py index f6f6c09c0c..87626f0e37 100644 --- a/zulipterminal/config/keys.py +++ b/zulipterminal/config/keys.py @@ -460,6 +460,28 @@ def primary_key_for_command(command: str) -> str: return keys_for_command(command).pop(0) +URWID_KEY_TO_DISPLAY_KEY_MAPPING = { + "page up": "PgUp", + "page down": "PgDn", +} + + +def display_key_for_urwid_key(urwid_key: str) -> str: + """ + Returns a displayable user-centric format of the urwid key. + """ + for urwid_map_key, display_map_key in URWID_KEY_TO_DISPLAY_KEY_MAPPING.items(): + if urwid_map_key in urwid_key: + urwid_key = urwid_key.replace(urwid_map_key, display_map_key) + display_key = [ + keyboard_key.capitalize() + if len(keyboard_key) > 1 and keyboard_key[0].islower() + else keyboard_key + for keyboard_key in urwid_key.split() + ] + return " ".join(display_key) + + def commands_for_random_tips() -> List[KeyBinding]: """ Return list of commands which may be displayed as a random tip From 48f32d12412ad796c197c18474bc3bdba9e20369 Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:54:27 +0530 Subject: [PATCH 195/276] keys: Generate user-friendly representations of hotkey commands. Since keys_for_command() is used for registering keypresses, separate methods are added for supporting keyboard representations of the urwid keys, without replacing the existing methods. Tests added. Co-authored-by: Parth Shah --- tests/config/test_keys.py | 24 +++++++++++++++++++++++- zulipterminal/config/keys.py | 16 ++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/config/test_keys.py b/tests/config/test_keys.py index b09fd8aac0..752e69af29 100644 --- a/tests/config/test_keys.py +++ b/tests/config/test_keys.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any, Dict, List import pytest from pytest_mock import MockerFixture @@ -139,3 +139,25 @@ def test_updated_urwid_command_map() -> None: ) def test_display_key_for_urwid_key(urwid_key: str, display_key: str) -> None: assert keys.display_key_for_urwid_key(urwid_key) == display_key + + +COMMAND_TO_DISPLAY_KEYS = [ + ("NEXT_LINE", ["Down", "Ctrl n"]), + ("TOGGLE_STAR_STATUS", ["Ctrl s", "*"]), + ("ALL_PM", ["P"]), +] + + +@pytest.mark.parametrize("command, display_keys", COMMAND_TO_DISPLAY_KEYS) +def test_display_keys_for_command(command: str, display_keys: List[str]) -> None: + assert keys.display_keys_for_command(command) == display_keys + + +@pytest.mark.parametrize("command, display_keys", COMMAND_TO_DISPLAY_KEYS) +def test_primary_display_key_for_command(command: str, display_keys: List[str]) -> None: + assert keys.primary_display_key_for_command(command) == display_keys[0] + + +def test_display_keys_for_command_invalid_command(invalid_command: str) -> None: + with pytest.raises(keys.InvalidCommand): + keys.display_keys_for_command(invalid_command) diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py index 87626f0e37..9e2ace94f9 100644 --- a/zulipterminal/config/keys.py +++ b/zulipterminal/config/keys.py @@ -482,6 +482,22 @@ def display_key_for_urwid_key(urwid_key: str) -> str: return " ".join(display_key) +def display_keys_for_command(command: str) -> List[str]: + """ + Returns the user-friendly display keys for a given mapped command + """ + return [ + display_key_for_urwid_key(urwid_key) for urwid_key in keys_for_command(command) + ] + + +def primary_display_key_for_command(command: str) -> str: + """ + Primary Display Key is the formatted display version of the primary key + """ + return display_key_for_urwid_key(primary_key_for_command(command)) + + def commands_for_random_tips() -> List[KeyBinding]: """ Return list of commands which may be displayed as a random tip From a2e616e482fb2c2efcb0d0bef01c03fc023cd80c Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:10:05 +0530 Subject: [PATCH 196/276] helper/model/ui/boxes/buttons/views: Refine UI display of hotkeys. Update all the files that display hotkeys to the user, to use the new display functions for consistent output. The internal usage (mainly for keypresses) is left to use the default Urwid representation. Tests updated. Co-authored-by: Parth Shah --- tests/helper/test_helper.py | 4 ++-- tests/ui_tools/test_boxes.py | 17 ++++++++++++----- zulipterminal/helper.py | 4 ++-- zulipterminal/model.py | 4 ++-- zulipterminal/ui.py | 13 ++++++++++--- zulipterminal/ui_tools/boxes.py | 21 ++++++++++++++------- zulipterminal/ui_tools/buttons.py | 20 +++++++++++++++----- zulipterminal/ui_tools/views.py | 30 ++++++++++++++++++++---------- 8 files changed, 77 insertions(+), 36 deletions(-) diff --git a/tests/helper/test_helper.py b/tests/helper/test_helper.py index 8a79359ea4..2e32e5c10f 100644 --- a/tests/helper/test_helper.py +++ b/tests/helper/test_helper.py @@ -5,7 +5,7 @@ from pytest_mock import MockerFixture from zulipterminal.api_types import Composition -from zulipterminal.config.keys import primary_key_for_command +from zulipterminal.config.keys import primary_display_key_for_command from zulipterminal.helper import ( Index, canonicalize_color, @@ -469,7 +469,7 @@ def test_notify_if_message_sent_outside_narrow( notify_if_message_sent_outside_narrow(req, controller) if footer_updated: - key = primary_key_for_command("NARROW_MESSAGE_RECIPIENT") + key = primary_display_key_for_command("NARROW_MESSAGE_RECIPIENT") report_success.assert_called_once_with( [ "Message is sent outside of current narrow." diff --git a/tests/ui_tools/test_boxes.py b/tests/ui_tools/test_boxes.py index dbdd0829e7..8fc050391f 100644 --- a/tests/ui_tools/test_boxes.py +++ b/tests/ui_tools/test_boxes.py @@ -12,7 +12,11 @@ TYPING_STARTED_WAIT_PERIOD, TYPING_STOPPED_WAIT_PERIOD, ) -from zulipterminal.config.keys import keys_for_command, primary_key_for_command +from zulipterminal.config.keys import ( + keys_for_command, + primary_display_key_for_command, + primary_key_for_command, +) from zulipterminal.config.symbols import ( INVALID_MARKER, STREAM_MARKER_PRIVATE, @@ -379,9 +383,12 @@ def test_footer_notification_on_invalid_recipients( expected_lines = [ "Invalid recipient(s) - " + invalid_recipients, " - Use ", - ("footer_contrast", primary_key_for_command("AUTOCOMPLETE")), + ("footer_contrast", primary_display_key_for_command("AUTOCOMPLETE")), " or ", - ("footer_contrast", primary_key_for_command("AUTOCOMPLETE_REVERSE")), + ( + "footer_contrast", + primary_display_key_for_command("AUTOCOMPLETE_REVERSE"), + ), " to autocomplete.", ] @@ -1760,8 +1767,8 @@ class TestPanelSearchBox: @pytest.fixture def panel_search_box(self, mocker: MockerFixture) -> PanelSearchBox: - # X is the return from keys_for_command("UNTESTED_TOKEN") - mocker.patch(MODULE + ".keys_for_command", return_value="X") + # X is the return from display_keys_for_command("UNTESTED_TOKEN") + mocker.patch(MODULE + ".display_keys_for_command", return_value="X") panel_view = mocker.Mock() update_func = mocker.Mock() return PanelSearchBox(panel_view, "UNTESTED_TOKEN", update_func) diff --git a/zulipterminal/helper.py b/zulipterminal/helper.py index 52e407628a..76d0a0158d 100644 --- a/zulipterminal/helper.py +++ b/zulipterminal/helper.py @@ -33,7 +33,7 @@ from typing_extensions import Literal, ParamSpec, TypedDict from zulipterminal.api_types import Composition, EmojiType, Message -from zulipterminal.config.keys import primary_key_for_command +from zulipterminal.config.keys import primary_display_key_for_command from zulipterminal.config.regexes import ( REGEX_COLOR_3_DIGIT, REGEX_COLOR_6_DIGIT, @@ -700,7 +700,7 @@ def check_narrow_and_notify( and current_narrow != outer_narrow and current_narrow != inner_narrow ): - key = primary_key_for_command("NARROW_MESSAGE_RECIPIENT") + key = primary_display_key_for_command("NARROW_MESSAGE_RECIPIENT") controller.report_success( [ diff --git a/zulipterminal/model.py b/zulipterminal/model.py index b7d5287ac3..af0b8469e7 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -56,7 +56,7 @@ UpdateMessageContentEvent, UpdateMessagesLocationEvent, ) -from zulipterminal.config.keys import primary_key_for_command +from zulipterminal.config.keys import primary_display_key_for_command from zulipterminal.config.symbols import STREAM_TOPIC_SEPARATOR from zulipterminal.config.ui_mappings import EDIT_TOPIC_POLICY, ROLE_BY_ID, STATE_ICON from zulipterminal.helper import ( @@ -1671,7 +1671,7 @@ def _handle_message_event(self, event: Event) -> None: "Press '{}' to close this window." ) notice = notice_template.format( - failed_command, primary_key_for_command("GO_BACK") + failed_command, primary_display_key_for_command("GO_BACK") ) self.controller.popup_with_message(notice, width=50) self.controller.update_screen() diff --git a/zulipterminal/ui.py b/zulipterminal/ui.py index 3507f72b6c..b6a5a3d507 100644 --- a/zulipterminal/ui.py +++ b/zulipterminal/ui.py @@ -9,7 +9,11 @@ import urwid -from zulipterminal.config.keys import commands_for_random_tips, is_command_key +from zulipterminal.config.keys import ( + commands_for_random_tips, + display_key_for_urwid_key, + is_command_key, +) from zulipterminal.config.symbols import ( APPLICATION_TITLE_BAR_LINE, AUTOHIDE_TAB_LEFT_ARROW, @@ -102,10 +106,13 @@ def get_random_help(self) -> List[Any]: if not allowed_commands: return ["Help(?): "] random_command = random.choice(allowed_commands) + random_command_display_keys = ", ".join( + [display_key_for_urwid_key(key) for key in random_command["keys"]] + ) return [ "Help(?): ", - ("footer_contrast", " " + ", ".join(random_command["keys"]) + " "), - " " + random_command["help_text"], + ("footer_contrast", f" {random_command_display_keys} "), + f" {random_command['help_text']}", ] @asynch diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index 6867d5a0e8..441eaba4ba 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -15,8 +15,9 @@ from zulipterminal.api_types import Composition, PrivateComposition, StreamComposition from zulipterminal.config.keys import ( + display_keys_for_command, is_command_key, - keys_for_command, + primary_display_key_for_command, primary_key_for_command, ) from zulipterminal.config.regexes import ( @@ -302,11 +303,11 @@ def _tidy_valid_recipients_and_notify_invalid_ones( invalid_recipients_error = [ "Invalid recipient(s) - " + ", ".join(invalid_recipients), " - Use ", - ("footer_contrast", primary_key_for_command("AUTOCOMPLETE")), + ("footer_contrast", primary_display_key_for_command("AUTOCOMPLETE")), " or ", ( "footer_contrast", - primary_key_for_command("AUTOCOMPLETE_REVERSE"), + primary_display_key_for_command("AUTOCOMPLETE_REVERSE"), ), " to autocomplete.", ] @@ -850,8 +851,10 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: invalid_stream_error = ( "Invalid stream name." " Use {} or {} to autocomplete.".format( - primary_key_for_command("AUTOCOMPLETE"), - primary_key_for_command("AUTOCOMPLETE_REVERSE"), + primary_display_key_for_command("AUTOCOMPLETE"), + primary_display_key_for_command( + "AUTOCOMPLETE_REVERSE" + ), ) ) self.view.controller.report_error([invalid_stream_error]) @@ -918,7 +921,9 @@ def __init__(self, controller: Any) -> None: super().__init__(self.main_view()) def main_view(self) -> Any: - search_text = f"Search [{', '.join(keys_for_command('SEARCH_MESSAGES'))}]: " + search_text = ( + f"Search [{', '.join(display_keys_for_command('SEARCH_MESSAGES'))}]: " + ) self.text_box = ReadlineEdit(f"{search_text} ") # Add some text so that when packing, # urwid doesn't hide the widget. @@ -967,7 +972,9 @@ def __init__( ) -> None: self.panel_view = panel_view self.search_command = search_command - self.search_text = f" Search [{', '.join(keys_for_command(search_command))}]: " + self.search_text = ( + f" Search [{', '.join(display_keys_for_command(search_command))}]: " + ) self.search_error = urwid.AttrMap( urwid.Text([" ", INVALID_MARKER, " No Results"]), "search_error" ) diff --git a/zulipterminal/ui_tools/buttons.py b/zulipterminal/ui_tools/buttons.py index 748c86c2f0..9ecb2d32e0 100644 --- a/zulipterminal/ui_tools/buttons.py +++ b/zulipterminal/ui_tools/buttons.py @@ -11,7 +11,11 @@ from typing_extensions import TypedDict from zulipterminal.api_types import RESOLVED_TOPIC_PREFIX, EditPropagateMode, Message -from zulipterminal.config.keys import is_command_key, primary_key_for_command +from zulipterminal.config.keys import ( + is_command_key, + primary_display_key_for_command, + primary_key_for_command, +) from zulipterminal.config.regexes import REGEX_INTERNAL_LINK_STREAM_ID from zulipterminal.config.symbols import ( ALL_MESSAGES_MARKER, @@ -125,7 +129,9 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: class HomeButton(TopButton): def __init__(self, *, controller: Any, count: int) -> None: - button_text = f"All messages [{primary_key_for_command('ALL_MESSAGES')}]" + button_text = ( + f"All messages [{primary_display_key_for_command('ALL_MESSAGES')}]" + ) super().__init__( controller=controller, @@ -139,7 +145,7 @@ def __init__(self, *, controller: Any, count: int) -> None: class PMButton(TopButton): def __init__(self, *, controller: Any, count: int) -> None: - button_text = f"Direct messages [{primary_key_for_command('ALL_PM')}]" + button_text = f"Direct messages [{primary_display_key_for_command('ALL_PM')}]" super().__init__( controller=controller, @@ -153,7 +159,9 @@ def __init__(self, *, controller: Any, count: int) -> None: class MentionedButton(TopButton): def __init__(self, *, controller: Any, count: int) -> None: - button_text = f"Mentions [{primary_key_for_command('ALL_MENTIONS')}]" + button_text = ( + f"Mentions [{primary_display_key_for_command('ALL_MENTIONS')}]" + ) super().__init__( controller=controller, @@ -167,7 +175,9 @@ def __init__(self, *, controller: Any, count: int) -> None: class StarredButton(TopButton): def __init__(self, *, controller: Any, count: int) -> None: - button_text = f"Starred messages [{primary_key_for_command('ALL_STARRED')}]" + button_text = ( + f"Starred messages [{primary_display_key_for_command('ALL_STARRED')}]" + ) super().__init__( controller=controller, diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 455d6a8136..208850da36 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -14,8 +14,9 @@ from zulipterminal.config.keys import ( HELP_CATEGORIES, KEY_BINDINGS, + display_key_for_urwid_key, + display_keys_for_command, is_command_key, - keys_for_command, primary_key_for_command, ) from zulipterminal.config.markdown_examples import MARKDOWN_ELEMENTS @@ -1225,9 +1226,14 @@ def __init__(self, controller: Any, title: str) -> None: for binding in KEY_BINDINGS.values() if binding["key_category"] == category ) - key_bindings = [] - for binding in keys_in_category: - key_bindings.append((binding["help_text"], ", ".join(binding["keys"]))) + key_bindings = [ + ( + binding["help_text"], + ", ".join(map(display_key_for_urwid_key, binding["keys"])), + ) + for binding in keys_in_category + ] + help_menu_content.append((HELP_CATEGORIES[category], key_bindings)) popup_width, column_widths = self.calculate_table_widths( @@ -1379,7 +1385,7 @@ def __init__(self, controller: Any, stream_id: int) -> None: if stream["history_public_to_subscribers"] else "Not Public to Users" ) - member_keys = ", ".join(map(repr, keys_for_command("STREAM_MEMBERS"))) + member_keys = ", ".join(map(repr, display_keys_for_command("STREAM_MEMBERS"))) # FIXME: This field was removed from the subscription data in Zulip 7.5 / ZFL226 # We should use the new /streams/{stream_id}/email_address endpoint instead @@ -1387,7 +1393,9 @@ def __init__(self, controller: Any, stream_id: int) -> None: if self._stream_email is None: stream_copy_text = "< Stream email is unavailable >" else: - email_keys = ", ".join(map(repr, keys_for_command("COPY_STREAM_EMAIL"))) + email_keys = ", ".join( + map(repr, display_keys_for_command("COPY_STREAM_EMAIL")) + ) stream_copy_text = f"Press {email_keys} to copy Stream email address" weekly_traffic = stream["stream_weekly_traffic"] @@ -1562,14 +1570,14 @@ def __init__( msg["timestamp"], show_seconds=True, show_year=True ) view_in_browser_keys = "[{}]".format( - ", ".join(map(str, keys_for_command("VIEW_IN_BROWSER"))) + ", ".join(map(str, display_keys_for_command("VIEW_IN_BROWSER"))) ) full_rendered_message_keys = "[{}]".format( - ", ".join(map(str, keys_for_command("FULL_RENDERED_MESSAGE"))) + ", ".join(map(str, display_keys_for_command("FULL_RENDERED_MESSAGE"))) ) full_raw_message_keys = "[{}]".format( - ", ".join(map(str, keys_for_command("FULL_RAW_MESSAGE"))) + ", ".join(map(str, display_keys_for_command("FULL_RAW_MESSAGE"))) ) msg_info = [ ( @@ -1601,7 +1609,9 @@ def __init__( if self.show_edit_history_label: msg_info[0][1][0] = ("Date & Time (Original)", date_and_time) - keys = "[{}]".format(", ".join(map(str, keys_for_command("EDIT_HISTORY")))) + keys = "[{}]".format( + ", ".join(map(str, display_keys_for_command("EDIT_HISTORY"))) + ) msg_info[1][1].append(("Edit History", keys)) # Render the category using the existing table methods if links exist. if message_links: From d65cbee7406a3413946e78fb7fcff8ddc8875251 Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Thu, 4 Apr 2024 18:37:44 +0530 Subject: [PATCH 197/276] lint-hotkeys: Refine display of keys in hotkeys doc. Uses the new display functions from keys.py. Applies: - Capitalization of special keys, eg. Esc, Meta, Ctrl, Up - Fix "page + up" to "PgUp" (& PgDn) Fixes parts of #945. --- docs/hotkeys.md | 74 +++++++++++++++++++++++----------------------- tools/lint-hotkeys | 12 ++++++-- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/docs/hotkeys.md b/docs/hotkeys.md index 38ad92b34d..c05e0886e3 100644 --- a/docs/hotkeys.md +++ b/docs/hotkeys.md @@ -6,31 +6,31 @@ |Command|Key Combination| | :--- | :---: | |Show/hide help menu|?| -|Show/hide markdown help menu|meta + m| -|Show/hide about menu|meta + ?| -|Go Back|esc| +|Show/hide markdown help menu|Meta + m| +|Show/hide about menu|Meta + ?| +|Go Back|Esc| |Open draft message saved in this session|d| -|Redraw screen|ctrl + l| -|Quit|ctrl + c| +|Redraw screen|Ctrl + l| +|Quit|Ctrl + c| |View user information (From Users list)|i| ## Navigation |Command|Key Combination| | :--- | :---: | -|Go up / Previous message|up / k| -|Go down / Next message|down / j| -|Go left|left / h| -|Go right|right / l| -|Scroll up|page + up / K| -|Scroll down|page + down / J| -|Go to bottom / Last message|end / G| -|Narrow to all messages|a / esc| +|Go up / Previous message|Up / k| +|Go down / Next message|Down / j| +|Go left|Left / h| +|Go right|Right / l| +|Scroll up|PgUp / K| +|Scroll down|PgDn / J| +|Go to bottom / Last message|End / G| +|Narrow to all messages|a / Esc| |Narrow to all direct messages|P| |Narrow to all starred messages|f| |Narrow to messages in which you're mentioned|#| |Next unread topic|n| |Next unread direct message|p| -|Perform current action|enter| +|Perform current action|Enter| ## Searching |Command|Key Combination| @@ -44,7 +44,7 @@ ## Message actions |Command|Key Combination| | :--- | :---: | -|Reply to the current message|r / enter| +|Reply to the current message|r / Enter| |Reply mentioning the sender of the current message|@| |Reply quoting the current message text|>| |Reply directly to the sender of the current message|R| @@ -57,7 +57,7 @@ |Narrow to a topic/direct-chat, or stream/all-direct-messages|z| |Toggle first emoji reaction on selected message|=| |Add/remove thumbs-up reaction to the current message|+| -|Add/remove star status of the current message|ctrl + s / *| +|Add/remove star status of the current message|Ctrl + s / *| |Show/hide message information|i| |Show/hide message sender information|u| |Show/hide edit history (from message information)|e| @@ -77,25 +77,25 @@ ## Composing a Message |Command|Key Combination| | :--- | :---: | -|Cycle through recipient and content boxes|tab| -|Send a message|ctrl + d / meta + enter| -|Save current message as a draft|meta + s| -|Autocomplete @mentions, #stream_names, :emoji: and topics|ctrl + f| -|Cycle through autocomplete suggestions in reverse|ctrl + r| -|Narrow to compose box message recipient|meta + .| -|Jump to the beginning of line|ctrl + a| -|Jump to the end of line|ctrl + e| -|Jump backward one word|meta + b| -|Jump forward one word|meta + f| -|Delete previous character (to left)|ctrl + h| -|Transpose characters|ctrl + t| -|Cut forwards to the end of the line|ctrl + k| -|Cut backwards to the start of the line|ctrl + u| -|Cut forwards to the end of the current word|meta + d| -|Cut backwards to the start of the current word|ctrl + w| -|Paste last cut section|ctrl + y| -|Undo last action|ctrl + _| -|Jump to the previous line|up / ctrl + p| -|Jump to the next line|down / ctrl + n| -|Clear compose box|ctrl + l| +|Cycle through recipient and content boxes|Tab| +|Send a message|Ctrl + d / Meta + Enter| +|Save current message as a draft|Meta + s| +|Autocomplete @mentions, #stream_names, :emoji: and topics|Ctrl + f| +|Cycle through autocomplete suggestions in reverse|Ctrl + r| +|Narrow to compose box message recipient|Meta + .| +|Jump to the beginning of line|Ctrl + a| +|Jump to the end of line|Ctrl + e| +|Jump backward one word|Meta + b| +|Jump forward one word|Meta + f| +|Delete previous character (to left)|Ctrl + h| +|Transpose characters|Ctrl + t| +|Cut forwards to the end of the line|Ctrl + k| +|Cut backwards to the start of the line|Ctrl + u| +|Cut forwards to the end of the current word|Meta + d| +|Cut backwards to the start of the current word|Ctrl + w| +|Paste last cut section|Ctrl + y| +|Undo last action|Ctrl + _| +|Jump to the previous line|Up / Ctrl + p| +|Jump to the next line|Down / Ctrl + n| +|Clear compose box|Ctrl + l| diff --git a/tools/lint-hotkeys b/tools/lint-hotkeys index 68e9f8cdc2..5ae1363923 100755 --- a/tools/lint-hotkeys +++ b/tools/lint-hotkeys @@ -6,7 +6,11 @@ from collections import defaultdict from pathlib import Path, PurePath from typing import Dict, List, Tuple -from zulipterminal.config.keys import HELP_CATEGORIES, KEY_BINDINGS +from zulipterminal.config.keys import ( + HELP_CATEGORIES, + KEY_BINDINGS, + display_keys_for_command, +) KEYS_FILE = ( @@ -129,8 +133,10 @@ def read_help_categories() -> Dict[str, List[Tuple[str, List[str]]]]: Get all help categories from KEYS_FILE """ categories = defaultdict(list) - for item in KEY_BINDINGS.values(): - categories[item["key_category"]].append((item["help_text"], item["keys"])) + for cmd, item in KEY_BINDINGS.items(): + categories[item["key_category"]].append( + (item["help_text"], display_keys_for_command(cmd)) + ) return categories From bb1280fded2141df6b2911269518324a713bb29f Mon Sep 17 00:00:00 2001 From: rsashank Date: Mon, 15 Apr 2024 01:21:21 +0530 Subject: [PATCH 198/276] refactor: api_types/model: Move presence interval defaults to api_types. From ZFL 164, the server provides presence interval parameters; this commit moves the default values for earlier feature levels to 'api_types'. --- zulipterminal/api_types.py | 8 ++++++++ zulipterminal/model.py | 11 +++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index 119770e174..b341993571 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -34,6 +34,14 @@ MAX_MESSAGE_LENGTH: Final = 10000 +############################################################################### +# These values are in the register response from ZFL 164 +# Before this feature level, they had the listed default (fixed) values + +PRESENCE_OFFLINE_THRESHOLD_SECS: Final = 140 +PRESENCE_PING_INTERVAL_SECS: Final = 60 + + ############################################################################### # Core message types (used in Composition and Message below) diff --git a/zulipterminal/model.py b/zulipterminal/model.py index af0b8469e7..1d5c89ce4d 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -34,6 +34,8 @@ MAX_MESSAGE_LENGTH, MAX_STREAM_NAME_LENGTH, MAX_TOPIC_NAME_LENGTH, + PRESENCE_OFFLINE_THRESHOLD_SECS, + PRESENCE_PING_INTERVAL_SECS, TYPING_STARTED_EXPIRY_PERIOD, TYPING_STARTED_WAIT_PERIOD, TYPING_STOPPED_WAIT_PERIOD, @@ -81,9 +83,6 @@ from zulipterminal.ui_tools.utils import create_msg_box_list -OFFLINE_THRESHOLD_SECS = 140 - - class ServerConnectionFailure(Exception): pass @@ -445,7 +444,7 @@ def _start_presence_updates(self) -> None: view = self.controller.view view.users_view.update_user_list(user_list=self.users) view.middle_column.update_message_list_status_markers() - time.sleep(60) + time.sleep(PRESENCE_PING_INTERVAL_SECS) @asynch def toggle_message_reaction( @@ -1202,7 +1201,7 @@ def _update_users_data_from_initial_data(self) -> None: * * Out of the ClientPresence objects found in `presence`, we * consider only those with a timestamp newer than - * OFFLINE_THRESHOLD_SECS; then of + * PRESENCE_OFFLINE_THRESHOLD_SECS; then of * those, return the one that has the greatest UserStatus, where * `active` > `idle` > `offline`. * @@ -1216,7 +1215,7 @@ def _update_users_data_from_initial_data(self) -> None: timestamp = client[1]["timestamp"] if client_name == "aggregated": continue - elif (time.time() - timestamp) < OFFLINE_THRESHOLD_SECS: + elif (time.time() - timestamp) < PRESENCE_OFFLINE_THRESHOLD_SECS: if status == "active": aggregate_status = "active" if status == "idle" and aggregate_status != "active": From 36d3cc307729a23dd629d207e35ec5fd15ed4293 Mon Sep 17 00:00:00 2001 From: rsashank Date: Mon, 15 Apr 2024 07:13:11 +0530 Subject: [PATCH 199/276] model: Store & use any presence interval values from server. Stores the presence interval values received from the register response in the model. If the feature level is lower, it defaults to the values defined in api_types. Test added. Fixes #1421. --- tests/model/test_model.py | 56 +++++++++++++++++++++++++++++++++++++++ zulipterminal/model.py | 22 ++++++++++++--- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 83ce9de168..6b5a18eac2 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -14,6 +14,8 @@ MAX_MESSAGE_LENGTH, MAX_STREAM_NAME_LENGTH, MAX_TOPIC_NAME_LENGTH, + PRESENCE_OFFLINE_THRESHOLD_SECS, + PRESENCE_PING_INTERVAL_SECS, TYPING_STARTED_EXPIRY_PERIOD, TYPING_STARTED_WAIT_PERIOD, TYPING_STOPPED_WAIT_PERIOD, @@ -1440,6 +1442,60 @@ def test__store_typing_duration_settings__with_values( assert model.typing_stopped_wait_period == typing_stopped_wait assert model.typing_started_expiry_period == typing_started_expiry + @pytest.mark.parametrize( + "feature_level, to_vary_in_initial_data, " + "expected_offline_threshold, expected_presence_ping_interval", + [ + (0, {}, PRESENCE_OFFLINE_THRESHOLD_SECS, PRESENCE_PING_INTERVAL_SECS), + (157, {}, PRESENCE_OFFLINE_THRESHOLD_SECS, PRESENCE_PING_INTERVAL_SECS), + ( + 164, + { + "server_presence_offline_threshold_seconds": 200, + "server_presence_ping_interval_seconds": 100, + }, + 200, + 100, + ), + ], + ids=[ + "Zulip_2.1_ZFL_0_hard_coded", + "Zulip_6.2_ZFL_157_hard_coded", + "Zulip_7.0_ZFL_164_server_provided", + ], + ) + def test__store_server_presence_intervals( + self, + model, + initial_data, + feature_level, + to_vary_in_initial_data, + expected_offline_threshold, + expected_presence_ping_interval, + ): + # Ensure inputs are not the defaults, to avoid the test accidentally passing + assert ( + to_vary_in_initial_data.get("server_presence_offline_threshold_seconds") + != PRESENCE_OFFLINE_THRESHOLD_SECS + ) + assert ( + to_vary_in_initial_data.get("server_presence_ping_interval_seconds") + != PRESENCE_PING_INTERVAL_SECS + ) + + initial_data.update(to_vary_in_initial_data) + model.initial_data = initial_data + model.server_feature_level = feature_level + + model._store_server_presence_intervals() + + assert ( + model.server_presence_offline_threshold_secs == expected_offline_threshold + ) + assert ( + model.server_presence_ping_interval_secs == expected_presence_ping_interval + ) + def test_get_message_false_first_anchor( self, mocker, diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 1d5c89ce4d..72215c8470 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -171,6 +171,8 @@ def __init__(self, controller: Any) -> None: self.server_version = self.initial_data["zulip_version"] self.server_feature_level: int = self.initial_data.get("zulip_feature_level", 0) + self._store_server_presence_intervals() + self.user_dict: Dict[str, MinimalUserData] = {} self.user_id_email_dict: Dict[int, str] = {} self._all_users_by_id: Dict[int, RealmUser] = {} @@ -444,7 +446,7 @@ def _start_presence_updates(self) -> None: view = self.controller.view view.users_view.update_user_list(user_list=self.users) view.middle_column.update_message_list_status_markers() - time.sleep(PRESENCE_PING_INTERVAL_SECS) + time.sleep(self.server_presence_ping_interval_secs) @asynch def toggle_message_reaction( @@ -813,6 +815,18 @@ def _store_typing_duration_settings(self) -> None: TYPING_STARTED_EXPIRY_PERIOD, ) + def _store_server_presence_intervals(self) -> None: + """ + In ZFL 164, these values were added to the register response. + Uses default values if not received. + """ + self.server_presence_offline_threshold_secs = self.initial_data.get( + "server_presence_offline_threshold_seconds", PRESENCE_OFFLINE_THRESHOLD_SECS + ) + self.server_presence_ping_interval_secs = self.initial_data.get( + "server_presence_ping_interval_seconds", PRESENCE_PING_INTERVAL_SECS + ) + @staticmethod def modernize_message_response(message: Message) -> Message: """ @@ -1201,7 +1215,7 @@ def _update_users_data_from_initial_data(self) -> None: * * Out of the ClientPresence objects found in `presence`, we * consider only those with a timestamp newer than - * PRESENCE_OFFLINE_THRESHOLD_SECS; then of + * self.server_presence_offline_threshold_secs; then of * those, return the one that has the greatest UserStatus, where * `active` > `idle` > `offline`. * @@ -1215,7 +1229,9 @@ def _update_users_data_from_initial_data(self) -> None: timestamp = client[1]["timestamp"] if client_name == "aggregated": continue - elif (time.time() - timestamp) < PRESENCE_OFFLINE_THRESHOLD_SECS: + elif ( + time.time() - timestamp + ) < self.server_presence_offline_threshold_secs: if status == "active": aggregate_status = "active" if status == "idle" and aggregate_status != "active": From 345a5ae357263d1fc6c012ff41db18f30670f65a Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Sat, 13 Apr 2024 12:02:11 +0530 Subject: [PATCH 200/276] refactor: core: Use a common scroll prompt in popup headers. --- zulipterminal/core.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/zulipterminal/core.py b/zulipterminal/core.py index 1e113545e8..99d6cac23a 100644 --- a/zulipterminal/core.py +++ b/zulipterminal/core.py @@ -51,6 +51,8 @@ ExceptionInfo = Tuple[Type[BaseException], BaseException, TracebackType] +SCROLL_PROMPT = "(up/down scrolls)" + class Controller: """ @@ -246,11 +248,11 @@ def exit_popup(self) -> None: self.loop.widget = self.view def show_help(self) -> None: - help_view = HelpView(self, "Help Menu (up/down scrolls)") + help_view = HelpView(self, f"Help Menu {SCROLL_PROMPT}") self.show_pop_up(help_view, "area:help") def show_markdown_help(self) -> None: - markdown_view = MarkdownHelpView(self, "Markdown Help Menu (up/down scrolls)") + markdown_view = MarkdownHelpView(self, f"Markdown Help Menu {SCROLL_PROMPT}") self.show_pop_up(markdown_view, "area:help") def show_topic_edit_mode(self, button: Any) -> None: @@ -266,7 +268,7 @@ def show_msg_info( msg_info_view = MsgInfoView( self, msg, - "Message Information (up/down scrolls)", + f"Message Information {SCROLL_PROMPT}", topic_links, message_links, time_mentions, @@ -315,7 +317,10 @@ def show_about(self) -> None: def show_user_info(self, user_id: int) -> None: self.show_pop_up( UserInfoView( - self, user_id, "User Information (up/down scrolls)", "USER_INFO" + self, + user_id, + f"User Information {SCROLL_PROMPT}", + "USER_INFO", ), "area:user", ) @@ -325,7 +330,7 @@ def show_msg_sender_info(self, user_id: int) -> None: UserInfoView( self, user_id, - "Message Sender Information (up/down scrolls)", + f"Message Sender Information {SCROLL_PROMPT}", "MSG_SENDER_INFO", ), "area:user", @@ -345,7 +350,7 @@ def show_full_rendered_message( topic_links, message_links, time_mentions, - "Full rendered message (up/down scrolls)", + f"Full rendered message {SCROLL_PROMPT}", ), "area:msg", ) @@ -364,7 +369,7 @@ def show_full_raw_message( topic_links, message_links, time_mentions, - "Full raw message (up/down scrolls)", + f"Full raw message {SCROLL_PROMPT}", ), "area:msg", ) @@ -383,7 +388,7 @@ def show_edit_history( topic_links, message_links, time_mentions, - "Edit History (up/down scrolls)", + f"Edit History {SCROLL_PROMPT}", ), "area:msg", ) From 107fa9b814fb6db6c4b5405faea1de727f8ceb04 Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Sun, 14 Apr 2024 15:00:52 +0530 Subject: [PATCH 201/276] lint-hotkeys: Add missing '--fix' flag in usage prompt. --- tools/lint-hotkeys | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/lint-hotkeys b/tools/lint-hotkeys index 5ae1363923..503a024e98 100755 --- a/tools/lint-hotkeys +++ b/tools/lint-hotkeys @@ -73,7 +73,9 @@ def lint_hotkeys_file() -> None: else: print("No hotkeys linting errors") if not output_file_matches_string(hotkeys_file_string): - print(f"Run './tools/{SCRIPT_NAME}' to update {OUTPUT_FILE_NAME} file") + print( + f"Run './tools/{SCRIPT_NAME} --fix' to update {OUTPUT_FILE_NAME} file" + ) error_flag = 1 sys.exit(error_flag) From 81b1e7247c2fd1f40ccb8385d84938e3697f904d Mon Sep 17 00:00:00 2001 From: sreecharan7 Date: Fri, 12 Jan 2024 09:52:36 +0530 Subject: [PATCH 202/276] api_types/model/boxes: Add read_by_sender when sending messages. This was added in ZFL 236 (Zulip 8.0). Tests updated. Fixes #1456. --- tests/model/test_model.py | 10 ++++++++-- zulipterminal/api_types.py | 2 ++ zulipterminal/model.py | 2 ++ zulipterminal/ui_tools/boxes.py | 2 ++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/model/test_model.py b/tests/model/test_model.py index 6b5a18eac2..43f80aee00 100644 --- a/tests/model/test_model.py +++ b/tests/model/test_model.py @@ -773,7 +773,7 @@ def test_send_private_message( result = model.send_private_message(recipients, content) - req = dict(type="private", to=recipients, content=content) + req = dict(type="private", to=recipients, content=content, read_by_sender=True) self.client.send_message.assert_called_once_with(req) assert result == return_value @@ -810,7 +810,13 @@ def test_send_stream_message( result = model.send_stream_message(stream, topic, content) - req = dict(type="stream", to=stream, subject=topic, content=content) + req = dict( + type="stream", + to=stream, + subject=topic, + content=content, + read_by_sender=True, + ) self.client.send_message.assert_called_once_with(req) assert result == return_value diff --git a/zulipterminal/api_types.py b/zulipterminal/api_types.py index b341993571..7e102e316f 100644 --- a/zulipterminal/api_types.py +++ b/zulipterminal/api_types.py @@ -93,6 +93,7 @@ class PrivateComposition(TypedDict): type: DirectMessageString content: str to: List[int] # User ids + read_by_sender: bool # New in ZFL 236, Zulip 8.0 class StreamComposition(TypedDict): @@ -100,6 +101,7 @@ class StreamComposition(TypedDict): content: str to: str # stream name # TODO: Migrate to using int (stream id) subject: str # TODO: Migrate to using topic + read_by_sender: bool # New in ZFL 236, Zulip 8.0 Composition = Union[PrivateComposition, StreamComposition] diff --git a/zulipterminal/model.py b/zulipterminal/model.py index 72215c8470..75f4c06069 100644 --- a/zulipterminal/model.py +++ b/zulipterminal/model.py @@ -541,6 +541,7 @@ def send_private_message(self, recipients: List[int], content: str) -> bool: type="private", to=recipients, content=content, + read_by_sender=True, ) response = self.client.send_message(composition) display_error_if_present(response, self.controller) @@ -557,6 +558,7 @@ def send_stream_message(self, stream: str, topic: str, content: str) -> bool: to=stream, subject=topic, content=content, + read_by_sender=True, ) response = self.client.send_message(composition) display_error_if_present(response, self.controller) diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index 441eaba4ba..b084ef5b4e 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -820,6 +820,7 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: type="private", to=self.recipient_user_ids, content=self.msg_write_box.edit_text, + read_by_sender=True, ) elif self.compose_box_status == "open_with_stream": this_draft = StreamComposition( @@ -827,6 +828,7 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: to=self.stream_write_box.edit_text, content=self.msg_write_box.edit_text, subject=self.title_write_box.edit_text, + read_by_sender=True, ) saved_draft = self.model.session_draft_message() if not saved_draft: From e32ae3e4c79363637c7788b33f763a52461d9b1b Mon Sep 17 00:00:00 2001 From: Niloth-p <20315308+Niloth-p@users.noreply.github.com> Date: Sat, 4 May 2024 11:21:45 +0530 Subject: [PATCH 203/276] boxes/views: Adopt urwid_readline for all editors. The panel search boxes were the only remaining editors not using urwid_readline.ReadlineEdit. --- zulipterminal/ui_tools/boxes.py | 2 +- zulipterminal/ui_tools/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/zulipterminal/ui_tools/boxes.py b/zulipterminal/ui_tools/boxes.py index b084ef5b4e..f6d3294241 100644 --- a/zulipterminal/ui_tools/boxes.py +++ b/zulipterminal/ui_tools/boxes.py @@ -964,7 +964,7 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]: return key -class PanelSearchBox(urwid.Edit): +class PanelSearchBox(ReadlineEdit): """ Search Box to search panel views in real-time. """ diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py index 208850da36..30fcc42a49 100644 --- a/zulipterminal/ui_tools/views.py +++ b/zulipterminal/ui_tools/views.py @@ -682,7 +682,7 @@ def update_user_list( user_list is not None and search_box is None and new_text is None ) # _start_presence_updates. - # Return if the method is called by PanelSearchBox (urwid.Edit) while + # Return if the method is called by PanelSearchBox (ReadlineEdit) while # the search is inactive and user_list is None. # NOTE: The additional not user_list check is to not false trap # _start_presence_updates but allow it to update the user list. From f65f8f274de4b60212998008250db77a6d6391ee Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Tue, 23 Apr 2024 11:38:35 -0700 Subject: [PATCH 204/276] github: Add preliminary issue templates & configuration. This adds basic issue templates to support the automatic labelling of reported issues, and some structure to guide users towards useful information to report. This doesn't use forms, much like in the main zulip/zulip repository, and also supports filing blank issues, since the structure is not necessary for each case. --- .github/ISSUE_TEMPLATE/01_crash.md | 39 +++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/02_bug.md | 36 +++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/03_parity.md | 25 +++++++++++++++++ .github/ISSUE_TEMPLATE/04_platform.md | 19 +++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++++++ 5 files changed, 127 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/01_crash.md create mode 100644 .github/ISSUE_TEMPLATE/02_bug.md create mode 100644 .github/ISSUE_TEMPLATE/03_parity.md create mode 100644 .github/ISSUE_TEMPLATE/04_platform.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/01_crash.md b/.github/ISSUE_TEMPLATE/01_crash.md new file mode 100644 index 0000000000..c3746708ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_crash.md @@ -0,0 +1,39 @@ +--- +name: Bug report (major) +about: A major bug which significantly impacts usability, eg. an unexpected quit, crash, freeze. +labels: 'bug: crash', +--- + +### Bug description + + + + + + +### How is the bug triggered? +How can you reproduce the bug? +1. + + +### Does it produce a 'traceback' or 'exception'? + +``` + + +``` + +### How are you running the application? +Please include as many of the following as possible: +- **Zulip-terminal version:** + eg. a specific version (0.7.0), or if running from `main` also ideally the git ref +- **Zulip server version(s):** + eg. Zulip Cloud, the version you are running self-hosted, or the Zulip Community server (chat.zulip.org) +- **Operating system (and version):** + eg. Debian Linux, Ubuntu Linux, macOS, WSL in Windows, Docker +- **Python version (and implementation):** + eg. 3.8, 3.9, 3.10, ... (implementation is likely to be eg. CPython, or PyPy) + +If possible, please provide details from the `About` menu: (hotkey: Meta + ?) +(this can provide some of the details above) + diff --git a/.github/ISSUE_TEMPLATE/02_bug.md b/.github/ISSUE_TEMPLATE/02_bug.md new file mode 100644 index 0000000000..00508e2c33 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_bug.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: A concrete bug report with steps to reproduce the behavior. +labels: bug, +--- + +### Bug description + + + + + + +### How is the bug triggered? +How can you reproduce the bug? +1. + + +### What did you expect to happen? + + + +### How are you running the application? +Please include as many of the following as possible: +- **Zulip-terminal version:** + eg. a specific version (0.7.0), or if running from `main` also ideally the git ref +- **Zulip server version(s):** + eg. Zulip Cloud, the version you are running self-hosted, or the Zulip Community server (chat.zulip.org) +- **Operating system (and version):** + eg. Debian Linux, Ubuntu Linux, macOS, WSL in Windows, Docker +- **Python version (and implementation):** + eg. 3.8, 3.9, 3.10, ... (implementation is likely to be eg. CPython, or PyPy) + +If possible, please provide details from the `About` menu: (hotkey: Meta + ?) +(this can provide some of the details above) + diff --git a/.github/ISSUE_TEMPLATE/03_parity.md b/.github/ISSUE_TEMPLATE/03_parity.md new file mode 100644 index 0000000000..7bc3960d02 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_parity.md @@ -0,0 +1,25 @@ +--- +name: Feature request (a missing known Zulip feature) +about: A request for a feature which is missing, but present in another Zulip client (eg. Web/Desktop/mobile). +labels: 'missing feature', +--- + +### Description of feature missing from another Zulip client + + + + + + + +### When was this feature first available in Zulip? +If you know: +- **Zulip version:** + (eg. 2.1, 5.0, 8.0, ..., or 'Zulip Cloud today') +- **Zulip feature level:** + (see https://zulip.com/api/changelog) + + +### Other details + + diff --git a/.github/ISSUE_TEMPLATE/04_platform.md b/.github/ISSUE_TEMPLATE/04_platform.md new file mode 100644 index 0000000000..708ecb6e60 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/04_platform.md @@ -0,0 +1,19 @@ +--- +name: Feature suggestion +about: A suggestion for an improvement, specific to the capabilities of the terminal environment. +labels: enhancement, +--- + + + +### Description of suggested feature + + + + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..77fa18bed5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Report broader Zulip issues (beyond Zulip-Terminal) + about: Includes links to best-practices for filing bug reports, feature requests, security vulnerabilities and more. + url: https://github.com/zulip/zulip/issues/new/choose + - name: Not sure? + about: 'Chat with us, using any of the Zulip clients, in #zulip-terminal on the Developer Community server (chat.zulip.org).' + url: https://zulip.com/development-community From 440a3fad9f43d256d3c472323a1a0bca0d411fab Mon Sep 17 00:00:00 2001 From: "neiljp (Neil Pilgrim)" Date: Fri, 17 May 2024 15:50:11 -0700 Subject: [PATCH 205/276] bugfix: github: Adjust label yaml quoting syntax. Prior to this, two templates were ignored due to syntax errors. Documentation suggests using quotes around all of labels, ie. including commas inside the quote. eg. https://github.com/orgs/community/discussions/23682 Labels with spaces and commas were already surrounded by quotes; this change quotes labels in each template, and includes a comma at the end inside the closing quote. --- .github/ISSUE_TEMPLATE/01_crash.md | 2 +- .github/ISSUE_TEMPLATE/02_bug.md | 2 +- .github/ISSUE_TEMPLATE/03_parity.md | 2 +- .github/ISSUE_TEMPLATE/04_platform.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01_crash.md b/.github/ISSUE_TEMPLATE/01_crash.md index c3746708ce..7c11abcd4f 100644 --- a/.github/ISSUE_TEMPLATE/01_crash.md +++ b/.github/ISSUE_TEMPLATE/01_crash.md @@ -1,7 +1,7 @@ --- name: Bug report (major) about: A major bug which significantly impacts usability, eg. an unexpected quit, crash, freeze. -labels: 'bug: crash', +labels: 'bug: crash,' --- ### Bug description diff --git a/.github/ISSUE_TEMPLATE/02_bug.md b/.github/ISSUE_TEMPLATE/02_bug.md index 00508e2c33..1ac0381330 100644 --- a/.github/ISSUE_TEMPLATE/02_bug.md +++ b/.github/ISSUE_TEMPLATE/02_bug.md @@ -1,7 +1,7 @@ --- name: Bug report about: A concrete bug report with steps to reproduce the behavior. -labels: bug, +labels: 'bug,' --- ### Bug description diff --git a/.github/ISSUE_TEMPLATE/03_parity.md b/.github/ISSUE_TEMPLATE/03_parity.md index 7bc3960d02..a3018c516b 100644 --- a/.github/ISSUE_TEMPLATE/03_parity.md +++ b/.github/ISSUE_TEMPLATE/03_parity.md @@ -1,7 +1,7 @@ --- name: Feature request (a missing known Zulip feature) about: A request for a feature which is missing, but present in another Zulip client (eg. Web/Desktop/mobile). -labels: 'missing feature', +labels: 'missing feature,' --- ### Description of feature missing from another Zulip client diff --git a/.github/ISSUE_TEMPLATE/04_platform.md b/.github/ISSUE_TEMPLATE/04_platform.md index 708ecb6e60..cd4592735a 100644 --- a/.github/ISSUE_TEMPLATE/04_platform.md +++ b/.github/ISSUE_TEMPLATE/04_platform.md @@ -1,7 +1,7 @@ --- name: Feature suggestion about: A suggestion for an improvement, specific to the capabilities of the terminal environment. -labels: enhancement, +labels: 'enhancement,' ---