Skip to content
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

Merged
merged 6 commits into from
Nov 4, 2024
Merged

Add support for TODO Widget on ZT #1549

merged 6 commits into from
Nov 4, 2024

Conversation

rsashank
Copy link
Member

@rsashank rsashank commented Oct 4, 2024

What does this PR do, and why?

Adds support for TODO Widget

External discussion & connections

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
  • It has a commit summary following the documented style (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

Before After Webapp
image image image

@zulipbot zulipbot added the size: XL [Automatic label added by zulipbot] label Oct 4, 2024
@rsashank rsashank force-pushed the todowidget branch 5 times, most recently from ac81d5c to 6587636 Compare October 4, 2024 05:00
Copy link
Collaborator

@neiljp neiljp left a 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.

@neiljp neiljp added the PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback label Oct 5, 2024
@rsashank rsashank force-pushed the todowidget branch 2 times, most recently from 433da5e to daaf808 Compare October 10, 2024 17:56
@rsashank
Copy link
Member Author

@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.

@rsashank rsashank added PR needs review PR requires feedback to proceed and removed PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback labels Oct 13, 2024
Copy link
Collaborator

@neiljp neiljp left a 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.

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))
Copy link
Collaborator

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.

Copy link
Member Author

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.

Copy link
Collaborator

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].

Comment on lines 506 to 513
class SubmessageEvent(TypedDict):
type: Literal["submessage"]

message_id: int

Copy link
Collaborator

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.

Copy link
Member Author

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?

Copy link
Collaborator

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.


if tasks:
for task_id, task_info in tasks.items():
task_status = "[✔ ]" if task_info["completed"] else "[ ]"
Copy link
Collaborator

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it's actually a bit shifted for resolved topics as well
image

Copy link
Collaborator

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?

Copy link
Member Author

@rsashank rsashank Nov 3, 2024

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.

Copy link
Collaborator

@neiljp neiljp Nov 4, 2024

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.

Comment on lines 39 to 41
"content": '{"widget_type": "todo", "extra_data": '
'{"task_list_title": "Today\'s Tasks", "tasks": [{"task": '
'"Write code", "desc": ""}, {"task": "Sleep", "desc": ""}]}}',
Copy link
Collaborator

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.

Copy link
Member Author

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.

Copy link
Collaborator

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:

Suggested change
"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):

Suggested change
"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": ""}]}}'
),

Copy link
Member Author

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 :)

Comment on lines 48 to 49
"content": '{"type":"new_task","key":2,"task":"Eat","desc":"",'
'"completed":false}',
Copy link
Collaborator

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.

Copy link
Member Author

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.

@@ -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]]]:
Copy link
Collaborator

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done :)

Copy link
Collaborator

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 :)

Copy link
Member Author

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.

Comment on lines +32 to +43
if widget.get("widget_type") == "todo":
if "extra_data" in widget and widget["extra_data"] is not None:
Copy link
Collaborator

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?

Copy link
Member Author

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.

@neiljp neiljp added PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback and removed PR needs review PR requires feedback to proceed labels Oct 19, 2024
@neiljp neiljp added this to the Next Release milestone Oct 19, 2024
@rsashank rsashank force-pushed the todowidget branch 3 times, most recently from 721a9e5 to 88719fb Compare October 25, 2024 11:53
@rsashank rsashank added PR needs review PR requires feedback to proceed and removed PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback labels Oct 25, 2024
Copy link
Collaborator

@neiljp neiljp left a 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.

Comment on lines 39 to 41
"content": '{"widget_type": "todo", "extra_data": '
'{"task_list_title": "Today\'s Tasks", "tasks": [{"task": '
'"Write code", "desc": ""}, {"task": "Sleep", "desc": ""}]}}',
Copy link
Collaborator

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:

Suggested change
"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):

Suggested change
"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": ""}]}}'
),

from typing import Any


def find_widget_type(submessage: Any) -> str:
Copy link
Collaborator

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 need TodoSubMessage 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

@@ -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]]]:
Copy link
Collaborator

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 :)

@neiljp neiljp added PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback and removed PR needs review PR requires feedback to proceed labels Nov 1, 2024
@neiljp
Copy link
Collaborator

neiljp commented Nov 1, 2024

@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.

@rsashank
Copy link
Member Author

rsashank commented Nov 3, 2024

@neiljp Thanks for the feedback! Updated this PR 👍

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.

Isn't editing on widget messages already restricted? I could be mistaken, but I can't seem to edit.

@rsashank rsashank added PR needs review PR requires feedback to proceed and removed PR awaiting update PR has been reviewed & is awaiting update or response to reviewer feedback labels Nov 3, 2024
@neiljp neiljp added PR ready to be merged PR has been reviewed & is ready to be merged and removed PR needs review PR requires feedback to proceed labels Nov 4, 2024
Added find_widget_type to identify the type of widget present in the message
(e.g., polls, todo, etc.).

Added test.
@neiljp
Copy link
Collaborator

neiljp commented Nov 4, 2024

@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.
@neiljp
Copy link
Collaborator

neiljp commented Nov 4, 2024

@rsashank You should be able to confirm using git range-commit, but I only updated the commit text slightly before pushing back here.

@neiljp neiljp merged commit d88adae into zulip:main Nov 4, 2024
21 checks passed
@neiljp
Copy link
Collaborator

neiljp commented Nov 4, 2024

I've started #zulip-terminal>Widget followup work to discuss what seem to be the outstanding points after this work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: widgets PR ready to be merged PR has been reviewed & is ready to be merged size: XL [Automatic label added by zulipbot]
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants