├── LICENSE ├── README.md ├── dist └── roam_showtime-1.1-fx.xpi ├── img ├── banner.png ├── icon.ai ├── icon.png ├── icon_128.png ├── icon_48.png ├── screenshot.png └── screenshot_detail.png ├── manifest.json ├── showtime.js └── underscore-min.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tomas Fiers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Roam ShowTime 2 | 3 | - 🚀 Update 2022. A subset of this functionality is now built-in in Roam. (Hover over a bullet to see the last edited time). 4 | - ⚠ Update Jan 2021. Another Roam update broke this plugin. I have no plans to update this plugin further. PRs are welcome though. 5 | 6 | --- 7 | 8 | ![logo](img/icon_128.png) 9 | 10 | Browser extension to show block creation & edit times on [Roam Research](https://roamresearch.com). 11 | 12 | What it looks like: 13 | 14 | 15 | 16 | This is useful when reviewing how much time you spent on a topic/thought/task. 17 | 18 | 19 | ### Instructions for use 20 | 21 | - Toggle the time display using `ctrl-c, ctrl-s`. 22 | - Note that this shortcut changed (it used to be `ctrl-c, ctrl-x`). 23 | - The first entry is the creation time, the second the time of last edit. 24 | - If those are the same (ignoring seconds), only one is displayed 25 | - For times more than 24 hours in the past, the full date is displayed. 26 | 27 | Note that the displayed times concern the block text only 28 | (and not any descendant blocks). 29 | 30 | ⏱ When a lot of blocks are loaded on the page, the app will start to respond 31 | slowly. Toggling off the time display while you are interacting with blocks 32 | will then help. 33 | 34 | 🎨 If you use custom CSS on Roam that drastically changes the layout, this 35 | plugin might well clash with it. Smaller theming changes should be fine. 36 | 37 | 38 | ### Installation 39 | 40 | [![chrome webstore badge](https://storage.googleapis.com/web-dev-uploads/image/WlD8wC6g8khYWPJUsQceQkhXSlv1/UV4C4ybeBTsZt43U4xis.png)](https://chrome.google.com/webstore/detail/roam-showtime/ojcaheglgnbmphkdppihchfodgpbebhp) 41 | 42 | - Installation from the Chrome webstore auto-updates. 43 | (Note however that there's a review delay whenever a new version is uploaded to the webstore). 44 | - For Firefox, download the XPI file in the `dist` directory (or on the Releases tab on GitHub). 45 | Then install [as follows](https://extensionworkshop.com/documentation/publish/distribute-sideloading/#install-addon-from-file). 46 | This installation will not auto update. 47 | (There is no auto-updating entry for this plugin on Addons.Mozilla.Org (yet): "Mozilla policy 48 | doesn't allow listings for add-ons for Roam Research because the site is limited access"). 49 | - When you want to install directly from source, 50 | here are the instructios [for Chrome](https://stackoverflow.com/a/24577660/2611913) 51 | and [for Firefox](https://extensionworkshop.com/documentation/develop/temporary-installation-in-firefox/). 52 | 53 | 54 | ### How it works 55 | 56 | It's based on the feature described in [this tweet](https://twitter.com/Conaw/status/1265253941727465476): 57 | > If you hit `C-c C-x` you'll get an edit icon for every block, with 58 | > `data-create-time` and `data-edit-time` [attributes] 59 | > – @Conaw (May 26, 2020) 60 | 61 | This extension 62 | - listens for DOM mutations (e.g. collapsing or editing any block); 63 | - [throttle](https://underscorejs.org/#throttle)s these events (so that the 64 | extension code does not run too often); 65 | - on each (throttled) DOM mutation, checks whether the document contains any 66 | divs with a `data-edit-time` attribute.. 67 | - ..and if so, adds absolutely positioned 68 | divs to the left of each bullet, with formatted time strings taken from the 69 | `data-..-time` attributes. 70 | - When the sidebar is open, some left-padding is added to both the main 71 | container and the sidebar, so that the timestamps fit on screen. 72 | -------------------------------------------------------------------------------- /dist/roam_showtime-1.1-fx.xpi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfiers/RoamShowTime/1f2d3c3238fec90d9adebbd1cf3fe7a55b1733f8/dist/roam_showtime-1.1-fx.xpi -------------------------------------------------------------------------------- /img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfiers/RoamShowTime/1f2d3c3238fec90d9adebbd1cf3fe7a55b1733f8/img/banner.png -------------------------------------------------------------------------------- /img/icon.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfiers/RoamShowTime/1f2d3c3238fec90d9adebbd1cf3fe7a55b1733f8/img/icon.ai -------------------------------------------------------------------------------- /img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfiers/RoamShowTime/1f2d3c3238fec90d9adebbd1cf3fe7a55b1733f8/img/icon.png -------------------------------------------------------------------------------- /img/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfiers/RoamShowTime/1f2d3c3238fec90d9adebbd1cf3fe7a55b1733f8/img/icon_128.png -------------------------------------------------------------------------------- /img/icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfiers/RoamShowTime/1f2d3c3238fec90d9adebbd1cf3fe7a55b1733f8/img/icon_48.png -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfiers/RoamShowTime/1f2d3c3238fec90d9adebbd1cf3fe7a55b1733f8/img/screenshot.png -------------------------------------------------------------------------------- /img/screenshot_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfiers/RoamShowTime/1f2d3c3238fec90d9adebbd1cf3fe7a55b1733f8/img/screenshot_detail.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Roam ShowTime", 4 | "description": "Show block creation & edit times on RoamResearch.com", 5 | "version": "1.1", 6 | "author": "Tomas Fiers (https://tomasfiers.net)", 7 | "homepage_url": "https://github.com/tfiers/RoamShowTime", 8 | "content_scripts": [ 9 | { 10 | "matches": [ 11 | "https://roamresearch.com/*" 12 | ], 13 | "js": [ 14 | "underscore-min.js", 15 | "showtime.js" 16 | ] 17 | } 18 | ], 19 | "icons": { 20 | "48": "img/icon_48.png", 21 | "128": "img/icon_128.png" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /showtime.js: -------------------------------------------------------------------------------- 1 | const DOMMutationListener = _.throttle(updateTimeDivs, 200) 2 | const observer = new MutationObserver(DOMMutationListener) 3 | const startObservingDOM = () => observer.observe(document, { childList: true, subtree: true }) 4 | const stopObservingDOM = () => observer.disconnect() 5 | 6 | startObservingDOM() 7 | 8 | const TIME_DIV_CLASS = 'roam-plugin-showtime' 9 | const METADATA_DIV_QUERY = 'div[data-edit-time]' 10 | 11 | function updateTimeDivs() { 12 | // Disable listening for DOM mutations while we mutate the DOM. 13 | stopObservingDOM() 14 | removeOldTimeDivs() 15 | resetContainerPadding() 16 | const sidebarOpen = (document.querySelector("div#roam-right-sidebar-content") != null) 17 | const inCcCxMode = (document.querySelector(METADATA_DIV_QUERY) != null) 18 | // Only add timedivs when in C-c C-x mode 19 | if (inCcCxMode) { 20 | if (sidebarOpen) { 21 | // With the sidebar open, our timedivs would be cutoff. So we make 22 | // some room for them. 23 | setContainerPadding() 24 | } 25 | // Add a div to each roam block. 26 | addTimeDivs() 27 | } 28 | startObservingDOM() 29 | } 30 | 31 | function setContainerPadding() { 32 | const mainContainer = document.querySelector('div.roam-center>div') 33 | if (mainContainer != null) { 34 | mainContainer.style.paddingLeft = "calc((100% - 520px) / 2)" 35 | } 36 | const sideContainer = document.querySelector('div#roam-right-sidebar-content') 37 | if (sideContainer != null) { 38 | sideContainer.style.position = "relative" // Make absolute timedivs move with scroll. 39 | sideContainer.style.paddingLeft = "7em" 40 | } 41 | } 42 | 43 | function resetContainerPadding() { 44 | const mainContainer = document.querySelector('div.roam-center>div') 45 | if (mainContainer != null) { 46 | mainContainer.style.paddingLeft = "calc((100% - 800px) / 2)" 47 | } 48 | const sideContainer = document.querySelector('div#roam-right-sidebar-content') 49 | if (sideContainer != null) { 50 | sideContainer.style.position = "static" 51 | sideContainer.style.paddingLeft = "0" 52 | } 53 | } 54 | 55 | function addTimeDivs() { 56 | const roamBlocks = document.getElementsByClassName('roam-block-container') 57 | Array.from(roamBlocks).forEach(block => { 58 | // Get block creation & last-edited times 59 | const metaDataDiv = block.querySelector('div[data-edit-time]') 60 | let createTime = extractTimeFrom(metaDataDiv, "data-create-time") 61 | const editTime = extractTimeFrom(metaDataDiv, "data-edit-time") 62 | // Sometimes, creation time is missing 63 | if (createTime == null) { 64 | createTime = editTime 65 | } 66 | // Make a human-readable time string 67 | const timeText = makeText(createTime, editTime) 68 | // Add text to a new div and style it. 69 | const timeDiv = document.createElement('div') 70 | timeDiv.setAttribute('class', TIME_DIV_CLASS) 71 | timeDiv.textContent = timeText 72 | style(timeDiv) 73 | position(timeDiv) 74 | // Add new div to Roam block container div. 75 | block.appendChild(timeDiv) 76 | // Bring collapse/expand arrows to front, above timedivs. 77 | const controlsDiv = block.querySelector("div.controls") 78 | controlsDiv.style.zIndex = 1 79 | }) 80 | } 81 | 82 | function removeOldTimeDivs() { 83 | const oldTimeDivs = document.getElementsByClassName(TIME_DIV_CLASS) 84 | Array.from(oldTimeDivs).forEach(div => div.remove()) 85 | } 86 | 87 | function extractTimeFrom(metaDataDiv, attributeName) { 88 | const timestamp = metaDataDiv.getAttribute(attributeName) 89 | if (timestamp == null) { 90 | return null 91 | } else { 92 | return new Date(parseInt(timestamp)) 93 | } 94 | } 95 | 96 | function makeText(createTime, editTime) { 97 | let timeDivText 98 | const createTimeText = formatTime(createTime) 99 | const editTimeText = formatTime(editTime) 100 | let combinedTimeText 101 | if (createTimeText == editTimeText) { 102 | combinedTimeText = createTimeText 103 | } else { 104 | combinedTimeText = `${createTimeText} – ${editTimeText}` 105 | } 106 | if (isLessThan24HoursAgo(createTime) && isLessThan24HoursAgo(editTime)) { 107 | timeDivText = combinedTimeText 108 | } else { 109 | const createDateText = formatDate(createTime) 110 | const editDateText = formatDate(editTime) 111 | if (createDateText == editDateText) { 112 | timeDivText = `${createDateText}, ${combinedTimeText}` 113 | } else { 114 | timeDivText = `${createDateText}, ${createTimeText}\n` 115 | + `${editDateText}, ${editTimeText}` 116 | } 117 | } 118 | return timeDivText 119 | } 120 | 121 | function isLessThan24HoursAgo(datetime) { 122 | now = new Date() 123 | interval = now - datetime // in ms 124 | return (interval / 1000) < (24 * 3600) 125 | } 126 | 127 | const formatTime = (datetime) => datetime.toLocaleTimeString( 128 | navigator.language, 129 | { hour: "2-digit", minute: "2-digit" } 130 | ) 131 | 132 | const formatDate = (datetime) => datetime.toLocaleDateString( 133 | navigator.language, 134 | { weekday: "short", month: "short", day: "numeric", year: "numeric" } 135 | ) 136 | 137 | function style(div) { 138 | div.style.fontSize = "0.6em" 139 | div.style.backgroundColor = "#eee" 140 | div.style.borderRadius = "0.4em" 141 | div.style.padding = "0.1em 0.5em" // vertical, horizontal 142 | div.style.whiteSpace = "pre" // Keep linebreak in textContent. 143 | } 144 | 145 | function position(div) { 146 | div.style.position = "absolute" 147 | div.style.transform = "translateX(-100%)" 148 | + "translateX(2.2em)" 149 | + "translateY(0.7em)" 150 | } 151 | -------------------------------------------------------------------------------- /underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.9.1 2 | // http://underscorejs.org 3 | // (c) 2009-2018 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | !function(){var n="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global||this||{},r=n._,e=Array.prototype,o=Object.prototype,s="undefined"!=typeof Symbol?Symbol.prototype:null,u=e.push,c=e.slice,p=o.toString,i=o.hasOwnProperty,t=Array.isArray,a=Object.keys,l=Object.create,f=function(){},h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"==typeof exports||exports.nodeType?n._=h:("undefined"!=typeof module&&!module.nodeType&&module.exports&&(exports=module.exports=h),exports._=h),h.VERSION="1.9.1";var v,y=function(u,i,n){if(void 0===i)return u;switch(null==n?3:n){case 1:return function(n){return u.call(i,n)};case 3:return function(n,r,t){return u.call(i,n,r,t)};case 4:return function(n,r,t,e){return u.call(i,n,r,t,e)}}return function(){return u.apply(i,arguments)}},d=function(n,r,t){return h.iteratee!==v?h.iteratee(n,r):null==n?h.identity:h.isFunction(n)?y(n,r,t):h.isObject(n)&&!h.isArray(n)?h.matcher(n):h.property(n)};h.iteratee=v=function(n,r){return d(n,r,1/0)};var g=function(u,i){return i=null==i?u.length-1:+i,function(){for(var n=Math.max(arguments.length-i,0),r=Array(n),t=0;t":">",'"':""","'":"'","`":"`"},P=h.invert(L),W=function(r){var t=function(n){return r[n]},n="(?:"+h.keys(r).join("|")+")",e=RegExp(n),u=RegExp(n,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=W(L),h.unescape=W(P),h.result=function(n,r,t){h.isArray(r)||(r=[r]);var e=r.length;if(!e)return h.isFunction(t)?t.call(n):t;for(var u=0;u/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var J=/(.)^/,U={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},V=/\\|'|\r|\n|\u2028|\u2029/g,$=function(n){return"\\"+U[n]};h.template=function(i,n,r){!n&&r&&(n=r),n=h.defaults({},n,h.templateSettings);var t,e=RegExp([(n.escape||J).source,(n.interpolate||J).source,(n.evaluate||J).source].join("|")+"|$","g"),o=0,a="__p+='";i.replace(e,function(n,r,t,e,u){return a+=i.slice(o,u).replace(V,$),o=u+n.length,r?a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":t?a+="'+\n((__t=("+t+"))==null?'':__t)+\n'":e&&(a+="';\n"+e+"\n__p+='"),n}),a+="';\n",n.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{t=new Function(n.variable||"obj","_",a)}catch(n){throw n.source=a,n}var u=function(n){return t.call(this,n,h)},c=n.variable||"obj";return u.source="function("+c+"){\n"+a+"}",u},h.chain=function(n){var r=h(n);return r._chain=!0,r};var G=function(n,r){return n._chain?h(r).chain():r};h.mixin=function(t){return h.each(h.functions(t),function(n){var r=h[n]=t[n];h.prototype[n]=function(){var n=[this._wrapped];return u.apply(n,arguments),G(this,r.apply(h,n))}}),h},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(r){var t=e[r];h.prototype[r]=function(){var n=this._wrapped;return t.apply(n,arguments),"shift"!==r&&"splice"!==r||0!==n.length||delete n[0],G(this,n)}}),h.each(["concat","join","slice"],function(n){var r=e[n];h.prototype[n]=function(){return G(this,r.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},h.prototype.valueOf=h.prototype.toJSON=h.prototype.value,h.prototype.toString=function(){return String(this._wrapped)},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}(); --------------------------------------------------------------------------------