📨 Pop-out Messaging Extension: Dev Whisper
Source: Dev.to
DEV Weekend Challenge: Community
“This is the fastest I’ve ever made something functional.”
– b r e a t h e s –
For the past couple of years I’ve used dev.to as a learning resource—discovering, experimenting, and tackling projects far outside my comfort zone. Without a local community of developers, dev.to felt like the “wild west,” but it provided a welcoming place for people from all walks of life to learn and push forward. That’s why I consider dev.to my community.
The Idea
I noticed dev.to has no peer‑to‑peer chat. It would be great if users could send simple messages about cool projects, memes, etc. By implementing the chat as an optional Chrome extension, we never force anyone to use it—it’s a personal choice.
From a community standpoint I also wanted to:
- Include all the Meme Monday memes inside a GIF picker in the chat.
- Let users search those memes (using the
alttags as search keywords).
The result is a free, open‑source messaging app for dev.to members that:
- Works without sharing personal information.
- Is shipped as a Chrome extension with an optional pop‑out window for ease of use.
Dev.to Messages (Unofficial 3rd‑Party Extension)
A Chrome extension that adds private direct messaging to dev.to, with built‑in link safety, spam reporting, and a first‑contact approval system.
File Structure
dev_messages/
├── manifest.json # MV3 manifest
├── background.js # Service worker – updates unread badge
├── content.js # Injected into dev.to – detects user, adds Message buttons
├── popup.html # All UI views & modals
├── popup.css # Dev.to‑inspired styles (380 px popup)
├── popup.js # Full SPA app logic
├── js/
│ ├── storage.js # chrome.storage.local wrapper
│ ├── eligibility.js # Dev.to API eligibility check
│ └── linkSafety.js # URL spoofing detection + safe renderer
└── icons/ # 16 / 48 / 128 px PNG icons
Features
| Feature | Details |
|---|---|
| Account eligibility | Checks joined_at & articles_count via the dev.to public API. Accounts must be at least 30 days old and have ≥ 1 published post before they can send or receive messages. |
| First‑contact approval | When someone messages you for the first time, you must approve the conversation before any further messages are exchanged. |
| Link safety | Detects URL spoofing and renders links in a sandboxed, safe view. |
| Spam reporting | Users can flag abusive messages; reports are sent to the extension’s backend for review. |
| Badge notifications | Unread message count appears on the extension icon. |
| Pop‑out window | Optional separate window for a larger chat experience. |
| GIF/Meme picker | Pulls Meme Monday images (including community‑submitted memes) and lets users search by alt text. |
View the source on GitHub →
Meme‑Monday GIF Picker
I wanted more than just a static list of memes—I wanted a searchable picker. By using the alt attributes that many community members add to their images, I could implement a lightweight search.
/* gifPicker.js – Fetches meme images from Ben Halpern's Meme Monday posts,
including every image posted in the comments (community memes). */
const GifPicker = {
_cache: null,
async fetchImages() {
if (this._cache) return this._cache;
/* ── Step 1: collect all Meme Monday articles ─────────────────────── */
const articles = [];
for (let page = 1; page {
if (!url || seen.has(url)) return;
seen.add(url);
images.push({ url, title });
};
/* Cover images first so they appear at the top of the grid */
for (const a of articles) {
if (a.cover_image) add(a.cover_image, a.title);
}
/* ── Step 3: fetch all comment threads in parallel ─────────────────── */
const commentThreads = await Promise.all(
articles.map(a =>
fetch(`https://dev.to/api/comments?a_id=${a.id}`)
.then(r => (r.ok ? r.json() : []))
.catch(() => [])
)
);
for (let i = 0; i ]+)>/gi);
for (const m of imgTags) {
const attrs = m[1];
const srcM = attrs.match(/src="([^"]+)"/i);
const altM = attrs.match(/alt="([^"]*)"/i);
if (srcM) {
const alt = altM?.[1]?.trim() || articleTitle;
add(srcM[1], alt);
}
}
if (comment.children?.length) {
this._extractImages(comment.children, articleTitle, add);
}
}
},
};
Backend & Persistence
Chat history needs a secure database. I’m using Neon’s free tier:
- Extension → Vercel (HTTPS) → Neon (encrypted connection).
- The database is never exposed to the public internet; only Vercel’s serverless functions can query it.
I scaffolded the backend with the Copilot CLI, which let me spin up a working template in minutes. This isn’t my first Chrome extension, but it’s definitely the fanciest I’ve built so far.
Happy hacking! 🎉
Extension Overview
To date, I tested the unpacked files in Google Chrome extensions. I was lucky enough to test functionality with another dev user – that was amazingly helpful. A big thank you to @trickell for being awesome.
I had fun making a settings and options area for users, which includes colors and text sizes for increased accessibility.
Created with the mindset of reducing spam and annoyances.
Core Features
-
Account eligibility – Checks
joined_at&articles_countvia the Dev.to public API.- Account must be at least 30 days old and have at least 1 published post before sending or receiving messages.
-
First‑contact approval – When someone messages you for the first time, you see a banner with their opening message and must hit Approve or Deny before the conversation unlocks.
-
Flag / report spam – Every incoming message has a 🚩 button. Clicking it opens a reason picker (spam, phishing, harassment, inappropriate, other) and marks the message as reported.
-
Link spoofing detection – Before any link opens,
LinkSafety.isSpoofed()compares the visible URL text against the actualhrefhostname. A red alert is shown if they differ. -
Link safety warning – Every link in every message routes through a warning modal that displays the full destination URL before opening it in a new tab.
-
Local storage persistence – All conversations, approvals, flags, and user data are stored in
chrome.storage.local. The extension must be installed and active to read messages. -
Unread badge – The extension icon shows a live unread count, updated by the background service worker whenever storage changes.
Current Status
My extension is getting reviewed by Google currently… wish me luck! haha
A user must have the extension to test it. Guess I’ve made myself a guinea pig, haven’t I? (hides, 😅)