This is a submission for the DEV Weekend Challenge: Community
This is the fastest I've ever made something functional.
- b r e a t h e s -
The Community
For the past couple years, I have actively used dev as a resource. Learning, discovering, and trying things way out of my league. Because of this, I have learned more than I would have on my own with no other support. When you enter the realm of web development and you don't really have anyone close to you doing it, it's the wild west. Dev has provided a place for all walks of life to learn and push forward to discover new things. For this reason, I chose Dev as my community!
The one thing I noticed about dev is there there was no p2p chat. I thought it would be cool if users could send simple messages together about cool projects, ect. If the chat is implemented by way of an optional extension, then we are never forcing people to deal with the chat. It's truly a personal decision.
Thinking from a community standpoint - I wanted to include all the meme monday memes inside the gif picker in the chat. YES you can probably find your meme you posted 2 months ago. This way, I'm using community data, for the community, to continue enhancing the experience.
What I Built
A free open source messaging app for dev members to communicate with each other without having to share personal information. It's shipped as an extension but has a pop-out option for ease of use.
Demo
Code
AnnaVi11arrea1
/
dev_messages
3rd party messaging app that works with Dev.to
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 (380px 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. 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, |
Meme Search Implementation
Arguably, the most important feature. XD
I wanted not to just pull in the memes, but to allow a user to search them. The best way I figured was to use the alt tags as search parameters. Many of us are nice enough to put alt tags on our stuff, and so I was able to leverage that to get a sort of lazy search going. Hey, it works. I thought that was a little cool and different so that is the snippet I'll be sharing below:
/* 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 <= 6; page++) {
try {
const res = await fetch(
`https://dev.to/api/articles?username=ben&per_page=100&page=${page}`
);
if (!res.ok) break;
const data = await res.json();
if (!data.length) break;
for (const a of data) {
if (/meme\s+monday/i.test(a.title)) articles.push(a);
}
if (data.length < 100) break;
} catch { break; }
}
/* āā Step 2: collect images with dedup āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
const seen = new Set();
const images = [];
const add = (url, title) => {
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 < articles.length; i++) {
this._extractImages(commentThreads[i], articles[i].title, add);
}
this._cache = images;
return images;
},
/* Recursively walk comment tree and pull every <img src="ā¦"> with its own alt */
_extractImages(comments, articleTitle, add) {
for (const comment of comments) {
const imgTags = (comment.body_html || '').matchAll(/<img([^>]+)>/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);
}
}
},
};
How I Built It
I very quickly realized that I was going to need a secure database so users could keep their chat history. I'm using Neon's free tier for this project. Give it a whirl! XD. Requests from the extension go to Vercel over HTTPS. Vercel connects to Neon with an encrypted connection. The database is never exposed to the internet, and it is only queried by Vercel's serverless functions.
I used copilot CLI to quickly build up a working template - I would not be able to build something so quickly without it. This is not my first chrome extension, but it is the 'fanciest' one I have made to date.
I tested with my unpacked files in the 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.
Features:
Created with the mindset of reducing spam and annoyances
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
My extension is getting reviewed by google currently... whish me luck! haha
A user must have the extension to test it. Guess I've made myself a guinea pig haven't I. (hides, š )



Top comments (2)
Haha! 𤣠Collecting memes from Meme Monday is definitely what all DEV Community users want. Also, this app has robust security. Very cool app!
Thank you! That means alot!