diff --git a/app/routers/round_messages.py b/app/routers/round_messages.py index 6b3a2f3..d0312c1 100644 --- a/app/routers/round_messages.py +++ b/app/routers/round_messages.py @@ -639,6 +639,7 @@ class FeedEntryOut(BaseModel): created_at: str reactions: list[ReactionSummary] = [] comment_count: int = 0 + tags: list[TagOut] = [] @router.get("/feed", response_model=list[FeedEntryOut]) @@ -703,6 +704,7 @@ async def get_feed( entry_ids = [r["id"] for r in rows] reactions_by_id = await _round_message_reactions(conn, entry_ids, user.user_id) comment_counts = await _round_message_comment_counts(conn, entry_ids) + tags_by_id = await _tags_for_messages(conn, "round_message_tag", "round_message_id", entry_ids) return [ FeedEntryOut( id=r["id"], @@ -718,6 +720,7 @@ async def get_feed( created_at=r["created_at"].isoformat(), reactions=reactions_by_id.get(r["id"], []), comment_count=comment_counts.get(r["id"], 0), + tags=tags_by_id.get(r["id"], []), ) for r in rows ] diff --git a/frontend/components/feed.tsx b/frontend/components/feed.tsx index 14a46c6..2e5257d 100644 --- a/frontend/components/feed.tsx +++ b/frontend/components/feed.tsx @@ -5,6 +5,8 @@ import Link from "next/link" import { cn } from "@/lib/utils" import { PostEngagement, type ReactionSummary } from "./post-engagement" +import { TaggedText } from "./mention-input" +import type { Tag } from "@/lib/mentions" import { BottomNav } from "@/components/teecup/bottom-nav" /** Rå feed-oppføring fra backend. */ @@ -22,6 +24,7 @@ export type ApiFeedEntry = { created_at: string // ISO 8601 reactions: ReactionSummary[] comment_count: number + tags: Tag[] } const PAGE_SIZE = 20 @@ -232,13 +235,14 @@ function FeedCard({ entry, currentUserId }: { entry: ApiFeedEntry; currentUserId {/* Tekst */} {entry.body && (

- {entry.body} +

)} void + placeholder?: string + rows?: number + id?: string + className?: string + autoFocus?: boolean + // Videresendes KUN når dropdownen ikke er åpen -- Enter/piltaster skal + // styre forslagslisten når den vises, ikke utløse f.eks. avsenderens + // egen Cmd/Ctrl+Enter-innsending samtidig (post-engagement.tsx). + onKeyDownWhenClosed?: (e: React.KeyboardEvent) => void +} + +export function MentionTextarea({ + roundId, + value, + tags, + onChange, + placeholder, + rows = 3, + id, + className, + autoFocus, + onKeyDownWhenClosed, +}: MentionTextareaProps) { + const textareaRef = useRef(null) + const [mention, setMention] = useState<{ start: number; query: string } | null>(null) + const [suggestions, setSuggestions] = useState([]) + const [loadingSuggestions, setLoadingSuggestions] = useState(false) + const [highlighted, setHighlighted] = useState(0) + // Markørposisjonen som stod da @-forsøket sist ble oppdatert -- trengs + // for å vite hvor SLUTTEN av "@søketekst" er ved faktisk innsetting + // (markøren kan ha rukket å flytte seg videre siden selve deteksjonen). + const cursorAtDetectionRef = useRef(0) + const pendingCursorRef = useRef(null) + + const open = mention !== null + + // Debouncet henting av kandidater -- kun mens dropdownen faktisk er åpen. + useEffect(() => { + if (!open) { + setSuggestions([]) + return + } + let cancelled = false + setLoadingSuggestions(true) + const handle = setTimeout(async () => { + const people = await fetchTaggablePeople(roundId, mention.query) + if (!cancelled) { + setSuggestions(people) + setHighlighted(0) + setLoadingSuggestions(false) + } + }, 200) + return () => { + cancelled = true + clearTimeout(handle) + } + // eslint-disable-next-line react-hooks/exhaustive-deps -- kun query/roundId skal trigge ny henting + }, [open, mention?.query, roundId]) + + // Sett markøren riktig sted ETTER at React har rendret den nye, + // lengre/kortere verdien -- kan ikke gjøres synkront i onChange, siden + //