-
-
Notifications
You must be signed in to change notification settings - Fork 114
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
Quote reply support on text-based posts and comments #526
Conversation
91881c2
to
6b4ff16
Compare
components/reply.js
Outdated
const quoteReply = useCallback(() => { | ||
if (!reply) { | ||
setReply(true) | ||
} | ||
let updatedValue | ||
if (formInnerRef.current && formInnerRef.current.values && !formInnerRef.current.values.text) { | ||
updatedValue = quote(item.text) | ||
} else if (formInnerRef.current?.values?.text) { | ||
// append quote reply text if the input already has content | ||
updatedValue = `${replyInput.current.value}\n${quote(item.text)}` | ||
} | ||
if (updatedValue) { | ||
replyInput.current.value = updatedValue | ||
formInnerRef.current.setValues({ text: updatedValue }) | ||
window.localStorage.setItem(`reply-${parentId}-text`, updatedValue) | ||
} | ||
}, [reply, item]) | ||
if (innerRef) { | ||
innerRef.current = { quoteReply } |
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.
Apparently I just re-invented https://react.dev/reference/react/useImperativeHandle lol, I'll refactor to use that
...and here's quote reply selection. Note it's not going to give you full markdown quoted since it's based on the rendered text, not the raw markdown. I think that should suffice. You get full markdown quoted when you quote the entire item instead of a selection cc @ekzyis stacker.news.quote.reply.selection.mov |
291ca7a
to
2e7bda8
Compare
components/action-dropdown.js
Outdated
@@ -8,7 +8,7 @@ export default function ActionDropdown ({ children }) { | |||
} | |||
return ( | |||
<Dropdown className={`pointer ${styles.dropdown}`} as='span'> | |||
<Dropdown.Toggle variant='success' as='a'> | |||
<Dropdown.Toggle variant='success' as='a' onMouseDown={e => e.preventDefault()}> |
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 to prevent clearing a text selection when opening the action dropdown menu, thus enabling quoting selections.
@@ -197,7 +200,7 @@ export default function Comment ({ | |||
/> | |||
) | |||
: ( | |||
<div className={styles.text}> | |||
<div className={styles.text} ref={contentContainerRef}> |
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.
Mark the DOM tree root where the item's text can be found, so we can check to see if a selection is made in the item when doing a quote reply, not just any selection in the entire page.
components/item-info.js
Outdated
@@ -140,6 +142,8 @@ export default function ItemInfo ({ | |||
!item.mine && !item.deletedAt && <DontLikeThisDropdownItem id={item.id} />} | |||
{item.mine && !item.position && !item.deletedAt && | |||
<DeleteDropdownItem itemId={item.id} type={item.title ? 'post' : 'comment'} />} | |||
{(item.parentId || item.text) && onQuoteReply && |
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.
If it has a parent, or if it has text, and if we have a handler for the quote reply action, render the menu item.
components/reply.js
Outdated
const replyInput = useRef(null) | ||
const formInnerRef = useRef() | ||
useImperativeHandle(ref, () => ({ | ||
quoteReply: () => { |
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.
An imperative quoteReply method to be triggered from above in the react tree.
components/reply.js
Outdated
const selection = window.getSelection() | ||
const selectedText = selection.isCollapsed ? undefined : selection.toString() | ||
const isSelectedTextInTarget = contentContainerRef?.current?.contains(selection.anchorNode) | ||
const textToQuote = isSelectedTextInTarget ? selectedText : item.text |
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.
Check to see what we should quote, the entire item, or a selection of text.
@@ -96,7 +122,6 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold | |||
setReply(replyOpen || false) | |||
}, [upsertComment, setReply, parentId]) | |||
|
|||
const replyInput = useRef(null) |
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 was just moved up above for earlier use.
</div>} | ||
</Form> | ||
</div>} | ||
<div className={styles.reply} style={{ display: reply ? 'block' : '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.
Instead of conditionally rendering this div, I am toggling display
CSS because I need the Form
to exist even when the reply isn't shown, for the ref to be used.
omg this looks amazing! (haven't looked at the code yet) maybe we should insert a newline after the quote or is that unergonomic for some reason? |
Thank you! This is one of those areas where react does not shine IMO (imperative code), so while I don't like the code that much, it works better than trying to do it declaratively via props.
Yep, that's a good idea. That way any text you write doesn't get lumped into the quote automatically. |
… just any selected text
823f4d3
to
f75ec5c
Compare
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.
To summarize and it helps me to make sure I understand what's going on:
You create two refs in <Comment>
. One to reference the reply (replyRef
) and one to reference the container holding the content we are replying to (contentContainerRef
).
The contentContainerRef
is simply passed to a <div>
inside <Comment>
. Nothing more to follow there.
Both refs get passed to <Reply>
.
In <Reply>
, replyRef
gets transformed using useImperativeHandle
to only expose the quoteReply
function. That way you can pass replyRef?.current?.quoteReply
as quoteReply
to <ItemInfo>
in <Comment>
. <ItemInfo>
then renders conditionally the menu item for quote item
.
contentContainerRef
is used in quoteReply
to detect if we selected something in the text we're replying to.
In <Reply>
two more refs are created: replyInput
and formInnerRef
.
replyInput
is passed to <MarkdownInput>
which then references the <input>
element (after going through <InputInner>
, too). This one then allows us to reference the existing reply in quoteReply
so we don't overwrite it.
formInnerRef
is passed to <Form>
and then to <Formik>
. That one is required to actually update the text in quoteReply
since it's controlled by formik
.
LGTM! 😄
But while playing around, I think I found a way to make quote item
disappear as a menu item ... then I started recording to reproduce it but I wasn't able to, lol
Will take another look later, but now I have gotten a good overview of what you did :)
You got it! As I mentioned above, the imperative stuff is kind of a mess. I wish this code was simpler but it just isn't. I welcome any simplification suggestions.
I have noticed it sometimes takes a moment for the "quote reply" action to show up when loading a page locally. I figured it was still loading the |
I always wondered if we should make use of more local context. Afaik, we currently only use it on the global level to provide global context. But what if we use it to provide context for individual components, like to make |
I actually didn't realize you could use the same context provider in a nested fashion. That could be useful in this context, no pun intended lol |
Since the reply component already has the item to which it's replying as a prop, local context may only help with the "is the selection part of the item" aspect, which is cool, but we'll still need a ref for the DOM node, so I'm not sure it'll save us much in this case. Just my quick thinking, could be missing something... |
fwiw we also use context to provide item roots to child comments |
All this imperative stuff scares the crap out of me, but it's well written and super useful so we have to let it fly I changed it to use Excellent work! 🚀🚀🚀 |
Thank you for the kind words 🙏 and thank you for the changes to enable mobile support + the other quote reply vector. Nice! |
Closes #516
Add support for a "Quote Reply" action on text-based posts and comments, available in the item info overflow menu.
Demo in 2 parts:
https://github.com/stackernews/stacker.news/assets/128755788/fd7ac334-492a-45b5-b7b1-e7a1545f4e28
https://github.com/stackernews/stacker.news/assets/128755788/8e226e09-b44f-4106-bb2e-1c27dd67a1ab