Test CodeReview
Contributing to Devdevian
Thanks for helping improve Devdevian. This is an early development project; focused changes are easier to review and validate.
Before starting
For a bug, describe the page, exact steps, expected result, actual result, browser, and viewport. Remove personal data from screenshots. For a significant feature or architectural change, discuss the proposed behavior in an issue before implementing it.
Follow the code of conduct.
Do not publish security vulnerabilities in public issues. Follow SECURITY.md.
Local setup
Follow the development guide and architecture overview. Use an isolated DB_PATH or npm run demo when experimenting with account deletion, moderation, or migrations. Never commit environment files, databases, uploads, session cookies, OAuth tokens, or production exports.
Implementation guidelines
- Keep the blue/violet palette and support both themes.
- Check permissions on the server; hiding a button does not protect a route.
- Preserve the distinction between public topics and private communities.
- Keep lists bounded and paginated. Respect private access in search, notifications, profiles, and uploads.
- Validate input, use bound SQL parameters, and use transactions for related writes.
- Preserve existing data during migrations. Document storage or configuration changes.
- Add comments for non-obvious constraints, not for every line.
- Keep interfaces usable with keyboard navigation, small touch screens, tablets, and reduced motion.
Validation
Run npm test and npm run build. For affected UI flows, also run npm run test:browser and inspect screenshots. Add a regression check when the change affects permissions, identity, persistence, or another behavior that is not reliably verified visually.
Pull requests
Explain the problem, the resulting behavior, and how you verified it. Attach relevant light/dark and mobile screenshots for visual changes. Identify migrations, configuration changes, and known limitations. Keep unrelated refactors separate.
By contributing, you agree that your contribution is licensed under GPL-3.0-only. Do not submit code or assets you do not have permission to distribute.
import { notifyMentions } from "./mentions.mjs";
import { notificationAllowed } from "./preferences.mjs";
import { scopeSQL, pageRows } from "./spaces.mjs";
// In-app activity only. No email, push service, or external delivery.
export function transaction(db, work) {
export function notify(
db,
{ recipient, actor, post = null, event = null, reply = null, type, key },
) {
if (
!recipient ||
recipient === actor ||
!notificationAllowed(db, recipient, type)
)
return;
db.prepare(
`INSERT OR IGNORE INTO notifications(recipient_id,actor_id,post_id,event_id,reply_id,type,event_key) VALUES(?,?,?,?,?,?,?)`,
).run(recipient, actor, post, event, reply, type, key);
}
// Fan out in one statement so a popular topic does not issue one remote query per follower.
export function notifySubscribers(
db,
{ kind, item, actor, post, reply = null, type, key },
) {
db.prepare(
`INSERT OR IGNORE INTO notifications(recipient_id,actor_id,post_id,reply_id,type,event_key)
SELECT s.user_id,?,?,?,?,? FROM subscriptions s
JOIN users u ON u.id=s.user_id JOIN posts p ON p.id=?
LEFT JOIN user_settings settings ON settings.user_id=s.user_id
WHERE s.kind=? AND s.item=? AND s.user_id<>? AND u.deleted_at IS NULL AND p.removed_at IS NULL
AND (p.community_id IS NULL OR EXISTS(SELECT 1 FROM memberships m WHERE m.user_id=s.user_id AND m.kind='group' AND m.item=p.community_id))
AND COALESCE(json_extract(settings.data,'$.notifications_enabled'),1)=1
AND COALESCE(json_extract(settings.data,'$.notify_subscriptions'),1)=1
AND (?<>'reply' OR COALESCE(json_extract(settings.data,'$.notify_replies'),1)=1)`,
).run(actor, post, reply, type, key, post, kind, String(item), actor, type);
}
export function notifyReply(db, post, replyId, actor, parentId) {
notifySubscribers(db, {
kind: "post",
item: post.id,
actor,
post: post.id,
reply: replyId,
type: "reply",
key: `reply:${replyId}`,
});
const recipients = new Set([post.user_id]);
if (parentId)
recipients.add(
db
.prepare("SELECT user_id FROM replies WHERE id=? AND post_id=?")
.get(parentId, post.id)?.user_id,
);
for (const recipient of recipients)
notify(db, {
recipient,
actor,
post: post.id,
reply: replyId,
type: "reply",
key: `reply:${replyId}`,
});
}
function text(value, max, min = 0) {
return (
typeof value === "string" &&
value.trim().length >= min &&
value.length <= max
);
}