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

adds live page preview on hover :) #52

Merged
merged 34 commits into from
May 17, 2020

Conversation

palashkaria
Copy link
Contributor

Draft PR, WIP

  • currently works for [[]] links in text. Working on #, & headings etc.
  • Need to test this with other roam features.
  • Can make the size/styling of the preview customisable? (currently hard-coded)

@Stvad
Copy link
Member

Stvad commented May 10, 2020

would be nice if it were possible to move mouse over popup and scroll that Iframe/etc

also should block references get preview as-well? (I'm not sure about it myself)

@Stvad
Copy link
Member

Stvad commented May 10, 2020

hmm, this seems to somehow add a small lag for when I'm typing things in the block (not trying to do hover preview or anything)

@Stvad
Copy link
Member

Stvad commented May 10, 2020

maybe just because we need to re-render 2 instances of Roam in the same thread?
wonder if this can be done in a background process or something 🤔

@Stvad
Copy link
Member

Stvad commented May 10, 2020

if the reference is close to the sight side of the page the popup can go outside of the screen

@palashkaria palashkaria changed the title adds page preview on hover :) adds live page preview on hover :) May 10, 2020
@palashkaria
Copy link
Contributor Author

@Stvad

  1. cursor into popover for scroll - this can be a little complex -- as we need to figure out when to close etc. The code itself will be complex

  2. popup getting cut off the screen -- I do plan to handle this, but with a minimal impl, but if we want it to be perfect, we might want to look at some small lib, like tippy. This might help us with the first issue too. Will do some research when implementing.

  3. Not sure about the lag -- might be because we have a mouseover handler? I don't think a roam running in an iframe would cause any issues. I also do a dom checks, on mouse over, which might be causing repaints/layout thrashing. Do you have inspect element open when you get the lag? I will try and do a perf profile to see if we can pinpoint the issue.

  4. Preview works by loading an iframe -- for block reference, we load the page?
    Might not make much sense as the context of the data is lost.

Also, currently the toggle does not work -- did not get to fixing it yet.

@Stvad
Copy link
Member

Stvad commented May 10, 2020

  • 1,2 using a lib sounds good to me
  • lag - I don' think it's mouseover - I'm not touching the mouse or anything, just typing the text in the block. Why I think it can be related to Roam in iframe, it's an aggregate of the following:
    • Roam is pretty slow in general.
    • It does re-rendering when you type things.
    • Now you have 2 instances of Roam running in your frontend thread, so you need to do 2x amount of live rendering for each typing event
  • Block references - no, if you construct URL the way you do right now, but putting the block uid vs page uid you'll get a "zoomed in" view of the block

Not quite sure what you mean by toggle?

@palashkaria
Copy link
Contributor Author

@Stvad we are hiding the iframe using opacity/height/width, set to 0; There will be no repaint/relayout happening in that case. We can easily check for this using profiling in chrome's devtools

  • AH, you mean Block references + it's children? Could be done, but for longer lengths it will get tricky.

@palashkaria
Copy link
Contributor Author

palashkaria commented May 10, 2020

I've been using roam with this enabled for some time now.. Don't see any lag, really. Perf profile also shows 60fps frames (mostly)

image

@palashkaria
Copy link
Contributor Author

Will keep a lookout for lag -- should see it if it happens; I have this enabled anyway

@Stvad
Copy link
Member

Stvad commented May 10, 2020

ok, rendering is the wrong word, I suppose, background processing would be more precise.
When the preview is enabled: I have these moments when the "scripting" consumes 100% CPU (if I read this correctly) and the Roam freezes up.

image

the following is without preview enabled:

image

in both cases I'm just typing in the text into the block

@Stvad
Copy link
Member

Stvad commented May 11, 2020

another thing to support here is Markdown aliases [alias]([[Page Name]])

@palashkaria
Copy link
Contributor Author

palashkaria commented May 11, 2020

@Stvad could you send a dump of the log with preview enabled? It can be done by using rightclick-> save
it does look like scripting, but the log will help us pinpoint which script, too

@Stvad
Copy link
Member

Stvad commented May 11, 2020

do you mean console log or saving the captured profile?

@palashkaria
Copy link
Contributor Author

captured profile @Stvad

@Stvad
Copy link
Member

Stvad commented May 11, 2020

put it here: https://file.io/o5rQDqPn

@palashkaria
Copy link
Contributor Author

@Stvad getting a 404 on that URL

@palashkaria
Copy link
Contributor Author

palashkaria commented May 11, 2020

