<html>
<head>
<meta charset="utf-8">
<meta name="google" content="notranslate">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<link rel="icon" type="image/png" sizes="32x32" href="twitgrid-icon-32.png">
<link rel="apple-touch-icon" sizes="512x512" href="twitgrid-icon-512.png">
<meta property="og:url" content="https://vilimpoc.org/research/TwitGrid">
<meta property="og:title" content="TwitGrid Live Demo">
<meta property="og:description" content="A simple, broadsheet layout for Twitter feeds, with keyboard navigation.">
<meta property="og:image" content="https://vilimpoc.org/research/TwitGrid/twitgrid-social.png">
<meta property="og:image:width" content="1280">
<meta property="og:image:height" content="720">
<meta property="og:locale" content="en_US">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@vilimpoc">
<meta name="twitter:creator" content="@vilimpoc">
<meta property="twitter:image" content="https://vilimpoc.org/research/TwitGrid/twitgrid-social.png">
<meta property="twitter:title" content="TwitGrid Live Demo">
<meta property="twitter:description" content="A simple, broadsheet layout for Twitter feeds, with keyboard navigation.">
<meta name="twitter:dnt" content="on">
<title>TwitGrid: Because TweetDeck is a mess at times.</title>
<style>
	/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}

	/*
		Rationale:

		Built this because I was tired of infinite scrolling a tiny block in a massive widescreen monitor, 
		and the way the normal Twitter UI surfaces new content.
		
		I wanted a broadsheet view of the handles I actually care about.
		
		Usage:

		Page Up / Page Down keys to scroll quickly by full-viewport height.
		Mouse Wheel Up / Down works, but more for up than down (depending on whether scrolling an iframe or not).

		Issues:

		(fixed) Scrolling with the mouse wheel causes two full vertical pages to be scrolled
		if scroll-snap-type is assigned to 'html' tag.

		(fixed) And, if you single-click on the window's scrollbar, the tab crashes if 
		scroll-snap-type assigned to 'html' tag, likely because height is 0 or 
		some kind of divide by zero issue.

		(out of my control) Users tweets from protected feeds will not show up using the widget, even
		if you are logged into Twitter. Not much we can do about that.

		Refs:

		Used first: https://css-tricks.com/practical-css-scroll-snapping/#article-header-id-5 (interestingly, the StackOverflow fixes the "body won't work" issue)
		Then added: https://stackoverflow.com/questions/8150191/disable-elastic-scrolling-in-safari/16327467#16327467 (by firing the scrolling context on document.body instead of window)
		https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-snap-type
	*/
	html {
		font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
		line-height: 1.5;
	}

	html,
	body {
		height: 100%;
		width: 100%;
		overflow: hidden;
	}

	body  { scroll-snap-type: y mandatory; background-color: rgb(41, 47, 51); }
	.page { scroll-snap-align: start; }

	/* https://css-tricks.com/snippets/css/a-guide-to-flexbox/ */
	.twits { display: flex; flex-direction: row; }
	.twit  { flex: 0 0 20%; }

	#pagingButtons {
		position: fixed;
		bottom: 2vw;
		right: 3vw;
	}

	@media (orientation: landscape) {
		#pagingButtons .button {
			min-width: 6rem;
			width:     6vw;
			margin:    1rem;
		}
	}

	@media (orientation: portrait) {
		#pagingButtons .button {
			min-width: 6rem;
			width:     8vh;
			margin:    1rem;
		}
	}

	@keyframes fadeIn {
		0%   { filter: brightness(1); }
		100% { filter: brightness(1.5); }
	}

	#pagingButtons .button:hover {
		animation: fadeIn 0.5s ease-in forwards;
	}

	/* https://philipwalton.github.io/solved-by-flexbox/demos/vertical-centering/ */
	.max-w-lg      { max-width: 32rem; }
	.self-start    { align-self: flex-start; }
	.self-end      { align-self: flex-end; }
	.error-flexbox { display: flex; align-items: center; justify-content: center; height: 85vh; }
	.error-box     { text-align: center; color: #fff5f5; }
	.error-icon    { font-size: 11.5rem; }
	.error-text    { font-size: 2.25rem; }
	.error-subtext { font-size: 1.25rem; }
</style>
</head>

<body>
	<!-- Static Way To Do It -->

	<!-- 
	<div class="page">
		<div class="twits">
			<div class="twit"><a class="twitter-timeline" data-width="100%" data-height="100vh" data-theme="dark" data-chrome="nofooter" href="https://twitter.com/vilimpoc"></a></div>
			<div class="twit"><a class="twitter-timeline" data-width="100%" data-height="100vh" data-theme="dark" data-chrome="nofooter" href="https://twitter.com/anandwrites"></a></div>
			<div class="twit"><a class="twitter-timeline" data-width="100%" data-height="100vh" data-theme="dark" data-chrome="nofooter" href="https://twitter.com/checklyhq"></a></div>
			<div class="twit"><a class="twitter-timeline" data-width="100%" data-height="100vh" data-theme="dark" data-chrome="nofooter" href="https://twitter.com/GreatDismal"></a></div>
			<div class="twit"><a class="twitter-timeline" data-width="100%" data-height="100vh" data-theme="dark" data-chrome="nofooter" href="https://twitter.com/tim_nolet"></a></div>
		</div>
	</div>
	<div class="page">
		<div class="twits">
			<div class="twit"><a class="twitter-timeline" data-width="100%" data-height="100vh" data-theme="dark" data-chrome="nofooter" href="https://twitter.com/BVG_Kampagne"></a></div>
			<div class="twit"><a class="twitter-timeline" data-width="100%" data-height="100vh" data-theme="dark" data-chrome="nofooter" href="https://twitter.com/ConanOBrien"></a></div>
			<div class="twit"><a class="twitter-timeline" data-width="100%" data-height="100vh" data-theme="dark" data-chrome="nofooter" href="https://twitter.com/jimmykimmel"></a></div>
			<div class="twit"><a class="twitter-timeline" data-width="100%" data-height="100vh" data-theme="dark" data-chrome="nofooter" href="https://twitter.com/steak_umm"></a></div>
			<div class="twit"><a class="twitter-timeline" data-width="100%" data-height="100vh" data-theme="dark" data-chrome="nofooter" href="https://twitter.com/taylorswift13"></a></div>
		</div>
	</div>
	[... and so on ...]
	-->

	<!-- ES6 Way To Do It -->

	<script>
		// Change this to suit the width of your monitor.
		let columnCount = 5;
		let pageCount   = 0;

		const setColumnCount = c => {
			columnCount  = c;

			let cssWidth = 100.0 / c;
			document.styleSheets[0].insertRule(`.twit { flex: 0 0 ${cssWidth}%; }`, document.styleSheets[0].cssRules.length);
		}

		const makePage = twits => {
			const d = document.createElement('div');
			d.setAttribute('class', 'page');
			d.setAttribute('id', `page-${pageCount}`);

			pageCount++;

			const t = document.createElement('div');
			t.setAttribute('class', 'twits');

			d.appendChild(t);

			twits.forEach(e => {
				t.appendChild(e);
			});

			return d;
		};

		const makeTwit = handle => {
			const d = document.createElement('div');
			d.setAttribute('class', 'twit');

			const a = document.createElement('a');
			a.setAttribute('class', 'twitter-timeline');
			a.setAttribute('data-width', '100%');
			a.setAttribute('data-height', '100vh');
			a.setAttribute('data-theme', 'dark');
			a.setAttribute('data-chrome', 'nofooter');
			a.setAttribute('data-dnt', 'true');
			a.setAttribute('href', `https://twitter.com/${handle}`);

			d.appendChild(a);

			return d;
		};

		const chunk = (arr, size) =>
			Array.from({ length: Math.ceil(arr.length / size) }, (v, i) =>
				arr.slice(i * size, i * size + size)
			);

		const render = (handles, sort=false, node=document.body, columns=columnCount) => {
			const h      = sort ? handles.split(' ').sort((a, b) => { return a.toLowerCase().localeCompare(b.toLowerCase()); }) : handles.split(' ');
			const twits  = h.map(makeTwit);
			const chunks = chunk(twits, columns);

			// console.log(h);
			// console.log(twits);
			// console.log(chunks);

			chunks.forEach(e => {
				node.appendChild(makePage(e));
			});
		};

		// https://dev.to/timhuang/a-simple-way-to-detect-if-browser-is-on-a-mobile-device-with-javascript-44j3
		// Need to do things a little different when on mobile.

		// const isMobile = () => {
		// 	if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
		// 		// true for mobile device
		// 		console.log("mobile device");
		// 		true;
		// 	} else {
		// 		// false for not mobile device
		// 		console.log("not mobile device");
		// 		false;
		// 	}
		// }

		// isMobile();

		// https://stackoverflow.com/a/15203639
		// Getting some weird readings on mobile with this method.

		// Scrolling is different. 
		// window.scrollY == 0 forever for some reason.

		// Ahh, now I understand. On mobile, it acts as though overflow: hidden; and doesn't
		// show a scrollbar for the document itself.

		// So 'top' actually decreases from 0 -> negative values as you "scroll" down.

		// Here's desktop:
		// document.getElementById('page-0').getBoundingClientRect()
		// DOMRect {x: 0, y: 0, width: 1050, height: 1607, top: 0, …}
		// document.getElementById('page-1').getBoundingClientRect()
		// DOMRect {x: 0, y: 1607, width: 1050, height: 1607, top: 1607, …}
		// document.getElementById('page-2').getBoundingClientRect()
		// DOMRect {x: 0, y: 3214, width: 1050, height: 1607, top: 3214, …}

		// Here's mobile:
		// document.getElementById('page-0').getBoundingClientRect()
		// DOMRect {x: 0, y: -1569.1429443359375, width: 979.8095703125, height: 1569.3035888671875, top: -1569.1429443359375, …}
		// document.getElementById('page-1').getBoundingClientRect()
		// DOMRect {x: 0, y: 0.1607142835855484, width: 979.8095703125, height: 1569.3035888671875, top: 0.1607142835855484, …}
		// document.getElementById('page-2').getBoundingClientRect()
		// DOMRect {x: 0, y: 1569.46435546875, width: 979.8095703125, height: 1569.3035888671875, top: 1569.46435546875, …}

		// Ok, I get it now.

		const isInViewport = el => {
			const { top } = el.getBoundingClientRect();

			// The pixel precision on mobile leads to occasional floating-point weirdness
			// in positioning.

			const visible = (-10.0 < top && top < 10.0);
			// console.log(`visible: ${visible}, ${el.getAttribute('id')}`);

			return visible;
		}

		const go = page => {
			document.getElementById(`page-${page}`).scrollIntoView( {behavior: 'smooth'} );
		};

		const clickedUp = e => {
			// Figure out which of the pages is currently in view.
			// Go to previous.

			let page = 0;

			for ( ; page < pageCount; ) {
				if (isInViewport(document.getElementById(`page-${page}`))) break;
				page++;
			}

			if (page > 0) {
				go(page - 1);
			}
		};

		const clickedDown = e => {
			// Figure out which of the pages is currently in view.
			// Go to next.

			let page = 0;

			for ( ; page < pageCount; ) {
				if (isInViewport(document.getElementById(`page-${page}`))) break;
				page++;
			}

			if (page < pageCount - 1) {
				go(page + 1);
			}
		};

		const keyUp = (e) => {
			// Because 'overflow: hidden', we need to implement scrolling keyboard handling
			// ourselves.
			if      (e.key == 'PageUp')   clickedUp();
			else if (e.key == 'PageDown') clickedDown();
			else if (e.key == 'Home')     go(0);
			else if (e.key == 'End')      go(pageCount - 1);
		};

		const showDefaults = () => {
			const handlesTopInterests = 'vilimpoc anandwrites checklyhq GreatDismal tim_nolet';
			const handlesFunStuff     = 'BVG_Kampagne fryuppolice KoreanTravel thingiverse NI_News steak_umm ConanOBrien taylorswift13 jimmykimmel';

			render(handlesTopInterests);
			render(handlesFunStuff, true);
		};

		(() => {
			// Parametrize URL fragment
			// Example: https://vilimpoc.org/research/TwitGrid/#users=vilimpoc,dspillere,gusso,kyran_dale,mihriminaz;users_sorted=therealelp,Fallout,SCMPNews,ADFC_Berlin,tacobell;columns=4
			//
			// users=
			// users_sorted=
			// columns=

			let hash = location.hash.trim().replaceAll('#', '');

			if (hash.length == 0 || (!hash.includes('users') && !(hash.includes('users_sorted')))) {
				showDefaults();
			}
			else {
				try {
					let params = hash.split(';');

					params.forEach(e => {
						const [ key, value ] = e.split('=');

						switch (key)
						{
							case 'columns':
								let newColumnCount = Math.floor(value);
								setColumnCount(newColumnCount > 0 ? newColumnCount : 1);
								break;
						}
					})

					params.forEach(e => {
						const [ view, handles ] = e.split('=');
						const sorted = view.includes('_sorted') ? true : false;

						if (view.startsWith('users')) {
							render(handles.replaceAll(',', ' '), sorted);
						}
					});
				}
				catch {
					showDefaults();
				}
			}

			// Handle global keyboard shortcuts.
			window.addEventListener('keyup', keyUp, false);

			window.onhashchange = () => {
				window.location.reload();
			};

			// Disable auto-scroll to last viewed position, also, y-location 0 seems to mean
			// something different on mobile when paging, it's the top of the currently-viewed
			// page.
			//
			// This doesn't quite work because of Twitter loading in iframes. Haven't found
			// quite the right call.
			window.onbeforeunload = () => {
				// const { top } = document.getElementById('page-0').getBoundingClientRect();
				window.scrollTo(0, 0);
			};
		})();
	</script>
	<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
	<noscript>
		<div class="error-flexbox" id='error'>
			<div class="self-start"></div>
			<div class="max-w-lg error-box">
				<div class="error-icon">🤦🏻‍♂️</div>
				<div class="error-text">Javascript Disabled!</div>
				<div class="error-subtext">TwitGrid won't work without it.</div>
			</div>
			<div class="max-w-lg self-end"></div>
		</div>
	</noscript>
	<div class="error-flexbox" id='loading'>
		<div class="self-start"></div>
		<div class="max-w-lg error-box">
			<div class="error-icon">⌛</div>
			<div class="error-text">Loading...</div>
			<div class="error-subtext">If you have an Ad Blocker or Strict
			Tracking Prevention enabled on Firefox or Edge, this may not
			work.<p/>Chrome of course, by default, leaks like a sieve.</div>
		</div>
		<div class="max-w-lg self-end"></div>
	</div>
	<div id="pagingButtons">
		<!-- https://css-tricks.com/forums/topic/how-to-make-an-svg-fit-to-the-parent-container-100/#post-159784 -->
		<div class="button" onclick="clickedUp()">
			<svg viewBox="0 0 39.6875 39.6875">
				<g transform="translate(-57.235313,-56.668976)">
					<circle r="19.84375" cy="76.512726" cx="77.079063"
						style="fill:#ffdc81;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:49.28266144;stroke-opacity:1;paint-order:normal" />
					<path d="M 64.869589,83.272279 77.07906,71.062809 89.288537,83.272279"
						style="fill:none;stroke:#643100;stroke-width:3.4395833;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
				</g>
			</svg>
		</div>
		<div class="button" onclick="clickedDown()">
			<svg viewBox="0 0 39.6875 39.6875">
				<g transform="translate(-57.235313,-56.668976)">
					<circle r="19.84375" cy="76.512726" cx="77.079063"
						style="fill:#ffdc81;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:49.28266144;stroke-opacity:1;paint-order:normal" />
					<path d="m 64.86959,70.811518 12.20948,12.20947 12.20947,-12.20947"
						style="fill:none;stroke:#643100;stroke-width:3.4395833;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
				</g>
			</svg>
		</div>
	</div>
</body>
</html>