55
66import type { MenuProps } from "antd" ;
77import {
8+ Badge ,
89 Button ,
910 Divider ,
1011 Dropdown ,
@@ -29,6 +30,7 @@ import {
2930 useRef ,
3031 useMemo ,
3132 useState ,
33+ useTypedRedux ,
3234} from "@cocalc/frontend/app-framework" ;
3335import { Icon , Loading } from "@cocalc/frontend/components" ;
3436import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown" ;
@@ -47,6 +49,14 @@ import {
4749 getThreadRootDate ,
4850} from "./utils" ;
4951import { ALL_THREADS_KEY , useThreadList } from "./threads" ;
52+ import type { ThreadListItem } from "./threads" ;
53+
54+ type ThreadMeta = ThreadListItem & {
55+ displayLabel : string ;
56+ hasCustomName : boolean ;
57+ readCount : number ;
58+ unreadCount : number ;
59+ } ;
5060
5161const FILTER_RECENT_NONE = {
5262 value : 0 ,
@@ -114,11 +124,6 @@ const THREAD_ITEM_LABEL_STYLE: React.CSSProperties = {
114124 pointerEvents : "none" ,
115125} as const ;
116126
117- const THREAD_ITEM_COUNT_STYLE : React . CSSProperties = {
118- fontSize : "11px" ,
119- color : "#999" ,
120- } as const ;
121-
122127export function ChatRoom ( {
123128 actions,
124129 project_id,
@@ -127,6 +132,7 @@ export function ChatRoom({
127132 desc,
128133} : EditorComponentProps ) {
129134 const useEditor = useEditorRedux < ChatState > ( { project_id, path } ) ;
135+ const account_id = useTypedRedux ( "account" , "account_id" ) ;
130136 const [ input , setInput ] = useState ( "" ) ;
131137 const search = desc ?. get ( "data-search" ) ?? "" ;
132138 const filterRecentH : number = desc ?. get ( "data-filterRecentH" ) ?? 0 ;
@@ -139,7 +145,37 @@ export function ChatRoom({
139145 const messages = useEditor ( "messages" ) as ChatMessages | undefined ;
140146 const [ filterRecentHCustom , setFilterRecentHCustom ] = useState < string > ( "" ) ;
141147 const [ filterRecentOpen , setFilterRecentOpen ] = useState < boolean > ( false ) ;
142- const threads = useThreadList ( messages ) ;
148+ const rawThreads = useThreadList ( messages ) ;
149+ const threads = useMemo < ThreadMeta [ ] > ( ( ) => {
150+ return rawThreads . map ( ( thread ) => {
151+ const rootMessage = thread . rootMessage ;
152+ const storedName = (
153+ rootMessage ?. get ( "name" ) as string | undefined
154+ ) ?. trim ( ) ;
155+ const hasCustomName = ! ! storedName ;
156+ const displayLabel = storedName || thread . label ;
157+ const readField =
158+ account_id && rootMessage
159+ ? rootMessage . get ( `read-${ account_id } ` )
160+ : null ;
161+ const readValue =
162+ typeof readField === "number"
163+ ? readField
164+ : typeof readField === "string"
165+ ? parseInt ( readField , 10 )
166+ : 0 ;
167+ const readCount =
168+ Number . isFinite ( readValue ) && readValue > 0 ? readValue : 0 ;
169+ const unreadCount = Math . max ( thread . messageCount - readCount , 0 ) ;
170+ return {
171+ ...thread ,
172+ displayLabel,
173+ hasCustomName,
174+ readCount,
175+ unreadCount,
176+ } ;
177+ } ) ;
178+ } , [ rawThreads , account_id ] ) ;
143179 const [ selectedThreadKey , setSelectedThreadKey0 ] = useState < string | null > (
144180 desc ?. get ( "data-selectedThreadKey" ) ?? null ,
145181 ) ;
@@ -190,6 +226,20 @@ export function ChatRoom({
190226 }
191227 } , [ selectedThreadKey ] ) ;
192228
229+ useEffect ( ( ) => {
230+ if ( ! singleThreadView || ! selectedThreadKey ) {
231+ return ;
232+ }
233+ const thread = threads . find ( ( t ) => t . key === selectedThreadKey ) ;
234+ if ( ! thread ) {
235+ return ;
236+ }
237+ if ( thread . unreadCount <= 0 ) {
238+ return ;
239+ }
240+ actions . markThreadRead ?.( thread . key , thread . messageCount ) ;
241+ } , [ singleThreadView , selectedThreadKey , threads , actions ] ) ;
242+
193243 useEffect ( ( ) => {
194244 if ( ! fragmentId || isAllThreadsSelected || messages == null ) {
195245 return ;
@@ -522,12 +572,7 @@ export function ChatRoom({
522572 threads . length === 0
523573 ? [ ]
524574 : threads . map ( ( thread ) => {
525- const { key, label, messageCount, rootMessage } = thread ;
526- const customName = rootMessage ?. get ( "name" ) as string | undefined ;
527- const trimmedName =
528- typeof customName === "string" ? customName . trim ( ) : "" ;
529- const hasCustomName = trimmedName . length > 0 ;
530- const displayLabel = hasCustomName ? trimmedName : label ;
575+ const { key, displayLabel, hasCustomName, unreadCount } = thread ;
531576 const isHovered = hoveredThread === key ;
532577 const showMenu = isHovered || selectedThreadKey === key ;
533578 return {
@@ -549,7 +594,17 @@ export function ChatRoom({
549594 value = { displayLabel }
550595 style = { THREAD_ITEM_LABEL_STYLE }
551596 />
552- < span style = { THREAD_ITEM_COUNT_STYLE } > { messageCount } </ span >
597+ { unreadCount > 0 && ! isHovered && (
598+ < Badge
599+ count = { unreadCount }
600+ size = "small"
601+ overflowCount = { 99 }
602+ style = { {
603+ backgroundColor : COLORS . GRAY_L0 ,
604+ color : COLORS . GRAY_D ,
605+ } }
606+ />
607+ ) }
553608 { showMenu && (
554609 < Dropdown
555610 menu = { threadMenuProps ( key , displayLabel , hasCustomName ) }
0 commit comments