@Stvad I'm, still not getting the profile you are getting.. Maybe it's the size of the page, etc. Can you please check that link? Maybe something got missed while pasting it here

@palashkaria
Copy link
Contributor Author

@Stvad also, I have all other features disabled; only this running. Can you try that too?

@Stvad
Copy link
Member

Stvad commented May 11, 2020

@palashkaria hrm, another attempt: https://file.io/xW8m0Gwn

@Stvad
Copy link
Member

Stvad commented May 11, 2020

oh, it seems those links are 1-time use

@Stvad
Copy link
Member

Stvad commented May 11, 2020

https://send.firefox.com/download/0f95ee924b341ebf/#fk0CxJsBcqEn73JPxpJ9Lw or https://file.io/mtjk1ySO

@palashkaria
Copy link
Contributor Author

palashkaria commented May 11, 2020

Got it, thanks

Also, are you running the other features? or just the live preview?
and what other browser extensions have you got? I'm currently using brave, with minimal extensions & only this feature, I'm not facing this problem as of now.

@Stvad

@palashkaria
Copy link
Contributor Author

palashkaria commented May 11, 2020

Going by the minified source, I see it's triggered from a 'write' function; which seems to be triggered whenever a new block is created; post this, roam seems to do a server sync.

That's why you see this randomly -- it happens when the sync is happening, and the main thread gets busy. This will need further debugging (or atleast a clean repro case). I'm running perf profile in the background to see if I get this

@Stvad

@Stvad
Copy link
Member

Stvad commented May 11, 2020

just fyi - tested in incognito mode, with all the other features disabled - same story.

@Stvad
Copy link
Member

Stvad commented May 11, 2020

yeah sync makes sense as a culprit - it's run not only when new blocks are created, but when you just add this to existing one, which is what I'm doing

@Stvad
Copy link
Member

Stvad commented May 11, 2020

a note - my main DB is rather large (19mb unpacked JSON file), just tried this on the smaller DB - it's far less pronounced, still get small stuttering (with corresponding cpu spikes) here and there, but a lot better then with the main db

@Stvad
Copy link
Member

Stvad commented May 11, 2020

another important thing to confirm is that we're doing the same thing while recording the profiles. As mentioned above - the issues occurs while I'm typing the text into the block (not moving mouse, not creating new blocks or links or anything like it, just continuously enter new text) - are you testing the same scenario?

@palashkaria
Copy link
Contributor Author

I just tried in fresh chrome profile, still nothing.. THe worse I get is this; which is still atleast 30fps

image

Could be due to the db size too, maybe? We should ask other people to test it, if possible

@palashkaria palashkaria requested a review from Stvad May 16, 2020 18:59
}
checkSettingsAndSetupIframe()

