̶I̶M̶P̶R̶O̶V̶I̶N̶G̶ THE PLAIN TEXT EXPERIENCE ON MOBILE AND BROWSERS Now you've got me complaining! Over the past 2 years I find this writing medium has gotten a generally favorable sentiment. The raw, imageless, textual stream of consciousness creates an intimacy much like a letter. This relationship ebbs and flows, where there are good days, bad days, and quiet days - the days where you expect the other(s) to write back, but there's no interest... :'( But between all this are those damn people who say "but I can't click the link". And that's the fricking worst of it all. Ho.lee.cow. Aurhhughhh! More often than not on any web-based piece I get a few people who say they can't read it on mobile (despite browsers requesting to transform it into "simple view"), or they can't click the links in the plain text, or that they wish the text would reflow (which would break the formatting of the original piece). They are valid points, and points which I wish would be considered by the browser vendors for plain-text. As we know that could take ages and we're developers god dammit! So with that... Introducing a plain-text viewer compatible with A̵n̵d̵r̵o̵i̵d̵,̵ ̵i̵O̵S̵,̵ Chrome, Firefox. It's called TextView. Wowwa. It's 100% free. It's 100% open source. Its source code is 100% included in this piece. Now go, lovers of text, and read more! Chrome Web Store: https://chrome.google.com/webstore/detail/textview/nagkoaknnibhiifbcgbgkocjleckgaen Add-ons for Firefox: https://addons.mozilla.org/en-CA/firefox/addon/textview/ ̶G̶o̶o̶g̶l̶e̶ ̶P̶l̶a̶y̶ ̶S̶t̶o̶r̶e̶:̶ ̶A̶p̶p̶l̶e̶ ̶A̶p̶p̶ ̶S̶t̶o̶r̶e̶:̶ ̶N̶o̶ ̶a̶c̶c̶e̶s̶s̶ ̶t̶o̶ ̶A̶p̶p̶l̶e̶ ̶e̶c̶o̶s̶y̶s̶t̶e̶m̶ ̶t̶o̶ ̶d̶e̶p̶l̶o̶y̶.̶ ̶ Hold on a minute... I thought you were fixing things? Well it turns out Chrome on Android handles text files on its own and there is no way to tell it to defer .txt to another program. The only option is to download the file and then select "open with" which really kills the experience. I had written a very simple Android application and spent about 2-3 days trying to debug "Intent Filters", but now I've learned it's useless. Regardless if someone wants to maybe try to hack on it themselves, the source is available (following the source to the Web extension). If Chrome on Android just supports web extensions this would be a non-problem on all platforms. At least Firefox users can stick their tongues out a bit further at those Chrome users! They'll be able to enjoy the extension on both desktop and mobile. With that I feel pretty defeated by the state of affairs. The only other solution I can think of forces writers to install some sort of nginx module that detects mobile browsers and returns a "html-ified" text file. And for me that's pretty much a non-starter because the solution needs to be universal, not "per author". At this point all I can hope for is someone from the Chrome on Android team to see this and propose that Chrome requests if the user would like to open txt files with another app first, rather than always opening them itself. So yeah, I've improved the experience, but only for some people, which sucks. P.S. Sorry Apple consumers, maybe the react native application will work better for you but you'll have to try on your own... --------------------------------Tear off here----------------------------------- #!/bin/sh # SOURCE CODE TO WEB EXTENSION # # Notes to Firefox users: change manifest version to 2 to properly build. # mkdir -p TextView/extension; tee "TextView/extension/content-script.js" << "EOF" const rss = document.querySelector('rss'); if (rss) { for (const link of document.querySelectorAll('link')) { link.addEventListener('click', function() { window.open(this.textContent); }); link.style = 'text-decorator: underline;'; } } else { const text = document.body.innerText; const regexpLinks = /http[s]?:\/\/[^;, \n\t]*/ const regexpLinksAll = /http[s]?:\/\/[^;, \n\t]*/g const links = [...text.matchAll(regexpLinksAll)] const pieces = text.split(regexpLinks) const combined = pieces.reduce((acc, text, index) => { acc.push(text); links[index] && acc.push(links[index]); return acc; }, []) const newText = '
' +
  combined
    .map((piece) => {
      if (!regexpLinks.test(piece)) {
        return piece;
      }
      return `${piece}`;
    })
    .join('')
  + '
'; document.body.innerHTML = newText; } EOF tee "TextView/extension/manifest.json" << "EOF" { "name": "TextView", "action": {}, "manifest_version": 3, "version": "1.0", "description": "Makes links in plain text one-click to open.", "content_scripts": [ { "matches": ["https://*/*.txt", "http://*/*.txt", "http://*/*.xml", "https://*/*.xml"], "css": [], "js": ["content-script.js"] } ] } ---------------------------------Tear off here---------------------------------- #!/bin/sh # SOURCE CODE TO BROKEN ANDROID APP USING EXPO # mkdir -p TextView/mobile; tee "TextView/mobile/App.json;" << "EOF" import { useCallback, useState, useEffect } from 'react'; import { StyleSheet, Text, SafeAreaView, ScrollView, StatusBar, View, Linking } from 'react-native'; export default function App() { const [url, setUrl] = useState('') const [text, setText] = useState('') const fetchText = useCallback((url) => { fetch(url) .then((res) => res.text()) .then(setText, url) }, [setText]) useEffect(() => { setUrl(Linking.getInitialURL()); }, [setUrl]) useEffect(() => { Linking.addEventListener('url', setUrl) return () => Linking.removeEventListener('url', setUrl); }, [setUrl]); useEffect(() => { if (!url) { return; } fetchText(url); }, [url]) const regexpLinks = /http[s]?:\/\/[^;, \n\t]*/ const regexpLinksAll = /http[s]?:\/\/[^;, \n\t]*/g const links = [...text.matchAll(regexpLinksAll)] const pieces = text.split(regexpLinks) const combined = pieces.reduce((acc, text, index) => { acc.push(text); links[index] && acc.push(links[index]); return acc; }, []) return ( { url } { combined.map((piece, index) => !regexpLinks.test(piece) ? { piece } : Linking.openURL(piece.toString())}> { piece } ) } ); } const styles = StyleSheet.create({ container: { backgroundColor: '#fff', flex: 1, alignItems: 'center', paddingTop: (Platform.OS == "android" ? StatusBar.currentHeight : 0) + 16 }, scroll: { flex: 1 }, text: { fontFamily: 'monospace', fontSize: 8, flex: 1 }, link: { textDecorationLine: 'underline', fontFamily: 'monospace', fontSize: 8, flex: 1 } }); EOF tee "TextView/mobile.app.json" << "EOF" { "expo": { "name": "TextView", "slug": "TextView", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#ffffff" }, "updates": { "fallbackToCacheTimeout": 0 }, "assetBundlePatterns": [ "**/*" ], "ios": { "supportsTablet": true }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#FFFFFF" }, "package": "com.lenfalken.TextView", "intentFilters": [ { "action": "VIEW", "data": [ { "scheme": "content", "mimeType": "text/plain", "pathPattern": "*.txt" }, { "scheme": "file", "mimeType": "text/plain", "pathPattern": "*.txt" } ], "category": [ "BROWSABLE", "DEFAULT" ] } ] }, "web": { "favicon": "./assets/favicon.png" } } } EOF tee "TextView/mobile/eas.json" << "EOF" { "cli": { "version": ">= 0.53.0" }, "build": { "preview": { "android": { "buildType": "apk" } }, "preview2": { "android": { "gradleCommand": ":app:assembleRelease" } }, "preview3": { "developmentClient": true }, "production": {} } } EOF