diff --git a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/PhotologScreen.kt b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/PhotologScreen.kt index d6ed6cf..168bfe5 100644 --- a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/PhotologScreen.kt +++ b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/PhotologScreen.kt @@ -671,6 +671,20 @@ private fun TagFaceDialog( private fun ContactList(vm: PhotologViewModel, persons: List, onOpen: (Long) -> Unit, onOpenPicker: () -> Unit, onOpenCommunities: () -> Unit, onOpenFollowups: () -> Unit, onOpenTopics: () -> Unit) { var query by remember { mutableStateOf("") } + // 真实最近联系时间:微信最后一条消息 + 最后一次被提到,取 MAX + val wxTimes by vm.repo.wechatMessages.observeLastContactTimes().collectAsState(emptyList()) + val mentionTimes by vm.repo.mentions.observeLastMentionTimes().collectAsState(emptyList()) + val lastContactMap = remember(persons, wxTimes, mentionTimes) { + val wxMap = wxTimes.associate { it.talker to it.maxTime } + val mnMap = mentionTimes.associate { it.personId to it.maxTime } + persons.associate { p -> + val wxid = p.contactRef?.takeIf { it.startsWith("wechat:") }?.removePrefix("wechat:") + val msgTs = if (wxid != null) wxMap[wxid] ?: 0L else 0L + val mnTs = mnMap[p.id] ?: 0L + p.id to maxOf(msgTs, mnTs) + } + } + val sections = remember(persons, query) { val q = query.trim() val filtered = if (q.isBlank()) persons else persons.filter { p -> @@ -682,9 +696,12 @@ private fun ContactList(vm: PhotologViewModel, persons: List, onOpen: (L .groupBy { sectionLetter(it) } .toSortedMap(compareBy { if (it == "#") "[" else it }) // # 排最后 } - val recent = remember(persons, query) { + val recent = remember(persons, query, lastContactMap) { if (query.isNotBlank()) emptyList() - else persons.filter { it.lastAccess > 0 }.sortedByDescending { it.lastAccess }.take(12) + else persons + .filter { (lastContactMap[it.id] ?: 0L) > 0L } + .sortedByDescending { lastContactMap[it.id] ?: 0L } + .take(12) } val ctx = LocalContext.current @@ -730,7 +747,7 @@ private fun ContactList(vm: PhotologViewModel, persons: List, onOpen: (L Text("★ 最近联系人", style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(horizontal = 16.dp, vertical = 3.dp)) } } - items(recent, key = { "r${it.id}" }) { p -> ContactRow(p) { onOpen(p.id) } } + items(recent, key = { "r${it.id}" }) { p -> ContactRow(p, lastContactMap[p.id] ?: 0L) { onOpen(p.id) } } } sections.forEach { (letter, group) -> stickyHeader { @@ -738,7 +755,7 @@ private fun ContactList(vm: PhotologViewModel, persons: List, onOpen: (L Text(letter, style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(horizontal = 16.dp, vertical = 3.dp)) } } - items(group, key = { it.id }) { p -> ContactRow(p) { onOpen(p.id) } } + items(group, key = { it.id }) { p -> ContactRow(p, lastContactMap[p.id] ?: 0L) { onOpen(p.id) } } } } } @@ -751,7 +768,7 @@ private fun sectionLetter(p: Person): String { } @Composable -private fun ContactRow(p: Person, onClick: () -> Unit) { +private fun ContactRow(p: Person, lastContactTs: Long = 0L, onClick: () -> Unit) { Row( modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(horizontal = 16.dp, vertical = 7.dp), verticalAlignment = Alignment.CenterVertically, @@ -773,6 +790,23 @@ private fun ContactRow(p: Person, onClick: () -> Unit) { ) if (tags.isNotEmpty()) Text(tags.joinToString(" · "), style = MaterialTheme.typography.labelSmall) } + // 最近联系时间(右侧对齐) + if (lastContactTs > 0L) { + val days = ((System.currentTimeMillis() - lastContactTs) / 86_400_000L).toInt() + val label = when { + days == 0 -> "今天" + days == 1 -> "昨天" + days < 30 -> "${days}天前" + days < 365 -> "${days / 30}月前" + else -> "${days / 365}年前" + } + Text( + if (days > 30) "⚠ $label" else label, + style = MaterialTheme.typography.labelSmall, + color = if (days > 30) MaterialTheme.colorScheme.error + else MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } } diff --git a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/Daos.kt b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/Daos.kt index 36d97ed..b0f9453 100644 --- a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/Daos.kt +++ b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/data/Daos.kt @@ -344,6 +344,10 @@ interface MentionDao { @Query("SELECT segmentId, personId FROM mentions") suspend fun allPairs(): List + /** 联系人列表用:每个人最后一次被提到的时间戳。 */ + @Query("SELECT personId, MAX(createdAt) AS maxTime FROM mentions GROUP BY personId") + fun observeLastMentionTimes(): Flow> + /** 某人「被提到」的录音段(join 转写 + 段 + 说话人=谁说的)。 */ @Query( """ @@ -368,6 +372,9 @@ interface MentionDao { data class MentionPair(val segmentId: Long, val personId: Long) +/** 最近联系时间:每个人最后一次被提到的时间戳。 */ +data class PersonMentionTime(val personId: Long, val maxTime: Long) + /** 段元信息(共现重建:按时间切会话 + 声纹在场)。 */ data class SegMeta(val id: Long, val startTs: Long, val speakerId: Long?) @@ -881,6 +888,9 @@ interface MomentDao { fun observeRecent(n: Int): Flow> } +/** 最近联系时间:每个 wxid 最后一条消息的时间戳。 */ +data class TalkerTime(val talker: String, val maxTime: Long) + @Dao interface WechatMessageDao { @Insert @@ -919,6 +929,10 @@ interface WechatMessageDao { /** 时间轴用:最近 n 条私聊(排除群聊)。 */ @Query("SELECT * FROM wechat_messages WHERE talker NOT LIKE '%@chatroom' ORDER BY createTime DESC LIMIT :n") fun observeRecent(n: Int): Flow> + + /** 联系人列表用:每个私聊对象最后一条消息时间(用于最近联系时间排序)。 */ + @Query("SELECT talker, MAX(createTime) AS maxTime FROM wechat_messages WHERE talker NOT LIKE '%@chatroom' GROUP BY talker") + fun observeLastContactTimes(): Flow> } @Dao