browser.runtime.onMessage.addListener(async message => {
Copy link
Member

Choose a reason for hiding this comment

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

can be moved to dispatcher, as it already does these checks/etc. would need to make it be able to trigger multiple functions though.

Copy link
Member

Choose a reason for hiding this comment

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

👀

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Stvad doing this causes the iframe removal to stop working, because of the same instance problem -- on toggle, it reloads this script, resulting in the instance being null.

If used via onMessage, it works fine.

Changes I made (which caused it to break

  • in live-preview, remove this browser.runtime.onMessage.addListener handling.

  • in dispatcher

 ['settings-updated', () => {
        updateShortcuts()
        checkSettingsAndSetupIframeToggle()
    }
  • now on toggle, it does not .destroy() as it does not find the instance.

Comment on lines 25 to 30
let iframeInstance: PreviewIframe | null = null

const setupIframe = (active: boolean) => {
if (!iframeInstance) {
iframeInstance = new PreviewIframe()
}
Copy link
Member

Choose a reason for hiding this comment

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

I think this can potentially be simplified if you are to always create the holding object, but create the actual view/iframe on activate

Copy link
Member

Choose a reason for hiding this comment

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

hmm, that's what you do. I guess alternative consideration is the complexity of managing state if you are to re-use the object. But you seem to be doing a lot of that anyhow..

iframe.style.width = '0'
iframe.style.border = '0'

// styles
Copy link
Member

Choose a reason for hiding this comment

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

nit: comment seems redundant

}

private removeIframe() {
if (this.iframe && document.body.contains(this.iframe)) {
Copy link
Member

Choose a reason for hiding this comment

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

when would those 2 not be true at the same time? (error scenario?) I imagine if that's the case we may still to cleanup the other part? To say it in other words - should this be 2 separate ifs that would cleanup the corresponding thing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we try to remove an iframe which doesn't exist in body, there will be an exception. So both of these need to be true at the same time @Stvad the condition is correct

Copy link
Member

Choose a reason for hiding this comment

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

I meant having something like:

if(document.body.contains(this.iframe)) { //false for undefined/null/etc
    document.body.removeChild(this.iframe)
}
if (this.iframe) this.iframe = null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Stvad typescript will throw an error there

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Stvad will need to check for this.iframe first. Updated

Copy link
Member

Choose a reason for hiding this comment

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

hrm, it's now more complicated then before 🙈.
is typescript error because the this.iframe can still potentially be null?

private getIFrameByUrl(url: string): HTMLIFrameElement | null {
return document.querySelector(`iframe[src="${url}"]`)
}
private getIsIFrameVisibleByUrl(url: string): boolean {
Copy link
Member

Choose a reason for hiding this comment

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

isUrlInPreview?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Stvad this is about being 'visible', not being in preview -- visible is if opacity is set to 1. Maybe isUrlPreviewVisible

Comment on lines 116 to 118
this.isHoveredOutFromTarget(target, relatedTarget, iframe) ||
this.isHoveredOutFromIframe(target, relatedTarget, iframe) ||
!this.isHoveredElementPresentInBody()
Copy link
Member

Choose a reason for hiding this comment

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

make this a function/variable

Comment on lines 120 to 123
this.hoveredElement = null
this.clearPopupTimeout()
this.resetIframeForNextHover(iframe)
this.destroyPopper()
Copy link
Member

Choose a reason for hiding this comment

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

make it into hidePreview function

private resetIframeForNextHover(iframe: HTMLIFrameElement) {
if (iframe) {
if (iframe.contentDocument) {
// scroll to top when removed, so the next popup is not scrolled
Copy link
Member

Choose a reason for hiding this comment

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

should this be using scrollToTopHack ?

Copy link
Contributor Author

@palashkaria palashkaria May 16, 2020

Choose a reason for hiding this comment

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

this is a different scollToTop hack :) extracting to another function

Copy link
Member

Choose a reason for hiding this comment

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

why do they have to be different. would scrolling to the top of html not be enough here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Stvad that is during loading. When a post is loaded, you don't scroll on the html, you scroll on a div inside .roam-center. Need to set the scrolltop of that.

Comment on lines 24 to 26
if (!uid) {
return this.baseUrl().toString()
}
Copy link
Member

Choose a reason for hiding this comment

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

what's use case for this?

Copy link
Contributor Author

@palashkaria palashkaria May 16, 2020

Choose a reason for hiding this comment

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

to get the daily notes page @Stvad

Copy link
Member

Choose a reason for hiding this comment

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

you can just call baseUrl explicitly..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that does not return a string -- don't want to leak .toString() into app code. And we might need the baseUrl as is for some other purpose

Copy link
Member

Choose a reason for hiding this comment

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

not sure what do you mean by leak .toString. you literally use this only to get baseUrl, and there is already function for that 😛
I don't find it intuitive that that this function would silently "succeed" on falsey input =\

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I use this to convert eh baseUrl (which is the class URL) to a string. It's not the same thing. @Stvad Should I add a separate function for this? What should it be called?

By leak toString, I mean exactly that. It needs to be in a function here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added a function getDailyNotesUrl for this @Stvad

Copy link
Member

Choose a reason for hiding this comment

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

hrm. ok 😛

src/ts/utils/settings.ts Show resolved Hide resolved
@palashkaria palashkaria requested a review from Stvad May 16, 2020 21:55

class PreviewIframe {
iframeId = 'roam-toolkit-iframe-preview'
iframe: HTMLIFrameElement | null = null
Copy link
Member

Choose a reason for hiding this comment

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

how about constructing this (and maybe calling activate in general?) as a part of class constructor, so we can can be sure that it's not null and avoid null-checking it everywhere?

Copy link
Member

Choose a reason for hiding this comment

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

as after all, iframe should exist during the whole lifecycle of this class

Comment on lines 70 to 72
if (this.iframe) {
this.iframe = null
}
Copy link
Member

Choose a reason for hiding this comment

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

actually I don't think we need to explicitly assign this to null. as we discard the whole PreviewIframe instance, this would just be garbage collected


private getIFrameByUrl(url: string): HTMLIFrameElement | null {
return document.querySelector(`iframe[src="${url}"]`)
}
Copy link
Member

Choose a reason for hiding this comment

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

a nit, but can the prettier be configured to require at list 1 space between functions? 😛

Copy link
Contributor Author

@palashkaria palashkaria May 17, 2020

Choose a reason for hiding this comment

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

@Stvad Haha, I wish 😅 It is not possible in prettier because of the way prettier works (rewriting your code). See this more more info

It turns out that empty lines are very hard to automatically generate. The approach that Prettier takes is to preserve empty lines the way they were in the original source code
https://prettier.io/docs/en/rationale.html#empty-lines

Copy link
Member

Choose a reason for hiding this comment

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

huh, interesting. May I suggest you configure your IDE to do so then ?😛

Comment on lines 75 to 77
private getIFrameByUrl(url: string): HTMLIFrameElement | null {
return document.querySelector(`iframe[src="${url}"]`)
}
Copy link
Member

Choose a reason for hiding this comment

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

so just occurred to me - why do we need to do this if we already have the reference to the instance as a part of the class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As mentioned, not sure. We get mouse events triggering multiple times, and these are to make sure that we don't add the iframes multiple times. Will not be able to debug and figure out immediately @Stvad


private initPreviewIframe() {
const url = Navigation.getPageUrl()
const existingIframe = this.getIFrameByUrl(url)
Copy link
Member

Choose a reason for hiding this comment

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

when would this happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No clear answer. Can happen, have seen mouse events getting triggered multiple times.

Comment on lines 205 to 209
const visibleIframe = this.getVisibleIframeByUrl(url)
if (visibleIframe) {
// if visible, just return the iframe
return
}
Copy link
Member

Choose a reason for hiding this comment

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

should use class iframe instance

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Stvad will not work -- mouse events get called 3 times

Comment on lines 24 to 26
if (!uid) {
return this.baseUrl().toString()
}
Copy link
Member

Choose a reason for hiding this comment

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

not sure what do you mean by leak .toString. you literally use this only to get baseUrl, and there is already function for that 😛
I don't find it intuitive that that this function would silently "succeed" on falsey input =\

}
checkSettingsAndSetupIframe()

browser.runtime.onMessage.addListener(async message => {
Copy link
Member

Choose a reason for hiding this comment

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

👀

@palashkaria
Copy link
Contributor Author

@Stvad as discussed, we have the problem of the script getting executed multiple times, and so multiple instances being created. Added a check based on ID for now, and simplified some more things in 9afd28e

@palashkaria
Copy link
Contributor Author

palashkaria commented May 17, 2020

might have found the root cause for this issue; we are loading the same contentscript multiple times -- once directly, once in dispatcher, for eg. that's why when I move the code to dispatcher, it causes it to laod another time. Each content script is supposed to be isolated on it's own. See this for more details:
https://stackoverflow.com/questions/26240463/how-do-i-re-use-code-between-content-scripts-in-a-chrome-extension

Also attaching screenshots of fuzzy_date getting loaded two separate times, this way (see stacktrace):

image

image

@Stvad as discussed, we have the problem of the script getting executed multiple times, and so multiple instances being created. Added a check based on ID for now, and simplified some more things in 9afd28e

const url = Navigation.getPageUrl()
const existingIframe = this.getIFrameByUrl(url)
const url = Navigation.getDailyNotesUrl()
const existingIframe = this.getExisitingIframe()
if (existingIframe) {
Copy link
Member

Choose a reason for hiding this comment

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

I hope we'd be getting rid of this now. but if we were not - I think the assignment would have been still required, to ensure that we operate on the appropriate underlying object

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this can be removed after #58 gets merged. I tested by merging with #57 already :)

this.iframe.style.width = '0'
}

private scrollToTopOnMouseOut() {
Copy link
Member

Choose a reason for hiding this comment

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

nit: can you place the scroll to top hacks close to each other and extract commonality between them?

Palash added 3 commits May 18, 2020 02:44
# Conflicts:
#	src/manifest.json
#	src/ts/features/features.ts
move/rename scrollToTop hack
@palashkaria palashkaria requested a review from Stvad May 17, 2020 21:33
} else if (iframeInstance) {
iframeInstance.destroy()
iframeInstance = null
if (active) {
Copy link
Member

Choose a reason for hiding this comment

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

why this?

Copy link
Member

Choose a reason for hiding this comment

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

i.e. why change from previous version of this

@Stvad Stvad merged commit 42e407a into roam-unofficial:master May 17, 2020
Stvad added a commit that referenced this pull request May 17, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants