├── 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 | 
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 | [](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})}();
--------------------------------------------------------------------------------