Grégory PAUL
banner
paulgreg.bsky.social
Grégory PAUL
@paulgreg.bsky.social
My interests are web development, IT security, hacking & climate change.
I work on http://fr.mappy.com & http://bonjour-ratp.fr.
Personal projects on paulgreg.me
I was able to build a MicroMac thanks to @AtTheHackOfDawn awesome work. Details are on his blog post : https://axio.ms/projects/2024/06/16/MicroMac.html ! What an awesome project 🙏 #RP2040 #macintosh #emulation #umac
November 23, 2024 at 11:47 AM
Je suis tombé sur un Minitel. C’est l’occasion de le connecter à un Arduino comme présenté dans @hackablemag 21 pour « jouer » avec ! #hack #minitel #old #90s
November 23, 2024 at 11:46 AM
November 23, 2024 at 11:46 AM
November 23, 2024 at 11:45 AM
My PS3 slim refused to read blu-ray anymore, I’ve ordered a KEM-450AAA laser lens, just replaced it and it worked ! PS3 is working again ! #ps3 #retrogaming #repair
November 23, 2024 at 11:44 AM
I‘m trying to build an ultrasonic levitation device following https://www.youtube.com/watch?v=B4pIJwy_ySY but I found weird to connect +5 V to ground here. What do you think ?
November 23, 2024 at 11:44 AM
My Z80-MBC2 has now an uTerm companion, a VT100-like terminal making it a stand-alone computer with VGA output and a PS2 input for keyboard https://hackaday.io/project/165325-uterm #retrocomputing #z80 #DIY
November 23, 2024 at 11:41 AM
68k-MBC is now finished too ! Collection is now complete : z80-mbc, v20-mbc & 68k-mbc. #diy #retrocomputing #hack https://hackaday.io/project/177988-68k-mbc-a-3-ics-68008-homebrew-computer Thanks @Just4Fun_J4Fun for that awesome design 🙏
November 23, 2024 at 11:40 AM
Thanks to https://t.co/3U3hEYabjc & https://t.co/HO1TauEGhB 🙏, I was able to install RaspberryPi OS (Debian 11) on an old Surface RT (Win 8.1 with IE11 😱 & no more update). Linux is awesome to give second life to old devices ! #Surface #RasbperryPiOs #linux #hardware
November 23, 2024 at 11:40 AM
Found 2 old books which may help me in my retro computing quest (V20-MBC/Z80-MBC/68K-MBC) #pascal #fundamentals #retrocomputing
November 23, 2024 at 11:40 AM
Just finished to solder my christmas gift : a MBC V20, a V20 (8088 + 8080) CPU based homebrew computer https://hackaday.io/project/170924-v20-mbc-a-v20-8088-8080-cpu-homebrew-computer/ #retrocomputing #v20 #CPM #hack #DIY
November 23, 2024 at 11:40 AM
Just discovered cool-retro-term project ! It’s awesome to use it for Z80-MBC2 via minicom ! #fun #retrocomputing #80s https://github.com/Swordfish90/cool-retro-term
November 23, 2024 at 11:39 AM
Was able to recover my first Z80-MBC2 board by de-soldering/resoldering new resistor networks in correct polarity 🎉. Now I have 2 boards, one with IOS « full » (with sdcard and RTC modules) and one with IOS Lite. Time to have fun with CP/M 😀
November 23, 2024 at 11:39 AM
Second attempt to build a Z80-MBC2 is a success ! I think resistor network on my first board are reversed. #z80 #retrocomputing
November 23, 2024 at 11:39 AM
November 23, 2024 at 11:38 AM
I’m now fetching temperature from an old Oregon Scientific sensor and displaying it on my e-ink weather station ! 🎉 https://hackaday.io/project/171910-iot-eink-weather-station Thanks for that awesome library https://github.com/Erriez/ErriezOregonTHN128 #eink #hack #esp32 #RF
November 23, 2024 at 11:35 AM
Ok, got temperatures from an Oregon Scientific sensor with an Arduino Uno using a RF 433 Mhz module and that excellent library https://github.com/Erriez/ErriezOregonTHN128
Next step, replace the Arduino by an ESP32 ! #previousTweet #hack #electronic #RF #arduino
November 23, 2024 at 11:33 AM
Getting temperatures from an old Oregon Scientific sensor using rtl_433 https://github.com/merbanan/rtl_433 and a low cost DVB USB key on GNU/Linux ! It works really great ! Next step : get temperature using an arduino. #hack #electronic
November 23, 2024 at 11:33 AM
Je me demande d’ailleurs si beaucoup de personnes ont ce graphique en tête... Personnellement, ça m’effraie car je ne vois pas comment changer cette situation sans un changement radical (qui ne me semble pas près d’arriver)... #changementClimatique #energiesFossiles
November 23, 2024 at 11:26 AM
Bonjour @Parisjecoute,
Bravo pour la campagne de sensibilisation des déchets à l'école mais pourquoi diable offrir une mini-poubelle en plastique aux enfants ? Mes enfants ne jouent pas avec et elle finira... à la poubelle ! Je trouve cela contre-productif. #écologie #déchets
November 23, 2024 at 11:25 AM
Followed that tutorial to add an external antenna to my esp32 based bluetooth audio receiver. It’s a hack but it works great ! https://t.co/xpMTJPdM9i https://t.co/GIxWIXEJVd #esp32 #antenna #bluetooth #audio #receiver
November 23, 2024 at 11:24 AM
November 23, 2024 at 11:22 AM
Installed Scaphandre on my home server (Shuttle DH310V2 & i5-9400T) to see its power consumption (and so, try to reduce it) https://t.co/PYCvUMwROp #cool #NiceWork #consumption #energy #power #ecology #oss #IT (learn about that on https://t.co/AYKhl0htsZ via @nitot)
November 23, 2024 at 11:11 AM
Any idea why, on http://youtube.com channels pages, is in <body> and not in <head> ? (ex : <a href="https://www.youtube.com/channel/UCD94IYXQINu04R0KxOrhEzA" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link" target="_blank" rel="noopener" data-link="bsky">https://www.youtube.com/channel/UCD94IYXQINu04R0KxOrhEzA</a>) <a href="/hashtag/html" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#html</a> <a href="/hashtag/standard" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#standard</a></div> <div class="mt-2 w-[calc(100%+3.5rem)] -ml-[3.5rem] no-card-link" data-controller="modal"> <img src="https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:2kpmcums7slsctgnponrmtan/bafkreicehabrfglldbfqlbmocvxvtzjl5w2jjqh6xvrgazvqi4spmbluo4@jpeg" data-action="click->modal#openWith" data-large-src="https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:2kpmcums7slsctgnponrmtan/bafkreicehabrfglldbfqlbmocvxvtzjl5w2jjqh6xvrgazvqi4spmbluo4@jpeg" alt="" class="w-full rounded cursor-zoom-in" loading="lazy"> <div data-modal-target="panel" class="hidden fixed inset-0 z-[100]"> <div class="absolute inset-0 bg-black/90" data-action="click->modal#close"></div> <div class="relative z-[101] h-full w-full flex items-center justify-center p-4" data-action="click->modal#stop"> <img data-modal-target="image" alt="" class="max-w-full max-h-full rounded"> <button type="button" aria-label="Close" class="absolute top-4 right-4 inline-flex items-center justify-center w-10 h-10 rounded-full bg-black/60 text-white hover:bg-black/80" data-action="click->modal#close"> <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> </button> </div> </div> </div> <div class="mt-3 text-xs text-gray-500 flex items-center gap-2 whitespace-nowrap overflow-hidden w-[calc(100%+3.5rem)] -ml-[3.5rem]"> <span>November 23, 2024 at 11:09 AM</span> </div> <div class="border-t my-2 w-[calc(100%+3.5rem)] -ml-[3.5rem]"></div> <div class="mt-3 flex items-center justify-between text-gray-600 text-sm w-[calc(100%+3.5rem)] -ml-[3.5rem] no-card-link" data-controller="bluesky-interactions"> <div class="flex items-center justify-between w-[300px] pr-4 no-card-link"> <div class="hidden" data-post-text-uri="at://did:plc:2kpmcums7slsctgnponrmtan/app.bsky.feed.post/3lbme5mwjcs2w"> Any idea why, on <a href="http://youtube.com" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link" target="_blank" rel="noopener" data-link="bsky">http://youtube.com</a> channels pages, <title> is in <body> and not in <head> ? (ex : <a href="https://www.youtube.com/channel/UCD94IYXQINu04R0KxOrhEzA" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link" target="_blank" rel="noopener" data-link="bsky">https://www.youtube.com/channel/UCD94IYXQINu04R0KxOrhEzA</a>) <a href="/hashtag/html" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#html</a> <a href="/hashtag/standard" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#standard</a> </div> <button type="button" class="flex items-center gap-1 hover:text-black no-card-link" data-action="click->bluesky-auth#requireConnection click->bluesky-interactions#reply" data-bluesky-interactions-reply-uri-param="at://did:plc:2kpmcums7slsctgnponrmtan/app.bsky.feed.post/3lbme5mwjcs2w" data-bluesky-interactions-reply-cid-param="bafyreie2kgiikifhd52qz6nsc6aop2wvydnoci5ggec4mvqtfo3hp5rtuq" data-bluesky-interactions-reply-root-uri-param="at://did:plc:2kpmcums7slsctgnponrmtan/app.bsky.feed.post/3lbme5mwjcs2w" data-bluesky-interactions-reply-root-cid-param="bafyreie2kgiikifhd52qz6nsc6aop2wvydnoci5ggec4mvqtfo3hp5rtuq" data-bluesky-interactions-reply-author-name-param="Grégory PAUL" data-bluesky-interactions-reply-author-avatar-param="https://cdn.bsky.app/img/avatar/plain/did:plc:2kpmcums7slsctgnponrmtan/bafkreibq5meslzyuvcl6un75xlp2ullhmaieeylde6pp4qwx7kcx7ajib4@jpeg" data-bluesky-interactions-post-path-param="/bsky/paulgreg.bsky.social/post/3lbme5mwjcs2w" title="Reply" style="pointer-events: auto; cursor: pointer; z-index: 10; position: relative;"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a4 4 0 0 1-4 4H7l-4 4V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"/></svg> </button> <div class="relative no-card-link" data-controller="bluesky-repost-menu" data-bluesky-repost-menu-uri-value="at://did:plc:2kpmcums7slsctgnponrmtan/app.bsky.feed.post/3lbme5mwjcs2w" data-bluesky-repost-menu-cid-value="bafyreie2kgiikifhd52qz6nsc6aop2wvydnoci5ggec4mvqtfo3hp5rtuq" data-bluesky-repost-menu-repost-uri-value="" data-bluesky-repost-menu-is-reposted-value="false" data-bluesky-repost-menu-author-handle-value="paulgreg.bsky.social" data-bluesky-repost-menu-post-text-value="Any idea why, on http://youtube.com channels pages, <title> is in <body> and not in <head> ? (ex : https://www.youtube.com/channel/UCD94IYXQINu04R0KxOrhEzA) #html #standard" style="pointer-events:auto; cursor:pointer;"> <button type="button" class="flex items-center gap-1 hover:text-black " data-action="click->bluesky-repost-menu#toggleMenu" data-bluesky-interactions-target="repostButton" title="Repost"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg> </button> <div class="hidden absolute left-0 bottom-full mb-3 w-48 rounded-2xl border border-white/10 bg-[#1C2333] text-white shadow-xl overflow-hidden z-50" data-bluesky-repost-menu-target="menu"> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-auth#requireConnection click->bluesky-repost-menu#repost"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg> <span>Repost</span> </button> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-auth#requireConnection click->bluesky-repost-menu#quote"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> <span>Quote post</span> </button> </div> </div> <button type="button" class="flex items-center gap-1 hover:text-black no-card-link " data-action="click->bluesky-auth#requireConnection click->bluesky-interactions#toggleLike" data-bluesky-interactions-uri-param="at://did:plc:2kpmcums7slsctgnponrmtan/app.bsky.feed.post/3lbme5mwjcs2w" data-bluesky-interactions-cid-param="bafyreie2kgiikifhd52qz6nsc6aop2wvydnoci5ggec4mvqtfo3hp5rtuq" data-bluesky-interactions-like-uri-param="" data-bluesky-interactions-target="likeButton" title="Like"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 1 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78z"/></svg> </button> </div> <div class="flex items-center gap-4 text-gray-500"> <button type="button" class="flex items-center hover:text-black no-card-link" data-controller="share" data-share-url-value="https://bushdrum.com/bsky/paulgreg.bsky.social/post/3lbme5mwjcs2w" data-share-title-value="Grégory PAUL on Lightnews" data-share-text-value="Any idea why, on http://youtube.com channels pages, <title> is in <body> and not in <head> ? (ex : https://www.youtube.com/channel/UCD94IYXQ…" data-action="click->share#shareSystem" title="Share" style="pointer-events:auto; cursor:pointer;"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12v7a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg> </button> <div class="relative no-card-link" data-controller="bluesky-options" data-bluesky-options-text-value="Any idea why, on http://youtube.com channels pages, <title> is in <body> and not in <head> ? (ex : https://www.youtube.com/channel/UCD94IYXQINu04R0KxOrhEzA) #html #standard" data-bluesky-options-author-handle-value="paulgreg.bsky.social" data-bluesky-options-author-did-value="did:plc:2kpmcums7slsctgnponrmtan" data-bluesky-options-muted-value="false" data-bluesky-options-blocking-uri-value="" data-bluesky-options-own-value="false" data-bluesky-options-delete-url-value="" data-bluesky-options-post-element-selector-value='[data-card-type="bsky-post"]'> <button type="button" class="inline-flex items-center" data-action="click->bluesky-options#toggleMenu" aria-label="Post options"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg> </button> <div class="hidden absolute right-0 bottom-full mb-3 w-60 rounded-2xl border border-white/10 bg-[#1C2333] text-white shadow-xl overflow-hidden" data-bluesky-options-target="menu"> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-options#translate"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h16v16H4z"/><path d="M9 8h6"/><path d="M9 12h6"/><path d="M9 16h6"/></svg> </span> <span>Translate</span> </button> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-options#copyText"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2h-8a2 2 0 0 0-2 2v8"/></svg> </span> <span>Copy post text</span> </button> <div class="border-t border-white/10"></div> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-auth#requireConnection click->bluesky-options#muteAccount"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 9v6h3l5 5V4L7 9H4z"/><line x1="16" y1="9" x2="19" y2="12"/><line x1="19" y1="12" x2="16" y2="15"/></svg> </span> <span data-bluesky-options-target="muteLabel">Mute account</span> </button> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-auth#requireConnection click->bluesky-options#blockAccount"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3h18v18H3z"/><path d="M3 3l18 18"/></svg> </span> <span data-bluesky-options-target="blockLabel">Block account</span> </button> </div> </div> </div> </div> </div> </div> </div> </div> <script> // Prevent navigation when clicking in avatar column area - must run in capture phase before global handler // Only initialize once to avoid duplicate listeners if (!window._avatarColumnNoNavInitialized) { window._avatarColumnNoNavInitialized = true; document.addEventListener('click', function(ev) { try { // Check if target itself has the class OR is inside an element with that class var avatarColumn = (ev.target.classList && ev.target.classList.contains('avatar-column-no-nav')) || (ev.target.closest && ev.target.closest('.avatar-column-no-nav')); if (avatarColumn) { // Only allow navigation if clicking directly on the avatar link itself var isAvatarLink = ev.target.tagName === 'A' && ev.target.href && ev.target.href.indexOf('/author') !== -1; if (!isAvatarLink) { ev.stopPropagation(); ev.stopImmediatePropagation(); ev.preventDefault(); return false; } } } catch(e) {} }, true); // Capture phase - runs BEFORE other handlers } </script> <div class="w-full border-y sm:border bg-white dark:bg-[#0C1220] bluesky-list-card digest-card card-clickable cursor-pointer group" data-href="/bsky/paulgreg.bsky.social/post/3lbmdyf4haz2t" role="link" tabindex="0" data-card-type="bsky-post" onclick="if(!event.target.closest('.no-card-link')){ window.location = this.dataset.href }" onkeydown="if((event.key==='Enter' || event.key===' ') && !event.target.closest('.no-card-link')){ window.location = this.dataset.href }"> <div class="px-4 py-3"> <div class="flex items-start gap-4"> <div class="relative w-10 self-stretch flex justify-center no-card-link avatar-column-no-nav" style="min-height:40px; align-self:stretch" onclick="event.stopPropagation(); event.stopImmediatePropagation(); return false;" onkeydown="if(event.key==='Enter' || event.key===' '){ event.stopPropagation(); event.stopImmediatePropagation(); return false; }"> <a href="/author?bsky_handle=paulgreg.bsky.social" class="no-card-link inline-block flex-shrink-0" aria-label="Grégory PAUL" style="align-self: flex-start;"> <img src="https://cdn.bsky.app/img/avatar/plain/did:plc:2kpmcums7slsctgnponrmtan/bafkreibq5meslzyuvcl6un75xlp2ullhmaieeylde6pp4qwx7kcx7ajib4@jpeg" alt="paulgreg.bsky.social" class="w-10 h-10 rounded-full relative z-10" loading="lazy"> </a> </div> <div class="flex-1 min-w-0 -mt-0.5"> <div> <div class="text-[1.1rem] sm:text-[1.15rem] font-semibold text-black flex items-center gap-1"> <a href="/author?bsky_handle=paulgreg.bsky.social" class="truncate hover:underline text-black no-card-link">Grégory PAUL</a> </div> <div class="text-gray-500 text-sm leading-4 -mt-0.5 truncate"> <a href="/author?bsky_handle=paulgreg.bsky.social" class="hover:underline text-gray-500 no-card-link">@paulgreg.bsky.social</a> </div> </div> <div class="mt-3 text-[1.2rem] leading-7 break-words text-gray-800 w-[calc(100%+3.5rem)] -ml-[3.5rem]">Touchpad wasn’t working anymore on my old t440s Thinkpad. I’ve finally tried to replace it ! Wasn’t easy to repair but It’s now fixed and upgraded ! <a href="/hashtag/thinkpad" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#thinkpad</a> <a href="/hashtag/t440s" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#t440s</a> <a href="/hashtag/repair" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#repair</a> <a href="/hashtag/touchpad" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#touchpad</a></div> <div class="mt-2 w-[calc(100%+3.5rem)] -ml-[3.5rem] no-card-link" data-controller="modal"> <div class="grid grid-cols-2 gap-1"> <img src="https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:2kpmcums7slsctgnponrmtan/bafkreih4xmtshi52br4vuwqmyyekm42irm45sy63lk4753s43dougwjtf4@jpeg" data-action="click->modal#openWith" data-large-src="https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:2kpmcums7slsctgnponrmtan/bafkreih4xmtshi52br4vuwqmyyekm42irm45sy63lk4753s43dougwjtf4@jpeg" alt="" class="w-full h-full object-cover rounded cursor-zoom-in" loading="lazy"> <img src="https://cdn.bsky.app/img/feed_thumbnail/plain/did:plc:2kpmcums7slsctgnponrmtan/bafkreib5tfjd5xxo4hzjwjh4vi4xlowqlc3sqcpq674guv7xh6j3rrsage@jpeg" data-action="click->modal#openWith" data-large-src="https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:2kpmcums7slsctgnponrmtan/bafkreib5tfjd5xxo4hzjwjh4vi4xlowqlc3sqcpq674guv7xh6j3rrsage@jpeg" alt="" class="w-full h-full object-cover rounded cursor-zoom-in" loading="lazy"> </div> <div data-modal-target="panel" class="hidden fixed inset-0 z-[100]"> <div class="absolute inset-0 bg-black/90" data-action="click->modal#close"></div> <div class="relative z-[101] h-full w-full flex items-center justify-center p-4" data-action="click->modal#stop"> <img data-modal-target="image" alt="" class="max-w-full max-h-full rounded"> <button type="button" aria-label="Close" class="absolute top-4 right-4 inline-flex items-center justify-center w-10 h-10 rounded-full bg-black/60 text-white hover:bg-black/80" data-action="click->modal#close"> <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> </button> </div> </div> </div> <div class="mt-3 text-xs text-gray-500 flex items-center gap-2 whitespace-nowrap overflow-hidden w-[calc(100%+3.5rem)] -ml-[3.5rem]"> <span>November 23, 2024 at 11:06 AM</span> </div> <div class="border-t my-2 w-[calc(100%+3.5rem)] -ml-[3.5rem]"></div> <div class="mt-3 flex items-center justify-between text-gray-600 text-sm w-[calc(100%+3.5rem)] -ml-[3.5rem] no-card-link" data-controller="bluesky-interactions"> <div class="flex items-center justify-between w-[300px] pr-4 no-card-link"> <div class="hidden" data-post-text-uri="at://did:plc:2kpmcums7slsctgnponrmtan/app.bsky.feed.post/3lbmdyf4haz2t"> Touchpad wasn’t working anymore on my old t440s Thinkpad. I’ve finally tried to replace it ! Wasn’t easy to repair but It’s now fixed and upgraded ! <a href="/hashtag/thinkpad" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#thinkpad</a> <a href="/hashtag/t440s" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#t440s</a> <a href="/hashtag/repair" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#repair</a> <a href="/hashtag/touchpad" class="hover:underline text-blue-600 dark:text-sky-400 no-card-link">#touchpad</a> </div> <button type="button" class="flex items-center gap-1 hover:text-black no-card-link" data-action="click->bluesky-auth#requireConnection click->bluesky-interactions#reply" data-bluesky-interactions-reply-uri-param="at://did:plc:2kpmcums7slsctgnponrmtan/app.bsky.feed.post/3lbmdyf4haz2t" data-bluesky-interactions-reply-cid-param="bafyreifsm3fkjpxjj55scc4id7vtib6ewwgqq6rnesy6pnaubonq3cqpia" data-bluesky-interactions-reply-root-uri-param="at://did:plc:2kpmcums7slsctgnponrmtan/app.bsky.feed.post/3lbmdyf4haz2t" data-bluesky-interactions-reply-root-cid-param="bafyreifsm3fkjpxjj55scc4id7vtib6ewwgqq6rnesy6pnaubonq3cqpia" data-bluesky-interactions-reply-author-name-param="Grégory PAUL" data-bluesky-interactions-reply-author-avatar-param="https://cdn.bsky.app/img/avatar/plain/did:plc:2kpmcums7slsctgnponrmtan/bafkreibq5meslzyuvcl6un75xlp2ullhmaieeylde6pp4qwx7kcx7ajib4@jpeg" data-bluesky-interactions-post-path-param="/bsky/paulgreg.bsky.social/post/3lbmdyf4haz2t" title="Reply" style="pointer-events: auto; cursor: pointer; z-index: 10; position: relative;"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a4 4 0 0 1-4 4H7l-4 4V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4z"/></svg> </button> <div class="relative no-card-link" data-controller="bluesky-repost-menu" data-bluesky-repost-menu-uri-value="at://did:plc:2kpmcums7slsctgnponrmtan/app.bsky.feed.post/3lbmdyf4haz2t" data-bluesky-repost-menu-cid-value="bafyreifsm3fkjpxjj55scc4id7vtib6ewwgqq6rnesy6pnaubonq3cqpia" data-bluesky-repost-menu-repost-uri-value="" data-bluesky-repost-menu-is-reposted-value="false" data-bluesky-repost-menu-author-handle-value="paulgreg.bsky.social" data-bluesky-repost-menu-post-text-value="Touchpad wasn’t working anymore on my old t440s Thinkpad. I’ve finally tried to replace it ! Wasn’t easy to repair but It’s now fixed and upgraded ! #thinkpad #t440s #repair #touchpad" style="pointer-events:auto; cursor:pointer;"> <button type="button" class="flex items-center gap-1 hover:text-black " data-action="click->bluesky-repost-menu#toggleMenu" data-bluesky-interactions-target="repostButton" title="Repost"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg> </button> <div class="hidden absolute left-0 bottom-full mb-3 w-48 rounded-2xl border border-white/10 bg-[#1C2333] text-white shadow-xl overflow-hidden z-50" data-bluesky-repost-menu-target="menu"> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-auth#requireConnection click->bluesky-repost-menu#repost"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg> <span>Repost</span> </button> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-auth#requireConnection click->bluesky-repost-menu#quote"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> <span>Quote post</span> </button> </div> </div> <button type="button" class="flex items-center gap-1 hover:text-black no-card-link " data-action="click->bluesky-auth#requireConnection click->bluesky-interactions#toggleLike" data-bluesky-interactions-uri-param="at://did:plc:2kpmcums7slsctgnponrmtan/app.bsky.feed.post/3lbmdyf4haz2t" data-bluesky-interactions-cid-param="bafyreifsm3fkjpxjj55scc4id7vtib6ewwgqq6rnesy6pnaubonq3cqpia" data-bluesky-interactions-like-uri-param="" data-bluesky-interactions-target="likeButton" title="Like"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 1 0-7.78 7.78L12 21.23l8.84-8.84a5.5 5.5 0 0 0 0-7.78z"/></svg> </button> </div> <div class="flex items-center gap-4 text-gray-500"> <button type="button" class="flex items-center hover:text-black no-card-link" data-controller="share" data-share-url-value="https://bushdrum.com/bsky/paulgreg.bsky.social/post/3lbmdyf4haz2t" data-share-title-value="Grégory PAUL on Lightnews" data-share-text-value="Touchpad wasn’t working anymore on my old t440s Thinkpad. I’ve finally tried to replace it ! Wasn’t easy to repair but It’s now fixed and up…" data-action="click->share#shareSystem" title="Share" style="pointer-events:auto; cursor:pointer;"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12v7a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg> </button> <div class="relative no-card-link" data-controller="bluesky-options" data-bluesky-options-text-value="Touchpad wasn’t working anymore on my old t440s Thinkpad. I’ve finally tried to replace it ! Wasn’t easy to repair but It’s now fixed and upgraded ! #thinkpad #t440s #repair #touchpad" data-bluesky-options-author-handle-value="paulgreg.bsky.social" data-bluesky-options-author-did-value="did:plc:2kpmcums7slsctgnponrmtan" data-bluesky-options-muted-value="false" data-bluesky-options-blocking-uri-value="" data-bluesky-options-own-value="false" data-bluesky-options-delete-url-value="" data-bluesky-options-post-element-selector-value='[data-card-type="bsky-post"]'> <button type="button" class="inline-flex items-center" data-action="click->bluesky-options#toggleMenu" aria-label="Post options"> <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg> </button> <div class="hidden absolute right-0 bottom-full mb-3 w-60 rounded-2xl border border-white/10 bg-[#1C2333] text-white shadow-xl overflow-hidden" data-bluesky-options-target="menu"> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-options#translate"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h16v16H4z"/><path d="M9 8h6"/><path d="M9 12h6"/><path d="M9 16h6"/></svg> </span> <span>Translate</span> </button> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-options#copyText"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2h-8a2 2 0 0 0-2 2v8"/></svg> </span> <span>Copy post text</span> </button> <div class="border-t border-white/10"></div> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-auth#requireConnection click->bluesky-options#muteAccount"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 9v6h3l5 5V4L7 9H4z"/><line x1="16" y1="9" x2="19" y2="12"/><line x1="19" y1="12" x2="16" y2="15"/></svg> </span> <span data-bluesky-options-target="muteLabel">Mute account</span> </button> <button type="button" class="w-full px-4 py-3 flex items-center gap-3 text-sm hover:bg-[#273045] text-left" data-action="click->bluesky-auth#requireConnection click->bluesky-options#blockAccount"> <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-white/10"> <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3h18v18H3z"/><path d="M3 3l18 18"/></svg> </span> <span data-bluesky-options-target="blockLabel">Block account</span> </button> </div> </div> </div> </div> </div> </div> </div> </div> <script> // Prevent navigation when clicking in avatar column area - must run in capture phase before global handler // Only initialize once to avoid duplicate listeners if (!window._avatarColumnNoNavInitialized) { window._avatarColumnNoNavInitialized = true; document.addEventListener('click', function(ev) { try { // Check if target itself has the class OR is inside an element with that class var avatarColumn = (ev.target.classList && ev.target.classList.contains('avatar-column-no-nav')) || (ev.target.closest && ev.target.closest('.avatar-column-no-nav')); if (avatarColumn) { // Only allow navigation if clicking directly on the avatar link itself var isAvatarLink = ev.target.tagName === 'A' && ev.target.href && ev.target.href.indexOf('/author') !== -1; if (!isAvatarLink) { ev.stopPropagation(); ev.stopImmediatePropagation(); ev.preventDefault(); return false; } } } catch(e) {} }, true); // Capture phase - runs BEFORE other handlers } </script> </div> </div> </div> </main> <!-- mobile footer CTA will be rendered after the grid to avoid stacking issues --> </div> <div class="hidden md:block"></div> </div> <!-- Mobile footer: CTA when logged out; mini-nav when logged in --> <div id="cta-footer" class="md:hidden fixed bottom-0 left-0 right-0 z-40 bg-white dark:bg-black border-t border-gray-200 dark:border-gray-700"> <div class="px-2 py-3"> <div class="flex items-center justify-between gap-3"> <!-- Left: Logo + Text --> <div class="flex items-center gap-2 flex-1 min-w-0"> <a class="inline-flex items-center gap-2" aria-label="Lightnews" data-apply-edition="1" href="/"> <img alt="Lightnews" class="logo-light h-10 w-10 flex-shrink-0" src="/assets/lightnews_black-993b5cbd.png" /> <img alt="Lightnews" class="logo-dark h-10 w-10 flex-shrink-0" src="/assets/lightnews-f13a19bd.png" /> <span class="text-base font-semibold text-gray-900 dark:text-white truncate brand-text">Lightnews</span> </a> </div> <!-- Right: Buttons --> <div class="flex items-center gap-2 flex-shrink-0"> <a class="btn btn-primary btn-sm whitespace-nowrap" data-turbo="false" href="/auth/bluesky/start">Create account</a> <a class="btn btn-light btn-sm whitespace-nowrap" data-turbo="false" href="/auth/bluesky/start">Sign in</a> </div> </div> </div> </div> <!-- Add bottom padding to body on mobile to prevent content from being hidden behind sticky footer --> <style> @media (max-width: 767px) { body { padding-bottom: 4.5rem; } } /* Hard guarantee: footer is visible on mobile widths */ @media (max-width: 1023px) { #cta-footer { display: block !important; } } </style> <!-- Sign In Modal --> <div id="signin-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" data-auth-modal-target="signinModal"> <div class="flex min-h-screen items-center justify-center p-4"> <!-- Backdrop --> <div class="fixed inset-0 bg-black/80 transition-opacity" data-action="click->auth-modal#close"></div> <!-- Modal Content --> <div class="relative bg-white dark:bg-black rounded-2xl w-full max-w-md p-8 shadow-2xl"> <!-- Close Button --> <button data-action="click->auth-modal#close" class="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"> <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <line x1="18" y1="6" x2="6" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line> </svg> </button> <!-- Logo --> <div class="flex justify-center mb-6"> <img alt="Lightnews" class="logo-light h-12 w-12" src="/assets/lightnews_black-993b5cbd.png" /> <img alt="Lightnews" class="logo-dark h-12 w-12" src="/assets/lightnews-f13a19bd.png" /> </div> <!-- Title --> <h2 class="text-black dark:text-white text-3xl font-bold text-center mb-6">Sign in to Lightnews</h2> <!-- Top CTA: Bluesky quick sign-in (neutral OAuth style with icon) --> <div class="mb-6"> <a class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600" data-turbo="false" href="/auth/bluesky/start"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.038.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 01-.415-.056c.14.018.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z" fill="#1185fe"/> </svg> Continue with Bluesky </a> </div> <!-- OAuth Buttons --> <div class="space-y-3 mb-6"> <!-- Google --> <button class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/> <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/> <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/> <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/> </svg> Continue with Google </button> <!-- Apple --> <button class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/> </svg> Continue with Apple </button> </div> <!-- Divider --> <div class="relative my-6"> <div class="absolute inset-0 flex items-center"> <div class="w-full border-t border-gray-700"></div> </div> </div> <!-- Email Form --> <form class="space-y-4"> <div class="mt-10"> <label for="signin-email" class="block text-black dark:text-white font-semibold text-sm mb-2">Email or username</label> <input type="text" id="signin-email" placeholder="Email or username" class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" /> </div> <button type="submit" class="w-full btn btn-primary py-3 font-semibold"> Continue </button> </form> <!-- Footer --> <div class="mt-8 text-center"> <p class="text-gray-600 dark:text-gray-400 mb-2">Don't have an account?</p> <button data-action="click->auth-modal#switchToSignup" class="text-blue-600 dark:text-white font-semibold underline hover:text-blue-500 transition-colors"> Sign up for Lightnews </button> </div> </div> </div> </div> <!-- Sign Up Modal --> <div id="signup-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" data-auth-modal-target="signupModal"> <div class="flex min-h-screen items-center justify-center p-4"> <!-- Backdrop --> <div class="fixed inset-0 bg-black/80 transition-opacity" data-action="click->auth-modal#close"></div> <!-- Modal Content --> <div class="relative bg-white dark:bg-black rounded-2xl w-full max-w-md p-8 shadow-2xl"> <!-- Close Button --> <button data-action="click->auth-modal#close" class="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"> <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <line x1="18" y1="6" x2="6" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line> </svg> </button> <!-- Logo --> <div class="flex justify-center mb-6"> <img alt="Lightnews" class="logo-light h-12 w-12" src="/assets/lightnews_black-993b5cbd.png" /> <img alt="Lightnews" class="logo-dark h-12 w-12" src="/assets/lightnews-f13a19bd.png" /> </div> <!-- Title --> <h2 class="text-black dark:text-white text-3xl font-bold text-center mb-6">Sign up to start reading</h2> <!-- Top CTA: Bluesky quick sign-up (neutral OAuth style with icon) --> <div class="mb-6"> <a class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600" data-turbo="false" href="/auth/bluesky/start"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.038.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 01-.415-.056c.14.018.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z" fill="#1185fe"/> </svg> Sign up with Bluesky </a> </div> <!-- Divider before email sign up --> <div class="relative my-4"> <div class="absolute inset-0 flex items-center"> <div class="w-full border-t border-gray-300 dark:border-gray-700"></div> </div> <div class="relative flex justify-center"> <span class="bg-white dark:bg-black px-3 text-sm text-gray-600 dark:text-gray-300">or</span> </div> </div> <!-- Email Form First --> <form class="space-y-4 mt-2"> <div> <label for="signup-email" class="block text-black dark:text-white font-semibold text-sm mt-2 mb-2">Email address</label> <input type="email" id="signup-email" placeholder="name@domain.com" class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" /> </div> <button type="submit" class="w-full btn btn-primary py-3 font-semibold"> Next </button> </form> <!-- Divider --> <div class="relative my-6"> <div class="absolute inset-0 flex items-center"> <div class="w-full border-t border-gray-700"></div> </div> <div class="relative flex justify-center"> <span class="bg-white dark:bg-black px-4 text-black dark:text-white text-sm">or</span> </div> </div> <!-- OAuth Buttons --> <div class="space-y-3 mb-6"> <!-- Google --> <button class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/> <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/> <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/> <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/> </svg> Sign up with Google </button> <!-- Apple --> <button class="w-full flex items-center justify-center gap-3 px-6 py-3 bg-transparent hover:bg-black/5 dark:hover:bg-white/10 text-black dark:text-white font-semibold rounded transition-colors border border-gray-300 dark:border-gray-600"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> <path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/> </svg> Sign up with Apple </button> </div> <!-- Footer --> <div class="mt-8 text-center"> <p class="text-gray-600 dark:text-gray-400 mb-2">Already have an account?</p> <button data-action="click->auth-modal#switchToSignin" class="text-blue-600 dark:text-white font-semibold underline hover:text-blue-500 transition-colors"> Sign in </button> </div> </div> </div> </div> <div data-controller="modal" data-reply-modal="true" id="global-reply-modal" data-viewer-avatar-url="" data-viewer-display-name=""> <div class="hidden fixed inset-0 z-50 overflow-y-auto" data-modal-target="panel" data-viewer-avatar-url="" data-viewer-display-name=""> <div class="flex min-h-screen items-start justify-center pt-4 md:pt-20"> <!-- Backdrop --> <div class="fixed inset-0 bg-black/80 transition-opacity" data-action="click->modal#close"></div> <!-- Modal Content --> <div class="relative bg-[#1e3a5f] rounded-2xl w-full max-w-xl mx-4 shadow-2xl" data-action="click->modal#stop"> <!-- Top bar: Cancel + Reply button --> <div class="flex items-center justify-between px-4 py-3 border-b border-gray-600"> <button type="button" class="text-blue-400 text-base font-medium" data-action="click->modal#close">Cancel</button> <button type="button" id="modal-reply-submit" class="px-5 py-1.5 bg-blue-600 text-white rounded-full font-semibold hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"> Reply </button> </div> <!-- Parent post preview (for replies) --> <div class="px-4 py-3 border-b border-gray-600 modal-reply-preview"> <div class="flex items-start gap-3"> <img src="" alt="" class="modal-reply-author-avatar w-10 h-10 rounded-full flex-shrink-0 hidden"> <div class="modal-reply-author-avatar-placeholder w-10 h-10 rounded-full bg-gray-300 flex-shrink-0"></div> <div class="flex-1 min-w-0"> <div class="font-semibold text-white text-base modal-reply-author-name"> </div> <div class="text-gray-300 text-sm line-clamp-3 modal-reply-post-text"> </div> </div> </div> </div> <!-- Reply/Post textarea --> <div class="px-4 py-3"> <div class="flex items-start gap-3"> <img src="" alt="You" class="modal-viewer-avatar w-10 h-10 rounded-full flex-shrink-0 hidden"> <div class="modal-viewer-avatar-placeholder w-10 h-10 rounded-full bg-gray-300 flex-shrink-0 "></div> <div class="flex-1 min-w-0"> <textarea id="modal-reply-text" placeholder="Write your reply" rows="8" maxlength="300" class="w-full px-0 py-2 bg-transparent text-white text-base placeholder-gray-400 resize-none focus:outline-none border-0" data-reply-uri="" data-reply-cid="" data-root-uri="" data-root-cid="" data-mode="reply" data-embed="" ></textarea> </div> </div> <!-- Quote preview (for quote posts, shown below textarea) --> <div class="modal-quote-preview hidden mt-3 p-3 border border-gray-600 rounded-xl bg-[#152642]"> <div class="flex items-start gap-3"> <img src="" alt="" class="modal-quote-author-avatar w-8 h-8 rounded-full flex-shrink-0 hidden"> <div class="modal-quote-author-avatar-placeholder w-8 h-8 rounded-full bg-gray-400 flex-shrink-0"></div> <div class="flex-1 min-w-0"> <div class="font-semibold text-white text-sm modal-quote-author-name"></div> <div class="text-gray-300 text-sm mt-1 modal-quote-post-text"></div> </div> </div> </div> <input type="file" id="modal-attachment-input" accept="image/*" class="hidden" multiple> <div id="modal-attachment-preview" class="mt-3 hidden"> <div class="flex flex-wrap gap-3" id="modal-attachment-preview-list"></div> </div> </div> <!-- Bottom toolbar --> <div class="px-4 py-3 border-t border-gray-600 flex items-center justify-between"> <div class="flex items-center gap-3"> <button type="button" class="text-blue-500 hover:text-blue-400" id="modal-attachment-button"> <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg> </button> <button type="button" class="text-blue-500 hover:text-blue-400" id="modal-gif-button"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><text x="3" y="18" font-family="Arial" font-size="14" font-weight="bold">GIF</text></svg> </button> </div> <div class="flex items-center gap-3 text-sm"> <span class="text-blue-400">English</span> <span class="text-gray-400" id="modal-reply-count">300</span> </div> </div> </div> </div> </div> </div> <script> (function(){ function setupReplyModal(){ var textarea = document.getElementById('modal-reply-text'); var submitBtn = document.getElementById('modal-reply-submit'); var countEl = document.getElementById('modal-reply-count'); var attachmentButton = document.getElementById('modal-attachment-button'); var gifButton = document.getElementById('modal-gif-button'); var attachmentInput = document.getElementById('modal-attachment-input'); var attachmentPreview = document.getElementById('modal-attachment-preview'); var attachmentList = document.getElementById('modal-attachment-preview-list'); if (!textarea || !submitBtn || !countEl) { return; } if (textarea.dataset.replyModalBound === '1') { return; } textarea.dataset.replyModalBound = '1'; var attachments = []; var currentEmbed = null; var isUploadingAttachment = false; var MAX_ATTACHMENTS = 4; function updateCount(){ var length = textarea.value.length; var remaining = 300 - length; countEl.textContent = remaining.toString(); countEl.classList.toggle('text-red-400', remaining < 0); countEl.classList.toggle('text-gray-400', remaining >= 0); submitBtn.disabled = ((length === 0) && attachments.length === 0) || length > 300; } function updateEmbedPayload() { if (!attachments.length) { currentEmbed = null; textarea.removeAttribute('data-embed'); return; } currentEmbed = { "$type": "app.bsky.embed.images", "images": attachments.map(function(item){ return { "alt": item.alt || 'Image', "image": item.blob }; }) }; textarea.dataset.embed = JSON.stringify(currentEmbed); } function refreshAttachmentPreview() { if (!attachmentList || !attachmentPreview) { return; } attachmentList.innerHTML = ''; attachments.forEach(function(item, index){ var wrapper = document.createElement('div'); wrapper.className = 'relative w-20 h-20 rounded-xl overflow-hidden'; var img = document.createElement('img'); img.src = item.previewUrl; img.alt = item.alt || 'Attachment'; img.className = 'object-cover w-full h-full'; wrapper.appendChild(img); var removeBtn = document.createElement('button'); removeBtn.type = 'button'; removeBtn.className = 'absolute top-1 right-1 bg-black/70 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs'; removeBtn.innerHTML = '×'; removeBtn.addEventListener('click', function(){ removeAttachment(index); }); wrapper.appendChild(removeBtn); attachmentList.appendChild(wrapper); }); if (attachments.length) { attachmentPreview.classList.remove('hidden'); } else { attachmentPreview.classList.add('hidden'); } if (attachmentButton) { attachmentButton.disabled = attachments.length >= MAX_ATTACHMENTS || isUploadingAttachment; attachmentButton.classList.toggle('opacity-50', attachmentButton.disabled); attachmentButton.classList.toggle('pointer-events-none', attachmentButton.disabled); } if (gifButton) { gifButton.disabled = attachments.length >= MAX_ATTACHMENTS || isUploadingAttachment; gifButton.classList.toggle('opacity-50', gifButton.disabled); gifButton.classList.toggle('pointer-events-none', gifButton.disabled); } updateCount(); } function removeAttachment(index) { if (index < 0 || index >= attachments.length) { return; } var removed = attachments.splice(index, 1)[0]; if (removed && removed.previewUrl) { try { URL.revokeObjectURL(removed.previewUrl); } catch (_) {} } updateEmbedPayload(); refreshAttachmentPreview(); } function setAttachmentUploading(state) { isUploadingAttachment = state; refreshAttachmentPreview(); } async function uploadFile(file) { if (!file || !attachmentInput) { return; } if (!file.type.match(/^image\//)) { alert('Only image uploads are supported right now.'); return; } if (attachments.length >= MAX_ATTACHMENTS) { alert('You can attach up to ' + MAX_ATTACHMENTS + ' images.'); return; } if (file.size > 976 * 1024) { alert('Images must be 976KB or smaller.'); return; } setAttachmentUploading(true); var formData = new FormData(); formData.append('file', file); formData.append('alt', file.name || 'Image'); try { var response = await fetch('/api/bluesky/uploads', { method: 'POST', headers: { 'X-CSRF-Token': (document.querySelector('meta[name="csrf-token"]') || {}).content || '', 'Accept': 'application/json' }, credentials: 'same-origin', body: formData }); if (!response.ok) { var errorText = await response.text(); var message = 'Upload failed'; try { var parsed = JSON.parse(errorText); message = parsed.error || message; } catch (_) { if (errorText && errorText.length < 200) { message = errorText; } } throw new Error(message); } var data = await response.json(); if (!data || !data.success || !data.blob) { throw new Error((data && data.error) || 'Upload failed'); } var previewUrl = URL.createObjectURL(file); attachments.push({ blob: data.blob, alt: data.alt || file.name || 'Image', previewUrl: previewUrl }); updateEmbedPayload(); refreshAttachmentPreview(); } catch (error) { console.error('Attachment upload error:', error); alert(error && error.message ? error.message : 'Unable to upload image right now.'); } finally { setAttachmentUploading(false); if (attachmentInput) { attachmentInput.value = ''; } } } function handleFileSelection(files) { if (!files || !files.length) { return; } var remaining = MAX_ATTACHMENTS - attachments.length; Array.from(files).slice(0, remaining).forEach(function(file){ uploadFile(file); }); } if (attachmentButton && attachmentInput) { attachmentButton.addEventListener('click', function(e){ e.preventDefault(); if (isUploadingAttachment || attachments.length >= MAX_ATTACHMENTS) { return; } attachmentInput.setAttribute('accept', 'image/*'); attachmentInput.click(); }); } if (gifButton && attachmentInput) { gifButton.addEventListener('click', function(e){ e.preventDefault(); if (isUploadingAttachment || attachments.length >= MAX_ATTACHMENTS) { return; } attachmentInput.setAttribute('accept', 'image/gif'); attachmentInput.click(); }); } if (attachmentInput) { attachmentInput.addEventListener('change', function(e){ handleFileSelection(e.target.files); }); } refreshAttachmentPreview(); textarea.addEventListener('input', updateCount); updateCount(); submitBtn.addEventListener('click', async function(e){ e.preventDefault(); var text = textarea.value.trim(); if ((text.length === 0 && attachments.length === 0) || text.length > 300) { return; } var replyUri = textarea.dataset.replyUri; var replyCid = textarea.dataset.replyCid; var rootUri = textarea.dataset.rootUri; var rootCid = textarea.dataset.rootCid; var mode = textarea.dataset.mode || 'reply'; if (mode === 'reply' && (!replyUri || !replyCid)) { return; } submitBtn.disabled = true; var originalText = submitBtn.textContent; submitBtn.textContent = mode === 'post' ? 'Posting...' : 'Replying...'; try { var endpoint, payload; if (mode === 'post') { endpoint = '/api/bluesky/posts'; payload = { text: text }; } else { endpoint = '/api/bluesky/replies'; payload = { text: text, reply_to_uri: replyUri, reply_to_cid: replyCid, root_uri: rootUri, root_cid: rootCid }; } // Attach quote embed if present (for quote posts) - check first var quoteEmbed = textarea.dataset.quoteEmbed; if (quoteEmbed && mode === 'post' && !currentEmbed) { try { payload.embed = JSON.parse(quoteEmbed); console.log('Added quote embed:', payload.embed); } catch (e) { console.error('Failed to parse quote embed', e); } } // Attach image embed if present (overrides quote for now) if (currentEmbed) { payload.embed = currentEmbed; console.log('Added image embed:', payload.embed); } console.log('Submitting payload:', JSON.stringify(payload).substring(0, 300)); var response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': (document.querySelector('meta[name="csrf-token"]') || {}).content || '' }, credentials: 'same-origin', body: JSON.stringify(payload) }); console.log('Reply response status:', response.status); if (!response.ok) { var errorText = await response.text(); var errorMessage = 'Network error. Please try again.'; var settingsUrl = null; try { var errorData = JSON.parse(errorText); errorMessage = errorData.error || errorMessage; settingsUrl = errorData.settings_url || null; } catch (e2) { if (errorText && errorText.length < 200) { errorMessage = errorText; } else { errorMessage = 'HTTP ' + response.status + ': ' + response.statusText; } } console.error('Error response:', response.status, errorMessage); if (response.status === 401 && settingsUrl) { if (confirm(errorMessage + '\n\nWould you like to go to Settings now?')) { window.location.href = settingsUrl; return; } } else { alert(errorMessage); } submitBtn.disabled = false; submitBtn.textContent = originalText; return; } var data = await response.json(); console.log('Reply response data:', data); if (data.success) { window.location.reload(); } else { alert(data.error || (mode === 'post' ? 'Failed to create post' : 'Failed to post reply')); submitBtn.disabled = false; submitBtn.textContent = originalText; } } catch (error) { console.error('Reply error:', error); alert(error && error.message ? error.message : 'Network error. Please try again.'); submitBtn.disabled = false; submitBtn.textContent = originalText; } }); } function openReplyIfRequested(){ try{ var qs = new URLSearchParams(window.location.search); if(qs.get('reply') === '1'){ var panel = document.querySelector('[data-modal-target="panel"]'); if(panel){ panel.classList.remove('hidden'); document.documentElement.classList.add('overflow-hidden'); document.body.classList.add('overflow-hidden'); var ta = document.getElementById('modal-reply-text'); if(ta){ ta.focus(); } } } }catch(e){} } function initReplyModal(){ setupReplyModal(); openReplyIfRequested(); } document.addEventListener('turbo:load', function(){ setTimeout(initReplyModal, 0); }); if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(initReplyModal, 0); } else { document.addEventListener('DOMContentLoaded', initReplyModal); } })(); </script> <script> // Sanitize any legacy nav=1 param from links and the current URL document.addEventListener('click', function(ev){ try{ var a = ev.target.closest && ev.target.closest('a'); if(!a) return; var url = new URL(a.href, location.origin); if(url.searchParams.has('nav')){ url.searchParams.delete('nav'); a.setAttribute('href', url.pathname + (url.searchParams.toString() ? ('?' + url.searchParams.toString()) : '') + url.hash); } }catch(e){} }, true); document.addEventListener('turbo:load', function(){ try{ var url = new URL(location.href); if(url.searchParams.has('nav')){ url.searchParams.delete('nav'); var cleaned = url.pathname + (url.searchParams.toString() ? ('?' + url.searchParams.toString()) : '') + url.hash; history.replaceState(null, '', cleaned); } }catch(e){} }); </script> <script> function loadTwitterEmbeds() { try { if (window.twttr && window.twttr.widgets) { window.twttr.widgets.load(); return true; } } catch(e) {} return false; } document.addEventListener("turbo:load", function() { if (!loadTwitterEmbeds()) { var tries = 0; var max = 10; (function retry(){ if (loadTwitterEmbeds() || ++tries > max) return; setTimeout(retry, 400); })(); } // Re-hydrate Bluesky embeds after Turbo navigation by reinjecting the script try { var s = document.createElement("script"); s.src = "https://embed.bsky.app/static/embed.js?ts=" + Date.now(); s.async = true; s.onload = function(){ try { s.remove(); } catch(_){} }; document.body.appendChild(s); } catch (e) {} }); </script> <script> // Decorate links to lists (home/sections) with stored edition at click time (web only) document.addEventListener('click', function(ev){ try{ var a = ev.target.closest && ev.target.closest('a[data-apply-edition="1"]'); if(!a) return; var pref = localStorage.getItem('preferredEdition'); if(pref && pref !== 'global'){ var url = new URL(a.href, location.origin); url.searchParams.set('edition', pref); if (url.toString() !== a.href) { // Update href and let the browser/Turbo handle the navigation once a.setAttribute('href', url.pathname + '?' + url.searchParams.toString() + url.hash); } } else { var url2 = new URL(a.href, location.origin); if(url2.searchParams.has('edition')){ url2.searchParams.delete('edition'); var newHref = url2.pathname + (url2.searchParams.toString() ? ('?' + url2.searchParams.toString()) : '') + url2.hash; a.setAttribute('href', newHref); } } }catch(e){} }, true); </script> <script> // Track last list path (home or sections) for back navigation on digest pages document.addEventListener('turbo:load', function(){ try{ var path = location.pathname; if(path === '/' || path.indexOf('/sections/') === 0){ var url = new URL(location.href); var ed = url.searchParams.get('edition'); var stored = path + (ed ? ('?edition=' + ed) : ''); sessionStorage.setItem('lastListPath', stored); sessionStorage.setItem('lastNonPost', stored); } else if(path === '/trending'){ sessionStorage.setItem('lastTrendingPath', '/trending'); sessionStorage.setItem('lastNonPost', '/trending'); } else if(path.indexOf('/digests/') === 0){ // Keep lastDigestPath fresh when on digest pages to avoid stale fallbacks sessionStorage.setItem('lastDigestPath', path + location.search); sessionStorage.setItem('lastNonPost', path + location.search); } }catch(e){} }); </script> <script> // Remove any l=... language parameter from digest links to unify cache keys document.addEventListener('click', function(ev){ try{ var a = ev.target.closest && ev.target.closest('a[href^="/digests/"]'); if(!a) return; var url = new URL(a.href, location.origin); if(url.searchParams.has('l')){ url.searchParams.delete('l'); var newHref = url.pathname + (url.searchParams.toString() ? ('?' + url.searchParams.toString()) : '') + url.hash; a.setAttribute('href', newHref); } }catch(e){} }, true); </script> <script> // Remember previous page before navigating to a local Bluesky post via anchor document.addEventListener('click', function(ev){ try{ var a = ev.target.closest && ev.target.closest('a[href^="/bsky/"]'); if(!a) return; var prev = location.pathname + location.search; sessionStorage.setItem('prevPage', prev); sessionStorage.setItem('lastPostPath', location.pathname); }catch(e){} }, true); </script> <script> // Remember previous page before navigating to an author profile document.addEventListener('click', function(ev){ try{ // Match both /author and /author/... and /author?... var a = ev.target.closest && (ev.target.closest('a[href^="/author/"]') || ev.target.closest('a[href^="/author"]')); if(!a) return; var prev = location.pathname + location.search; sessionStorage.setItem('prevPage', prev); if(location.pathname.indexOf('/author/') !== 0){ sessionStorage.setItem('lastNonAuthor', prev); } sessionStorage.setItem('entrySource','author'); }catch(e){} }, true); </script> <script> // Also sanitize card-clickable data-href: strip l=... if present document.addEventListener('click', function(ev){ try{ var card = ev.target.closest && ev.target.closest('.card-clickable'); if(!card) return; var href = (card.getAttribute('data-href') || '').trim(); if(!href) return; // Strip accidental wrapping quotes if((href[0]==='"' && href[href.length-1]==='"') || (href[0]==="'" && href[href.length-1]==="'")){ href = href.slice(1,-1); } var url = new URL(href, location.origin); if(url.searchParams.has('l')){ url.searchParams.delete('l'); var newHref = url.pathname + (url.searchParams.toString() ? ('?' + url.searchParams.toString()) : '') + url.hash; card.setAttribute('data-href', newHref); } // Navigate on card click unless clicking an inner actionable link var isInlineVideo = ev.target.closest && ev.target.closest('.inline-video'); // Tag nested-card navigations so back button can return to previous post instead of thread parent var inNested = ev.target.closest && ev.target.closest('.bsky-nested, .nested-bsky, .bluesky-nested'); // Don't navigate if clicking in avatar column area (left side of post with avatar and thread lines) // Check if target itself has the class OR is inside an element with that class var isAvatarColumn = (ev.target.classList && ev.target.classList.contains('avatar-column-no-nav')) || (ev.target.closest && ev.target.closest('.avatar-column-no-nav')); if(!ev.target.closest('.no-card-link') && !isInlineVideo && !isAvatarColumn){ ev.preventDefault(); var targetHref = (card.getAttribute('data-href') || href).trim(); if((targetHref[0]==='"' && targetHref[targetHref.length-1]==='"') || (targetHref[0]==="'" && targetHref[targetHref.length-1]==="'")){ targetHref = targetHref.slice(1,-1); } try{ // If navigating from a digest page to a post, remember this digest for back navigation if(location.pathname.indexOf('/digests/') === 0){ sessionStorage.setItem('lastDigestPath', location.pathname + location.search); sessionStorage.setItem('entrySource','digest'); } else if(location.pathname.indexOf('/bsky/') === 0){ // Navigating from a post to another post (e.g., reply): mark source as post sessionStorage.setItem('entrySource', inNested ? 'nested' : 'post'); } // Always remember the immediate previous page when going to a post if(targetHref.indexOf('/bsky/') === 0){ var prevp = location.pathname + location.search; sessionStorage.setItem('prevPage', prevp); sessionStorage.setItem('lastPostPath', location.pathname); } }catch(e){} if(window.Turbo && Turbo.visit){ Turbo.visit(targetHref); } else { window.location.href = targetHref; } } }catch(e){} }, true); </script> <script> // Keyboard activation for card-clickable elements document.addEventListener('keydown', function(ev){ try{ if(ev.key !== 'Enter' && ev.key !== ' ') return; var card = ev.target.closest && ev.target.closest('.card-clickable'); if(!card) return; var isInlineVideo = ev.target.closest && ev.target.closest('.inline-video'); if(isInlineVideo) return; var href = (card.getAttribute('data-href') || '').trim(); if(!href) return; if((href[0]==='"' && href[href.length-1]==='"') || (href[0]==="'" && href[href.length-1]==="'")){ href = href.slice(1,-1); } ev.preventDefault(); if(window.Turbo && Turbo.visit){ Turbo.visit(href); } else { window.location.href = href; } }catch(e){} }, true); </script> <script> // Sync stored edition from current URL on list pages (no navigation) // Only update when the URL explicitly includes an edition param; otherwise keep prior preference. document.addEventListener("turbo:load", function(){ try{ var onDigest = location.pathname.indexOf('/digests/') === 0; if(onDigest) return; var url = new URL(location.href); if(url.searchParams.has('edition')){ var ed = url.searchParams.get('edition'); localStorage.setItem('preferredEdition', (ed && ed !== 'global') ? ed : 'global'); } }catch(e){} }); </script> <div data-controller="modal" data-bluesky-auth-target="modal" id="bluesky-pds-modal"> <div class="hidden fixed inset-0 z-50 overflow-y-auto" data-modal-target="panel"> <div class="flex min-h-screen items-center justify-center p-4"> <div class="fixed inset-0 bg-black/80 transition-opacity" data-action="click->modal#close"></div> <div class="relative bg-white dark:bg-black rounded-2xl w-full max-w-md p-8 shadow-2xl" data-action="click->modal#stop"> <button type="button" class="absolute top-4 right-4 text-gray-400 hover:text-black dark:hover:text-white transition" data-action="click->modal#close"> <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <line x1="18" y1="6" x2="6" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line> </svg> </button> <div class="flex justify-center mb-6"> <img alt="Lightnews" class="logo-light h-12 w-12" src="/assets/lightnews_black-993b5cbd.png" /> <img alt="Lightnews" class="logo-dark h-12 w-12" src="/assets/lightnews-f13a19bd.png" /> </div> <h2 class="text-2xl font-bold text-center text-black dark:text-white mb-3">Connect Bluesky</h2> <p class="text-center text-sm text-gray-600 dark:text-gray-300 mb-6"> Enter your Bluesky handle and app password to unlock posting, likes, and your Following feed. </p> <div class="mb-4 hidden text-sm text-red-500" data-bluesky-auth-target="error"></div> <form data-bluesky-auth-target="form" class="space-y-4" action="/auth/bluesky/pds_session" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="i7UOwkW8lAeOlXhCU7DPQIAsBt6QQIPgqIf0wwL_OLFNV2wubQxtEMPZ0BAk7qJB_BcAyTAPqSK11kFRclW-HA" autocomplete="off" /> <div> <label class="block text-sm font-semibold text-black dark:text-white mb-2" for="identifier">Handle or email</label> <input placeholder="@you.bsky.social" autocomplete="username" required="required" class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" data-bluesky-auth-target="identifierInput" type="text" name="identifier" id="identifier" /> </div> <div> <label class="block text-sm font-semibold text-black dark:text-white mb-2" for="password">App password</label> <input placeholder="xxxx-xxxx-xxxx-xxxx" autocomplete="current-password" required="required" class="w-full px-4 py-3 bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg text-black dark:text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-transparent" data-bluesky-auth-target="passwordInput" type="password" name="password" id="password" /> </div> <button type="submit" class="w-full btn btn-primary py-3 font-semibold" data-bluesky-auth-target="submitButton"> Connect Bluesky </button> </form> <p class="mt-5 text-xs text-center text-gray-500 dark:text-gray-400"> Need an app password? Open Bluesky, go to Settings > App passwords, and create a new one. </p> </div> </div> </div> </div> <!-- Bluesky OAuth Modal - Shows when logged-out user tries Bluesky action --> <div id="bluesky-oauth-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" data-bluesky-auth-target="oauthModal"> <div class="flex min-h-screen items-center justify-center p-4"> <!-- Backdrop --> <div class="fixed inset-0 bg-black/80 transition-opacity" data-action="click->bluesky-auth#closeOAuthModal"></div> <!-- Modal Content --> <div class="relative bg-white dark:bg-black rounded-2xl w-full max-w-md p-8 shadow-2xl" data-action="click->bluesky-auth#stopPropagation"> <!-- Close Button --> <button data-action="click->bluesky-auth#closeOAuthModal" class="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"> <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <line x1="18" y1="6" x2="6" y2="18"></line> <line x1="6" y1="6" x2="18" y2="18"></line> </svg> </button> <!-- Logo --> <div class="flex justify-center mb-6"> <img alt="Lightnews" class="logo-light h-12 w-12" src="/assets/lightnews_black-993b5cbd.png" /> <img alt="Lightnews" class="logo-dark h-12 w-12" src="/assets/lightnews-f13a19bd.png" /> </div> <!-- Title --> <h2 class="text-black dark:text-white text-3xl font-bold text-center mb-3">Connect with Bluesky</h2> <!-- Description --> <p class="text-center text-base text-gray-600 dark:text-gray-300 mb-6"> Sign in with your Bluesky account to unlock posting, likes, and your Following feed. </p> <!-- Buttons --> <div class="space-y-3"> <!-- Sign In Button - Grey --> <a class="bsky-signin-btn w-full flex items-center justify-center gap-3 px-6 py-3 bg-gray-600 hover:bg-gray-700 font-semibold rounded transition-colors border border-gray-600 text-white" data-turbo="false" data-bluesky-auth-target="signinButton" href="/auth/bluesky/start?return_to=%2Fbsky%2Fpaulgreg.bsky.social%3Ftab%3Dmedia"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" style="color: #1185fe !important;"> <path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.038.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 01-.415-.056c.14.018.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z" fill="#1185fe"/> </svg> <span>Sign in</span> </a> <!-- Create Account Button - Blue --> <a class="bsky-signup-btn w-full flex items-center justify-center gap-3 px-6 py-3 bg-blue-600 hover:bg-blue-700 font-semibold rounded transition-colors border border-blue-600" data-turbo="false" data-bluesky-auth-target="signupButton" style="color: #ffffff !important;" href="/auth/bluesky/start?return_to=%2Fbsky%2Fpaulgreg.bsky.social%3Ftab%3Dmedia"> <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" style="color: #ffffff !important;"> <path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.038.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 01-.415-.056c.14.018.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z" fill="currentColor"/> </svg> <span style="color: #ffffff !important;">Create an account</span> </a> </div> </div> </div> </div> </body> </html>