-
-
Notifications
You must be signed in to change notification settings - Fork 268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for TODO Widget on ZT #1549
Conversation
ac81d5c
to
6587636
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rsashank See my comments in the channel, but from quick testing this appears to render todo widgets well statically, except for changing the todo title 👍
Having the code in its own file is fine, though as I possibly commented in the other PR, the processing is something I'd consider a model feature. However, that file is large and I suspect we'll split that file in due course, so it's fine to have this file separate as long as it's clear it's not UI-related.
For find_widget_type
, it seems odd to see the poll case in this PR, but more importantly note that there are other cases the code handles beyond the two expected cases. That is, it doesn't validate to only todo or poll, and the empty list is only triggering one "unknown"
case. For a contrived example, consider the unexpected behavior of:
{
...
"content": {"widget_type": "unknown", ...}
...
}
We do essentially validate that in the rendering code, but it could prove confusing depending on future widgets.
Generally I'd like to see the expected submessage types documented in api_types, even if we don't assume the data matches every time. Similarly, while you have a clear type for the return of process_todo_widget, this doesn't define the field names or types - typeddicts would make that clearer, but you could also use a dataclass or class.
Next steps
Other than handling the renaming issue, and fine-tuning the code you have, the main aspect I'd like to see is event-handling, so that todo list messages at least update from other clients, without needing to restart ZT.
433da5e
to
daaf808
Compare
@neiljp Thanks for the feedback! I've updated the PR to handle the Submessage event, so ZT reflects updates in real-time without needing a refresh. Thanks for pointing out the issue with the title not updating - it's fixed now! 👍 Added tests for both. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rsashank Thanks for the update - this looks to work very well, including the dynamic updating now 👍
This shouldn't need much more final polishing; most comments should be small changes?
Ideally the types would be added to the raw data, and we'd define the internal data we're passing between the model (widget) and UI. The latter would be preferable to include here, since you can type the new files better than with Any
in more places - and you are defining new data to pass around. The API may be more complex to cover well so would be fine later instead.
zulipterminal/model.py
Outdated
message_id = event["message_id"] | ||
if message_id in self.index["messages"]: | ||
message = self.index["messages"][message_id] | ||
message["submessages"].append(cast(Dict[str, Any], event)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needing this cast would make me feel more comfortable that we know the format of events and the original data, as per my other typing comment in this review cycle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood.
I constructed the dictionary instead of casting it. The casting was necessary to satisfy mypy, not for functional reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Constructing the dictionary is certainly an improvement. The aspects this doesn't cover is what 'str' are the keys in the dict, and what the 'Any' are in Message submessages. Note that this appears to be distinct from submessage events, and mypy would still be complaining about this if we went further than Dict[str, Any]
.
zulipterminal/api_types.py
Outdated
class SubmessageEvent(TypedDict): | ||
type: Literal["submessage"] | ||
|
||
message_id: int | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's much more in this type, based on the tests and code - and that this works :)
The way you're assigning it in the event suggests there's at least some compatibility in what you expect to see between this type and those that can appear in the original submessages dict of a message - but maybe not all?
In particular
- I expect there's one 'original' type (dict), and then different ones that can appear after it?
- Do all the fields match between the events and those types, when you're adding them?
Can you dig out those types and add them? That would help document the Message
type in this file, and make this code clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, got it. Added the other fields for SubmessageEvent, is this fine?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For SubmessageEvent
this is fine, though we might drop id
since I believe that's an event identifier which is common to all events.
Re the typing more generally, I was mainly looking to see if we could improve the submessage(s) typing in the Message
typedict - I did mention that above. Combined with the definition of SubmessageEvent, then it would be clearer how compatible these two types are, including via mypy.
Reading again, some of my comments above may not be applicable, since the content is a string.
zulipterminal/ui_tools/messages.py
Outdated
|
||
if tasks: | ||
for task_id, task_info in tasks.items(): | ||
task_status = "[✔ ]" if task_info["completed"] else "[ ]" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed this briefly in the channel, and you indicated that the double space was to avoid overrunning the square brackets?
Do you see 'shifted' tick marks for resolved topics too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only thing I can think of here is that the resolve-topic marker somehow has a unicode object which is setting a color or variant (if that's possible), or alternatively that it's something in the font you have locally?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔I tried changing a few fonts, didn't seem to make much of a difference. Does it appear well on your terminal?
Changed it to single space, I'll debug this soon.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Started #zulip-terminal>Resolved symbol not rendering for discussing this.
tests/widget/test_widget.py
Outdated
"content": '{"widget_type": "todo", "extra_data": ' | ||
'{"task_list_title": "Today\'s Tasks", "tasks": [{"task": ' | ||
'"Write code", "desc": ""}, {"task": "Sleep", "desc": ""}]}}', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I'm aware these are JSON fields, but do you think there's a cleaner way of expressing this? It looks like it's doing the right thing, but it's not that clear being line-wrapped, at least for the longer ones.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding noqa comments is one approach, but I think breaking up the lines like this improves readability more than having a single long line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes, a single long line would not be great for this, I was more thinking of making it more clearer to read again.
For example, ensuring the value was more distinct from the submessage keys:
"content": '{"widget_type": "todo", "extra_data": ' | |
'{"task_list_title": "Today\'s Tasks", "tasks": [{"task": ' | |
'"Write code", "desc": ""}, {"task": "Sleep", "desc": ""}]}}', | |
"content": ( | |
'{"widget_type": "todo", "extra_data": ' | |
'{"task_list_title": "Today\'s Tasks", "tasks": [{"task": ' | |
'"Write code", "desc": ""}, {"task": "Sleep", "desc": ""}]}}' | |
), |
or even adjusting the layout of the string to make this clearer (though this is difficult to do well):
"content": '{"widget_type": "todo", "extra_data": ' | |
'{"task_list_title": "Today\'s Tasks", "tasks": [{"task": ' | |
'"Write code", "desc": ""}, {"task": "Sleep", "desc": ""}]}}', | |
"content": ( | |
'{"widget_type": "todo",' | |
' "extra_data": {"task_list_title": "Today\'s Tasks",' | |
' "tasks": [{"task": "Write code", "desc": ""}, ' | |
'{"task": "Sleep", "desc": ""}]}}' | |
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it! Did the first one :)
tests/widget/test_widget.py
Outdated
"content": '{"type":"new_task","key":2,"task":"Eat","desc":"",' | ||
'"completed":false}', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this represent the real way it is formatted from the server? These look more compact than the widget_type
version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I used debug mode to retrieve.
zulipterminal/widget.py
Outdated
@@ -15,3 +15,49 @@ def find_widget_type(submessage: Any) -> str: | |||
return "unknown" | |||
else: | |||
return "unknown" | |||
|
|||
|
|||
def process_todo_widget(todo_list: Any) -> Tuple[str, Dict[str, Dict[str, Any]]]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The input here is the raw data, which relates back to my other comment, though at a minimum we know it is a List
or an abstract form of that (to avoid modifying it).
For the return, the Dict is one you're defining yourself, so the Any can be avoided there based on the fields you use. If you use a simpler type, mypy will have no idea if the UI code is using the names/types correctly. You can use whatever names you want in the returned data, to make it read clearly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is improved 👍
To clarify my point re the processed data, as per other comments, using a TypedDict (or class or dataclass) enables the str to be constrained to what you expect, as well as the value types for each key :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I'll update that in a follow-up PR, if that's fine.
if widget.get("widget_type") == "todo": | ||
if "extra_data" in widget and widget["extra_data"] is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This part could be slightly clearer and simpler in how it handles extra_data
.
When is extra_data None? Is there an example/test for that? Should it be something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't come across any instances where extra_data
is None
, but I added a check as a precaution. I couldn't find well-documented handling for the submessages API, so I wanted to be thorough.
721a9e5
to
88719fb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rsashank Thanks for the update! A few minor checks left
The remaining priority for me here would be consistency of the typing names you've used so far, eg. avoiding Any
and using SubMessage
across various files instead of it (or Dict[str, Any]
).
The other small change I saw was you are now using an or []
form in the widget processing - can we just use []
in the .get
?
The message output looks fine for now.
We could improve the definition of SubMessage
in a followup PR, since that would allow work on the other widget PR sooner.
Similarly, the typing of the processed widget data could be a followup improvement.
tests/widget/test_widget.py
Outdated
"content": '{"widget_type": "todo", "extra_data": ' | ||
'{"task_list_title": "Today\'s Tasks", "tasks": [{"task": ' | ||
'"Write code", "desc": ""}, {"task": "Sleep", "desc": ""}]}}', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes, a single long line would not be great for this, I was more thinking of making it more clearer to read again.
For example, ensuring the value was more distinct from the submessage keys:
"content": '{"widget_type": "todo", "extra_data": ' | |
'{"task_list_title": "Today\'s Tasks", "tasks": [{"task": ' | |
'"Write code", "desc": ""}, {"task": "Sleep", "desc": ""}]}}', | |
"content": ( | |
'{"widget_type": "todo", "extra_data": ' | |
'{"task_list_title": "Today\'s Tasks", "tasks": [{"task": ' | |
'"Write code", "desc": ""}, {"task": "Sleep", "desc": ""}]}}' | |
), |
or even adjusting the layout of the string to make this clearer (though this is difficult to do well):
"content": '{"widget_type": "todo", "extra_data": ' | |
'{"task_list_title": "Today\'s Tasks", "tasks": [{"task": ' | |
'"Write code", "desc": ""}, {"task": "Sleep", "desc": ""}]}}', | |
"content": ( | |
'{"widget_type": "todo",' | |
' "extra_data": {"task_list_title": "Today\'s Tasks",' | |
' "tasks": [{"task": "Write code", "desc": ""}, ' | |
'{"task": "Sleep", "desc": ""}]}}' | |
), |
zulipterminal/widget.py
Outdated
from typing import Any | ||
|
||
|
||
def find_widget_type(submessage: Any) -> str: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few points here:
- nit: for types it's fine to stick with typical class type naming, so eg. Submessage or SubMessage, instead of all upper case
- the
Any
that was originally passed in here is the submessages entry of a Message, so it should have the same typing, and can use the same name/type, so can go in api_types - it can also be applied to various other places where you seem to still use
Any
? - as per another comment recently, a typeddict will pin down the valid keys (str) and value types per key, which is much more specific and clean than
Dict[str, Union[int, str]]
- the core structure of
SubMessage
may be common among all the widgets, so we may not needTodoSubMessage
at this point - ... but where it may be useful is if we were to define the different layouts of the content when it is extracted out of json form
zulipterminal/widget.py
Outdated
@@ -15,3 +15,49 @@ def find_widget_type(submessage: Any) -> str: | |||
return "unknown" | |||
else: | |||
return "unknown" | |||
|
|||
|
|||
def process_todo_widget(todo_list: Any) -> Tuple[str, Dict[str, Dict[str, Any]]]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is improved 👍
To clarify my point re the processed data, as per other comments, using a TypedDict (or class or dataclass) enables the str to be constrained to what you expect, as well as the value types for each key :)
@rsashank We may need some followups on widget messages, such as limiting direct editing? I'm not sure what else may be necessary at this point. |
@neiljp Thanks for the feedback! Updated this PR 👍
Isn't editing on widget messages already restricted? I could be mistaken, but I can't seem to edit. |
Added find_widget_type to identify the type of widget present in the message (e.g., polls, todo, etc.). Added test.
@rsashank Thanks for the update; this looks good to go now - the remaining elements can be fine-tuned later 🎉 |
Added process_todo_widget function to process submessages containing a todo widget. Returns the title of the widget along with tasks and their state (completed/uncompleted) as a dict. Tests added.
Added _handle_submessage_event method to Model to handle new changes to widgets (polls, todo etc.). Tests added.
@rsashank You should be able to confirm using |
I've started #zulip-terminal>Widget followup work to discuss what seem to be the outstanding points after this work. |
What does this PR do, and why?
Adds support for TODO Widget
External discussion & connections
topic
How did you test this?
Self-review checklist for each commit
Visual changes