diff --git a/backend/auth.js b/backend/auth.js index 28c4fea..32bd274 100644 --- a/backend/auth.js +++ b/backend/auth.js @@ -14,7 +14,7 @@ const REFBOARD_API_KEY = process.env.REFBOARD_API_KEY || ''; function generateToken(user) { return jwt.sign( - { id: user.id, email: user.email, role: user.role }, + { id: user.id, email: user.email, role: user.role, username: user.username, display_name: user.display_name }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } ); diff --git a/backend/db.js b/backend/db.js index e860f1c..57fd47b 100644 --- a/backend/db.js +++ b/backend/db.js @@ -527,33 +527,6 @@ function updateImageMedia(imageId, { posterAssetKey, duration, nativeWidth, nati `).run(posterAssetKey || null, duration || null, nativeWidth || null, nativeHeight || null, imageId); } -// --------------------- -// Vote helpers -// --------------------- -function getVotesByBoard(boardId) { - return db.prepare('SELECT * FROM object_votes WHERE board_id = ?').all(boardId); -} - -const _toggleVoteTx = db.transaction((boardId, objectId, userId) => { - const existing = db.prepare( - 'SELECT 1 FROM object_votes WHERE board_id = ? AND object_id = ? AND user_id = ?' - ).get(boardId, objectId, userId); - - if (existing) { - db.prepare('DELETE FROM object_votes WHERE board_id = ? AND object_id = ? AND user_id = ?') - .run(boardId, objectId, userId); - return false; // vote removed - } else { - db.prepare('INSERT INTO object_votes (board_id, object_id, user_id) VALUES (?, ?, ?)') - .run(boardId, objectId, userId); - return true; // vote added - } -}); - -function toggleVote(boardId, objectId, userId) { - return _toggleVoteTx(boardId, objectId, userId); -} - // --------------------- // Thread helpers // --------------------- @@ -681,6 +654,4 @@ module.exports = { incrementThreadCommentCount, decrementThreadCommentCount, // Comments getCommentsByThread, getCommentsByBoard, getComment, createComment, updateComment, deleteComment, - // Votes - getVotesByBoard, toggleVote, }; diff --git a/backend/routes/threads.js b/backend/routes/threads.js index b866cb0..068dc34 100644 --- a/backend/routes/threads.js +++ b/backend/routes/threads.js @@ -14,9 +14,19 @@ const { deleteComment, incrementThreadCommentCount, decrementThreadCommentCount, + getUserById, } = require('../db'); const { hasCollectionRole, resolveBoard } = require('./board-access'); +function resolveAuthorName(reqUser) { + if (reqUser.display_name || reqUser.username) { + return reqUser.display_name || reqUser.username; + } + // Fallback: look up from DB (old JWT tokens lack these fields) + const dbUser = getUserById(reqUser.id); + return dbUser ? (dbUser.display_name || dbUser.username || dbUser.email) : 'Unknown'; +} + const router = Router(); router.use(authMiddleware); @@ -86,7 +96,7 @@ router.post('/:boardId/threads', (req, res) => { id: commentId, threadId, userId, - authorName: req.user.display_name || req.user.username, + authorName: resolveAuthorName(req.user), authorColor: null, content: content.trim(), }); @@ -198,7 +208,7 @@ router.post('/:boardId/threads/:threadId/comments', (req, res) => { id: commentId, threadId: req.params.threadId, userId, - authorName: req.user.display_name || req.user.username, + authorName: resolveAuthorName(req.user), authorColor: null, content: content.trim(), }); diff --git a/backend/routes/votes.js b/backend/routes/votes.js deleted file mode 100644 index 1444ab8..0000000 --- a/backend/routes/votes.js +++ /dev/null @@ -1,54 +0,0 @@ -const { Router } = require('express'); -const { authMiddleware } = require('../auth'); -const { getVotesByBoard, toggleVote } = require('../db'); -const { resolveBoard } = require('./board-access'); - -const router = Router(); - -router.use(authMiddleware); - -// GET /api/boards/:boardId/votes — all votes for board -router.get('/:boardId/votes', (req, res) => { - try { - const result = resolveBoard(req, res, 'viewer'); - if (!result) return; - - const votes = getVotesByBoard(req.params.boardId); - return res.json({ votes }); - } catch (err) { - console.error('[votes] list error:', err); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -// POST /api/boards/:boardId/votes — toggle vote -router.post('/:boardId/votes', (req, res) => { - try { - const result = resolveBoard(req, res, 'viewer'); - if (!result) return; - - const { object_id } = req.body; - if (!object_id) { - return res.status(400).json({ error: 'object_id is required' }); - } - - const active = toggleVote(req.params.boardId, object_id, req.user.id); - - const io = req.app.get('io'); - if (io) { - io.to(`board:${req.params.boardId}`).emit('vote:toggle', { - boardId: req.params.boardId, - objectId: object_id, - userId: req.user.id, - active, - }); - } - - return res.json({ active }); - } catch (err) { - console.error('[votes] toggle error:', err); - return res.status(500).json({ error: 'Internal server error' }); - } -}); - -module.exports = router; diff --git a/backend/server.js b/backend/server.js index b36ca71..07a8405 100644 --- a/backend/server.js +++ b/backend/server.js @@ -101,7 +101,6 @@ const uploadRoutes = require('./routes/upload'); const adminRoutes = require('./routes/admin'); const mmBridgeRoutes = require('./routes/mattermost-bridge'); const threadRoutes = require('./routes/threads'); -const voteRoutes = require('./routes/votes'); app.use('/api/auth', authRoutes); app.use('/api/collections', collectionRoutes); @@ -110,7 +109,6 @@ app.use('/api/upload', uploadRoutes); app.use('/api/admin', adminRoutes); app.use('/api/boards', mmBridgeRoutes); app.use('/api/boards', threadRoutes); -app.use('/api/boards', voteRoutes); // Public shared collection route (no auth required) app.get('/api/c/:shareToken', (req, res) => { diff --git a/frontend/src/canvas/PinOverlay.ts b/frontend/src/canvas/PinOverlay.ts index 00cfbc9..c070f5b 100644 --- a/frontend/src/canvas/PinOverlay.ts +++ b/frontend/src/canvas/PinOverlay.ts @@ -9,7 +9,6 @@ const PIN_COLOR_RESOLVED = 0x666666; export class PinOverlay extends Container { private _pins = new Map(); - private _voteBadges = new Map(); private _pool: Graphics[] = []; private _viewport: Viewport; private _scene: SceneManager; @@ -41,11 +40,9 @@ export class PinOverlay extends Container { refresh(showResolved = false) { const scale = 1 / this._viewport.scale.x; - // Return current pins and badges to pool + // Return current pins to pool for (const gfx of this._pins.values()) this._release(gfx); - for (const gfx of this._voteBadges.values()) this._release(gfx); this._pins.clear(); - this._voteBadges.clear(); // ── Thread pins ── for (const thread of this._store.threads.values()) { @@ -89,35 +86,6 @@ export class PinOverlay extends Container { if (!gfx.parent) this.addChild(gfx); this._pins.set(thread.id, gfx); } - - // ── Vote badges ── - for (const [objectId, voters] of this._store.votes) { - if (voters.size === 0) continue; - const item = this._scene.items.get(objectId); - if (!item || !item.displayObject) continue; - - const bounds = item.displayObject.getBounds(); - const wx = bounds.x + bounds.width; - const wy = bounds.y + bounds.height; - - const gfx = this._acquire(); - const pw = 24 * scale; - const ph = 16 * scale; - const pr = 4 * scale; - gfx.roundRect(-pw / 2, -ph / 2, pw, ph, pr); - gfx.fill({ color: 0x2a3a50, alpha: 0.9 }); - gfx.position.set(wx - 16 * scale, wy - 4 * scale); - - const label = new Text({ - text: `${voters.size}`, - style: new TextStyle({ fontSize: 9 * scale, fill: '#4a9eff', fontWeight: 'bold' }), - }); - label.anchor.set(0.5); - gfx.addChild(label); - - if (!gfx.parent) this.addChild(gfx); - this._voteBadges.set(objectId, gfx); - } } getThreadIdAtPoint(worldX: number, worldY: number): string | null { @@ -134,10 +102,8 @@ export class PinOverlay extends Container { override destroy(options?: any) { for (const gfx of this._pins.values()) gfx.destroy(); - for (const gfx of this._voteBadges.values()) gfx.destroy(); for (const gfx of this._pool) gfx.destroy(); this._pins.clear(); - this._voteBadges.clear(); this._pool = []; super.destroy(options); } diff --git a/frontend/src/components/FeedbackPanel.tsx b/frontend/src/components/FeedbackPanel.tsx index ff6b383..0d29c01 100644 --- a/frontend/src/components/FeedbackPanel.tsx +++ b/frontend/src/components/FeedbackPanel.tsx @@ -101,16 +101,6 @@ export default function FeedbackPanel({ } catch {} }, [boardId, token, apiFetch]); - const toggleVote = useCallback(async (objectId: string) => { - try { - await apiFetch(`/api/boards/${boardId}/votes`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, - body: JSON.stringify({ object_id: objectId }), - }); - } catch {} - }, [boardId, token, apiFetch]); - const createThread = useCallback(async () => { if (!newCommentText.trim() || !selectedObjectId) return; try { @@ -166,15 +156,6 @@ export default function FeedbackPanel({ {expandedThread.status === 'resolved' ? 'Resolved' : 'Open'} - {onJumpToObject && ( {firstComment && (
diff --git a/frontend/src/hooks/useCanvasSetup.ts b/frontend/src/hooks/useCanvasSetup.ts index 52ebb7e..23b7740 100644 --- a/frontend/src/hooks/useCanvasSetup.ts +++ b/frontend/src/hooks/useCanvasSetup.ts @@ -304,8 +304,8 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { } }); - // ── Annotations: load threads + votes, wire socket events ── - const token = localStorage.getItem('token'); + // ── Annotations: load threads, wire socket events ── + const token = localStorage.getItem('refboard_token'); if (token) { fetch(`/api/boards/${resolvedBoardId}/threads`, { headers: { Authorization: `Bearer ${token}` }, @@ -315,15 +315,6 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { if (data.threads) annotationStoreRef.current?.loadThreads(data.threads); }) .catch((err) => console.error('[annotations] load threads error:', err)); - - fetch(`/api/boards/${resolvedBoardId}/votes`, { - headers: { Authorization: `Bearer ${token}` }, - }) - .then((r) => r.json()) - .then((data) => { - if (data.votes) annotationStoreRef.current?.loadVotes(data.votes); - }) - .catch((err) => console.error('[annotations] load votes error:', err)); } socket.on('thread:add', (data: any) => { @@ -344,10 +335,6 @@ export function useCanvasSetup(deps: CanvasSetupDeps) { socket.on('comment:delete', (data: any) => { annotationStoreRef.current?.onCommentDelete(data.threadId, data.commentId); }); - socket.on('vote:toggle', (data: any) => { - annotationStoreRef.current?.onVoteToggle(data.objectId, data.userId, data.active); - }); - // ── Pin overlay (hidden by default, shown in Review Mode) ── const pinOverlay = new PinOverlay(viewport, scene, annotationStoreRef.current!); pinOverlay.visible = false; diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx index 358d457..e8d6959 100644 --- a/frontend/src/pages/Editor.tsx +++ b/frontend/src/pages/Editor.tsx @@ -675,7 +675,7 @@ export default function Editor({ isPublicView }: EditorProps) { selectedObjectId={selectedLayerIds.length === 1 ? selectedLayerIds[0] : null} userId={user.id} boardId={resolvedBoardId} - token={localStorage.getItem('token') || ''} + token={localStorage.getItem('refboard_token') || ''} canvasObjects={canvasObjectMap} onError={(msg) => showToast(msg)} onJumpToObject={(objectId) => { diff --git a/frontend/src/stores/annotationStore.ts b/frontend/src/stores/annotationStore.ts index 0bcbdb1..ff9d9b2 100644 --- a/frontend/src/stores/annotationStore.ts +++ b/frontend/src/stores/annotationStore.ts @@ -28,18 +28,10 @@ export interface Comment { created_at: string; } -export interface Vote { - board_id: string; - object_id: string; - user_id: string; -} - type Listener = () => void; export class AnnotationStore { threads = new Map(); - /** objectId → Set */ - votes = new Map>(); private _listeners = new Set(); private _version = 0; @@ -67,18 +59,8 @@ export class AnnotationStore { this._notify(); } - loadVotes(votes: Vote[]) { - this.votes.clear(); - for (const v of votes) { - if (!this.votes.has(v.object_id)) this.votes.set(v.object_id, new Set()); - this.votes.get(v.object_id)!.add(v.user_id); - } - this._notify(); - } - clear() { this.threads.clear(); - this.votes.clear(); this._notify(); } @@ -133,16 +115,16 @@ export class AnnotationStore { this._notify(); } - onVoteToggle(objectId: string, userId: string, active: boolean) { - if (!this.votes.has(objectId)) this.votes.set(objectId, new Set()); - const set = this.votes.get(objectId)!; - if (active) set.add(userId); else set.delete(userId); - if (set.size === 0) this.votes.delete(objectId); - this._notify(); - } - // ── Convenience getters ── + /** Get 1-based sequential pin number for a thread on this board */ + getPinNumber(threadId: string): number { + const sorted = Array.from(this.threads.values()) + .sort((a, b) => a.created_at.localeCompare(b.created_at)); + const idx = sorted.findIndex((t) => t.id === threadId); + return idx + 1; + } + getThreadsForObject(objectId: string): Thread[] { const result: Thread[] = []; for (const t of this.threads.values()) { @@ -151,11 +133,4 @@ export class AnnotationStore { return result; } - getVoteCount(objectId: string): number { - return this.votes.get(objectId)?.size ?? 0; - } - - hasVoted(objectId: string, userId: string): boolean { - return this.votes.get(objectId)?.has(userId) ?? false; - } } diff --git a/frontend/src/utils/authorColors.ts b/frontend/src/utils/authorColors.ts new file mode 100644 index 0000000..651ef5a --- /dev/null +++ b/frontend/src/utils/authorColors.ts @@ -0,0 +1,35 @@ +const AUTHOR_COLORS = [ + '#4a9eff', // blue + '#f97316', // orange + '#22c55e', // green + '#a855f7', // purple + '#ef4444', // red + '#06b6d4', // cyan + '#eab308', // yellow + '#ec4899', // pink +]; + +const AUTHOR_COLORS_HEX = [ + 0x4a9eff, 0xf97316, 0x22c55e, 0xa855f7, + 0xef4444, 0x06b6d4, 0xeab308, 0xec4899, +]; + +function hashCode(str: string): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0; + } + return Math.abs(hash); +} + +export function getAuthorColor(userId: string): string { + return AUTHOR_COLORS[hashCode(userId) % AUTHOR_COLORS.length]; +} + +export function getAuthorColorHex(userId: string): number { + return AUTHOR_COLORS_HEX[hashCode(userId) % AUTHOR_COLORS_HEX.length]; +} + +export function getAuthorInitial(name: string): string { + return (name || '?').charAt(0).toUpperCase(); +}