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: WIP support for URL previews #63

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft

Conversation

rayes0
Copy link

@rayes0 rayes0 commented Apr 14, 2022

Implements basic support for asynchronous URL previews, currently almost everything is working. Getting the preview, inserting the preview to the proper place, inserting later messages to the proper places, etc.

It can be turned on by the variable ement-room-url-previews, set to nil by default because some homeservers don't support URL previews (probably be best to mention something in the readme about this if it gets merged).

There are two things that currently don't work very well (probably more I haven't found yet though, haven't thoroughly tested this):

  • Type errors when sender headers are enabled coming from somewhere in this block:
    (when ement-room-sender-headers

    I'm not too sure why, all the macros make it hard to debug, though I suspect it is trivial. And everything appears to work fine except that it throws an error. The previews are inserted correctly, and even the headers are inserted correctly.
  • Sometimes there is a race condition when the pretty printer is called in quick succession and the previews haven't had time to be inserted into the EWOC yet causing two previews to be inserted. I'm not sure how to solve this one, it appears how you did it with inserting retro messages was by setting a variable while loading and not starting a process it the variable is set. I'm not sure how that would work with URL previews, especially multiple URLs in the same message. Any help/suggestions would be appreciated :)

Feel free to edit, change, or delete anything you wish :)

@alphapapa
Copy link
Owner

alphapapa commented Apr 14, 2022

Thanks. I like the way you render the previews with the bar to the left, it looks nice.

A few notes:

As I mentioned in the chat room, before this could be accepted, I'd need you to do the FSF copyright assignment, because I plan to submit Ement to GNU ELPA. If you're uninterested or unable to do that, this could still be useful as a proof-of-concept.

For the implementation: Generally, we should avoid adding new types in the EWOC nodes, because that would require changes in several places that check node types and easily lead to new bugs (like the one you noted about sender headers). So rather than inserting a new node for each preview, it would be simpler to handle it like reactions, by updating the message formatter to append any previews to the end of a message event. The preview-getting callback would add the preview data to the event's local slot and invalidate the node in the EWOC, which would cause the message to be reformatted and display the preview at the end. That would also naturally keep the previews with the message content they are for, and it would probably avoid the race condition you mentioned, because fetching previews and displaying them should be idempotent actions.

@alphapapa
Copy link
Owner

As I mentioned in the chat room, what's probably needed is for me to implement a queue system in plz so that requests like this could be queued.

Regarding the current code in this PR: it's not quite what I had in mind. It could be simpler and integrate more naturally into the existing code. But I can't ask you to put more work into this until you've done the FSF CA, otherwise your work could be in vain, and I'll end up having to do it myself anyway. So I don't think I should give more specific feedback until that's done.

@alphapapa alphapapa force-pushed the master branch 2 times, most recently from 12c2abf to 2e072ef Compare April 15, 2022 15:30
@alphapapa
Copy link
Owner

Copy link
Owner

@alphapapa alphapapa left a comment

Choose a reason for hiding this comment

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

Thanks for your work on this. I made some comments on the code.

ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
do (when-let ((inner-data (map-elt data (intern (concat "preview-" (number-to-string i))))))
(unless (looking-at-p "^\n") (insert "\n"))
(insert (ement-room--format-url-preview inner-data)))))
(dolist (preview previews)
Copy link
Owner

Choose a reason for hiding this comment

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

This is looking better. Now it probably should go into its own function, similar to ement-room--format-m.image. And the code that calls it should probably go in ement-room--format-message-body, treating it as a kind of "appendix" to the message body.

Copy link
Author

Choose a reason for hiding this comment

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

I've currently left it how it is, mainly due to the fact that there is already a function named ement-room--format-url-preview. I think the most reasonable way to go about it is integrating the dolist into ement-room--format-url-preview and calling it from ement-room--format-message-body, but just wanted to confirm with you before I do it.

Copy link
Author

Choose a reason for hiding this comment

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

I'll also probably implement the message parsing in this combined function to insert the previews in the correct order rather than just iterating using dolist.

ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
ement-room.el Outdated Show resolved Hide resolved
@rayes0
Copy link
Author

rayes0 commented May 13, 2022

Sorry for not working on this in a while, was mainly waiting for the FSF CA to get done. Finally got it complete the other day.

@alphapapa
Copy link
Owner

Sorry for not working on this in a while, was mainly waiting for the FSF CA to get done. Finally got it complete the other day.

No problem. That's great news. I don't have time for another full review, but I'll make a few comments...

Copy link
Owner

@alphapapa alphapapa left a comment

Choose a reason for hiding this comment

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

Thanks.

@@ -2573,25 +2573,25 @@ Formats according to `ement-room-message-format-spec', which see."
;; TODO: Like other events, pop to a buffer showing the raw reaction events when a key is pressed.
(if-let ((reactions (map-elt (ement-event-local event) 'reactions)))
(cl-labels ((format-reaction
(ks) (pcase-let* ((`(,key . ,senders) ks)
Copy link
Owner

