Last active
September 12, 2024 03:47
-
-
Save donaldguy/8b56909e704f2977914ac61ec586c735 to your computer and use it in GitHub Desktop.
app.hey.com: add unread counts and auto-advance to the "Imbox" - very much a work in progress
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Add counts to Hey.com Imbox (and contents) | |
// @run-at document-end | |
// @match https://app.hey.com/* | |
// @grant GM_getValue | |
// @grant GM_setValue | |
const BASE_URL_PATTERN = 'https://app\\.hey\\.com' | |
let UnreadCount = -1; | |
let SeenCount = -999; | |
let CachedUnreadURLs = new Set() | |
let ShouldAutoAdvance = false | |
function countsText() { | |
if (UnreadCount < 0) { | |
return "" | |
} | |
if (SeenCount < 0) { | |
return ` (${UnreadCount})` | |
} | |
return ` (${UnreadCount} / ${UnreadCount + SeenCount})` | |
} | |
class Page { | |
static url_pattern = new RegExp(`${BASE_URL_PATTERN}/.+`); | |
loaded() { return; } | |
} | |
const Imbox = new (class Imbox extends Page { | |
url_pattern = new RegExp(`^${BASE_URL_PATTERN}/$|^${BASE_URL_PATTERN}/imbox`); | |
get(_url) { return this } | |
loaded() { | |
this.postings = document.getElementById("postings") | |
this.selectUnread() | |
if (UnreadCount < 0) { | |
this.insertCountButton("Count Unread", this.scrollToLoadUnread.bind(this)) | |
} else if (SeenCount < 0) { | |
this.insertCountButton("Count Total", this.scrollToLoadAll.bind(this)) | |
} | |
this.processUnread() | |
if (ShouldAutoAdvance) { | |
this.auto_advance() | |
} else { | |
const titleWithCounts = `Imbox${countsText()}` | |
document.getElementsByTagName('h1')[0].innerText = titleWithCounts | |
document.title = titleWithCounts | |
} | |
} | |
auto_advance() { | |
ShouldAutoAdvance = false; | |
this.postings.querySelector("a.permalink").click() | |
} | |
selectUnread() { | |
this.unreadEmailArticles = this.postings.querySelectorAll('article[data-new-for-you="true"]'); | |
} | |
insertCountButton(text, handler) { | |
const sheetActions = document.getElementsByClassName('sheet-actions')[0]; | |
const firstButton = sheetActions.querySelector('a.btn'); | |
this.countButton = document.createElement('a') | |
this.countButton.href = "#" | |
this.countButton.classList.add('btn') | |
this.countButton.classList.add('btn--secondary') | |
this.countButton.innerText = text | |
this.countButton.addEventListener('click', (e) => { | |
e.preventDefault(); | |
handler() | |
}); | |
sheetActions.insertBefore(this.countButton, firstButton) | |
} | |
scrollToLoadUnread() { | |
if (!this.scrollPoller) { | |
this.scrollPoller = setInterval(this.scrollToLoadUnread.bind(this), 300) | |
return | |
} | |
const readEmailLoaded = this.postings.querySelector('article[data-seen="true"]') | |
if (!readEmailLoaded) { | |
const oldestNewEmail = this.unreadEmailArticles[this.unreadEmailArticles.length - 1] | |
oldestNewEmail.scrollIntoView(true) | |
this.selectUnread() | |
return | |
} else { | |
readEmailLoaded.scrollIntoView(true) | |
this.selectUnread() | |
} | |
// if we made it to here, we have an old email in view and fetching | |
// _should_ be done | |
clearInterval(this.scrollPoller) | |
this.scrollPoller = false | |
UnreadCount = this.unreadEmailArticles.length | |
CachedUnreadURLs = new Set() | |
this.countButton.remove() | |
window.scrollTo(0, 0) | |
this.loaded() | |
} | |
scrollToLoadAll() { | |
// XXX: only works if cover is not active; state is in localStorage.peeking | |
// but how to mutate best? | |
if (!this.scrollPoller) { | |
this.scrollPoller = setInterval(this.scrollToLoadAll.bind(this), 800) | |
return | |
} | |
if (UnreadCount > 0) { | |
this.unreadEmailArticles[this.unreadEmailArticles.length - 1].scrollIntoView(true) | |
} | |
const readEmailLoaded = this.postings.querySelectorAll('article[data-seen="true"]') | |
if (!readEmailLoaded) { | |
return | |
} | |
const paginationLinkExists = this.postings.querySelector('.pagination-link') | |
while (paginationLinkExists) { | |
const oldestEmail = readEmailLoaded[readEmailLoaded.length - 1] | |
oldestEmail.scrollIntoView(true) | |
return | |
} | |
// if we made it to here, we have an old email in view and fetching | |
// _should_ be done | |
clearInterval(this.scrollPoller) | |
this.scrollPoller = false | |
SeenCount = readEmailLoaded.length | |
this.countButton.remove() | |
window.scrollTo(0, 0) | |
this.loaded() | |
} | |
processUnread() { | |
for (const article of this.unreadEmailArticles) { | |
CachedUnreadURLs.add(article.querySelector('a.permalink').href) | |
} | |
} | |
})() | |
class Thread extends Page { | |
static url_pattern = new RegExp(`^${BASE_URL_PATTERN}/topics/[^/]+$`); | |
constructor(url) { | |
super() | |
this.url = url | |
this.was_unread = CachedUnreadURLs && CachedUnreadURLs.has(this.url) | |
} | |
static get(url) { return new Thread(url); } | |
loaded() { | |
document.title = `${document.title}${countsText()}` | |
} | |
} | |
// --------- | |
class GMHeyNavigation { | |
constructor(from_page, to_page, why) { | |
this.from_page = from_page | |
this.to_page = to_page | |
this.why = why | |
console.log(this); | |
if (!from_page) { | |
return; | |
} | |
if (to_page.constructor === Thread && to_page.was_unread) { | |
UnreadCount -= 1 | |
SeenCount += 1 | |
CachedUnreadURLs.delete(from_page.url) | |
} | |
if (from_page.constructor === Thread && why == 'unseen') { | |
UnreadCount += 1 | |
SeenCount -= 1 | |
CachedUnreadURLs.add(from_page.url) | |
} | |
if (from_page.constructor === Thread && why == 'status/trashed') { | |
SeenCount -= 1 | |
} | |
//unless its being moved _to_ the Imbox | |
if (from_page.constructor === Thread && why.startsWith('moves')) { | |
SeenCount -= 1 | |
} | |
if (from_page.constructor === Thread && to_page === Imbox && why != 'back' && why != 'unseen') { | |
ShouldAutoAdvance = true | |
} | |
} | |
} | |
// -------- | |
const detectPage = (url) => { | |
for (page of [Imbox, Thread]) { | |
if (page.url_pattern.test(url)) { | |
return page.get(url) | |
} | |
} | |
} | |
let CurrentPage = detectPage(window.location.href); | |
(function() { | |
new GMHeyNavigation( | |
undefined, | |
CurrentPage, | |
'load' | |
); | |
document.addEventListener('turbo:visit', function(event) { | |
var from = CurrentPage | |
var to = detectPage(event.detail.url) | |
var why = 'visit'; | |
if (from.exit_reason) { | |
why = from.exit_reason | |
} else if (to === Imbox && event.detail.action == 'restore') { | |
why = 'back' | |
} | |
new GMHeyNavigation(from, to, why); | |
CurrentPage = to; | |
return true; | |
}); | |
document.addEventListener('turbo:load', function() { | |
CurrentPage.loaded(); | |
}); | |
document.addEventListener('turbo:submit-end', function(event) { | |
if (!event.detail.success) { | |
return true; | |
} | |
if (CurrentPage.constructor === Thread && event.target.action.startsWith(CurrentPage.url)) { | |
CurrentPage.exit_reason = event.target.action.substr(CurrentPage.url.length + 1) | |
} | |
return true; | |
}); | |
})(); |
Posted on reddit (and elaborated known issues, inter alia) here: https://www.reddit.com/r/HeyEmail/comments/1enms2q/sharing_my_userscript_js_arc_boost_imbox/
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For the record, I'm not really sure how I feel about 37signals, in view of e.g. their (then temporarily dba Basecamp Inc) early 2021 "no politics at work" and COVID-safety bullshit.
I've definitely been, as such, a little embarrassed by my
@hey.com
email address very nearly as long as I've head it. But haven't as yet taken the time/energy to figure out a better planBut the $99 I've given them a year so far is probably not exactly my least ethical consumption under capitalism
--
I do kinda like some of the product innovations/ideas in Hey - but I find deeply irritating (each of and especially the combination of) their failure to build ~table stakes workflow functionality AND arbitrary1 refusal to work with any external clients, nor offer an API
Anyway, this is my limited scratch-own-itch attempt to re-inject some of the most glaring omissions
It is neither done nor perfect, but it certainly has helped me catch up after getting behind on my email on several occasions since I wrote it
And this gist seems like a better pin for my Github profile than the ancient gists and ruby that was there til an hour ago.
Footnotes
I simply do not believe there is neither IMAP nor e.g. JMAP in their backends already ; and if there truly isn't, I still think they should build a standards-compliant bridge to whatever data backends are there - if they really want it can be read-only (or for 90% of what I want from it like ... mark-as-read and delete only). ↩