Choose a reason for hiding this comment

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

Please remove any whitespace-only hunks from the patch.


(defun ement-room--preview-urls (event rendered)
"Parse RENDERED body of EVENT for URLs and fetch previews for them."
(if (and (not (alist-get 'url-previews (ement-event-local event)))
Copy link
Owner

Choose a reason for hiding this comment

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

I'm not sure what rendered is here.

"Parse RENDERED body of EVENT for URLs and fetch previews for them."
(if (and (not (alist-get 'url-previews (ement-event-local event)))
rendered)
(let ((room-buffer (alist-get 'buffer (ement-room-local ement-room))))
Copy link
Owner

Choose a reason for hiding this comment

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

If you can, please use pcase-let to destructure when possible. I'm trying to use it consistently (although sometimes using alist-get is still necessary).

(insert rendered)
(goto-char (point-min))
(cl-loop with match = nil
while (setf match (or (text-property-search-forward 'ement-url)
Copy link
Owner

Choose a reason for hiding this comment

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

Why not just do a regexp search for URLs on the message's plain-text body? It will probably be faster, because Emacs's regexp engine is very well optimized, and the code will be much simpler.

(defun ement-room--insert-preview (message related num data)
"Insert link preview for DATA into MESSAGE from node RELATED if it isn't already present.
NUM is the index for this link."
(defun ement-room--insert-preview (message related key data)
Copy link
Owner

Choose a reason for hiding this comment

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

Please write in terms of events rather than messages, because events are the logical unit we work in here.

NUM is the index for this link."
(defun ement-room--insert-preview (message related key data)
"Insert link preview for HTTP response DATA into MESSAGE from node RELATED,
if it isn't already present. KEY will be the key used for inserting."
Copy link
Owner

@alphapapa alphapapa May 13, 2022

Choose a reason for hiding this comment

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

I don't understand what "KEY will be the key used for inserting" means.

As well, "related" could be more clearly written as "related-event" or "related-node", etc.

NUM is the index for this link."
(defun ement-room--insert-preview (message related key data)
"Insert link preview for HTTP response DATA into MESSAGE from node RELATED,
if it isn't already present. KEY will be the key used for inserting."
(cond ((alist-get 'errcode data) (ement-debug "Rate limit in url preview"))
(data (let ((related-node (ement-room--ewoc-node-before ement-ewoc
Copy link
Owner

Choose a reason for hiding this comment

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

Please avoid one-symbol-per-line formatting style. This function has (declare (indent defun)) specified to avoid this. :)

(cl-pushnew (cons 'url-previews nil) (ement-event-local related)))
(unless (alist-get key (map-elt (ement-event-local related) 'url-previews))
(cl-pushnew (cons key data) (map-elt (ement-event-local related) 'url-previews)))
(event (ement-event-local related)))
Copy link
Owner

Choose a reason for hiding this comment

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

If you're binding the event's local alist slot, the binding should be named local, not event--event should be the event struct itself.

But since you need to push to that alist anyway, you probably shouldn't bind it. I may be mistaken, but I don't think setting the bound variable will also set the slot value; you need to set the place form's value (at least, that's been my experience).

(unless (alist-get key (map-elt (ement-event-local related) 'url-previews))
(cl-pushnew (cons key data) (map-elt (ement-event-local related) 'url-previews)))
(event (ement-event-local related)))
(unless (alist-get 'url-previews event)
Copy link
Owner

Choose a reason for hiding this comment

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

Please be consistent: use either map-elt or alist-get. :) (If performance matters, using alist-get will be faster, because map-elt is generic. But see also map-nested-elt, which can make for clearer code than nested alist-gets, and it's fast enough--although I don't think setf works with it, yet.)

(ewoc-invalidate ement-ewoc related-node)))))

(defun ement-room--format-url-preview (data)
"Return a pretty-printed preview for DATA."
"Return a pretty-printed preview for DATA, an alist of parsed JSON from the homeserver."
Copy link
Owner

Choose a reason for hiding this comment

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

Nitpick: To keep the first line of the docstring short, the second clause (which is just supporting information) can go in a second sentence on a second line. (Checkdoc will complain, otherwise (although getting rid of all Checkdoc warnings is sometimes impractical)).

@alphapapa alphapapa added this to the Future milestone Jul 9, 2023
@alphapapa alphapapa self-assigned this Jul 9, 2023
@alphapapa alphapapa added the enhancement New feature or request label Jul 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request waiting
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants