feat: タイムラインにリアクションを表示するように

This commit is contained in:
usbharu 2024-09-08 16:40:51 +09:00
parent 2b567bb1d5
commit e42919ce3c
Signed by: usbharu
GPG Key ID: 6556747BF94EEBC8
6 changed files with 67 additions and 13 deletions

View File

@ -29,9 +29,9 @@ sealed class Emoji {
abstract fun id(): String abstract fun id(): String
override fun toString(): String { override fun toString(): String {
return "Emoji(" + return "Emoji(" +
"domain='$domain', " + "domain='$domain', " +
"name='$name'" + "name='$name'" +
")" ")"
} }
} }
@ -50,6 +50,10 @@ data class CustomEmoji(
data class UnicodeEmoji( data class UnicodeEmoji(
override val name: String override val name: String
) : Emoji() { ) : Emoji() {
override val domain: Domain = Domain("unicode.org") override val domain: Domain = Companion.domain
override fun id(): String = name override fun id(): String = name
companion object {
val domain = Domain("unicode.org")
}
} }

View File

@ -1,5 +1,6 @@
package dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail package dev.usbharu.hideout.core.domain.model.support.timelineobjectdetail
import dev.usbharu.hideout.core.application.model.Reactions
import dev.usbharu.hideout.core.domain.model.actor.Actor import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.media.Media import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.post.Post import dev.usbharu.hideout.core.domain.model.post.Post
@ -29,7 +30,8 @@ data class TimelineObjectDetail(
val isPureRepost: Boolean, val isPureRepost: Boolean,
val lastUpdateAt: Instant, val lastUpdateAt: Instant,
val hasMediaInRepost: Boolean, val hasMediaInRepost: Boolean,
val warnFilter: List<TimelineObjectWarnFilter> val warnFilter: List<TimelineObjectWarnFilter>,
val reactionsList: List<Reactions>
) { ) {
companion object { companion object {
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -48,7 +50,8 @@ data class TimelineObjectDetail(
repostPostMedias: List<Media>?, repostPostMedias: List<Media>?,
repostPostActor: Actor?, repostPostActor: Actor?,
repostPostActorIconMedia: Media?, repostPostActorIconMedia: Media?,
warnFilter: List<TimelineObjectWarnFilter> warnFilter: List<TimelineObjectWarnFilter>,
reactionsList: List<Reactions>
): TimelineObjectDetail { ): TimelineObjectDetail {
return TimelineObjectDetail( return TimelineObjectDetail(
id = timelineObject.id, id = timelineObject.id,
@ -69,7 +72,8 @@ data class TimelineObjectDetail(
isPureRepost = timelineObject.isPureRepost, isPureRepost = timelineObject.isPureRepost,
lastUpdateAt = timelineObject.lastUpdatedAt, lastUpdateAt = timelineObject.lastUpdatedAt,
hasMediaInRepost = timelineObject.hasMediaInRepost, hasMediaInRepost = timelineObject.hasMediaInRepost,
warnFilter = warnFilter warnFilter = warnFilter,
reactionsList = reactionsList
) )
} }
} }

View File

@ -1,16 +1,18 @@
package dev.usbharu.hideout.core.infrastructure.exposedquery package dev.usbharu.hideout.core.infrastructure.exposedquery
import dev.usbharu.hideout.core.application.model.Reactions import dev.usbharu.hideout.core.application.model.Reactions
import dev.usbharu.hideout.core.domain.model.emoji.UnicodeEmoji
import dev.usbharu.hideout.core.domain.model.post.PostId import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository import dev.usbharu.hideout.core.infrastructure.exposedrepository.AbstractRepository
import dev.usbharu.hideout.core.infrastructure.exposedrepository.CustomEmojis import dev.usbharu.hideout.core.infrastructure.exposedrepository.CustomEmojis
import dev.usbharu.hideout.core.infrastructure.exposedrepository.toCustomEmojiOrNull import dev.usbharu.hideout.core.infrastructure.exposedrepository.toCustomEmojiOrNull
import dev.usbharu.hideout.core.infrastructure.exposedrepository.toReaction import dev.usbharu.hideout.core.infrastructure.exposedrepository.toReaction
import dev.usbharu.hideout.core.query.reactions.ReactionsQueryService import dev.usbharu.hideout.core.query.reactions.ReactionsQueryService
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.*
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.net.URI
import dev.usbharu.hideout.core.infrastructure.exposedrepository.Reactions as ExposedrepositoryReactions import dev.usbharu.hideout.core.infrastructure.exposedrepository.Reactions as ExposedrepositoryReactions
@Repository @Repository
@ -33,6 +35,35 @@ class ExposedReactionsQueryService : ReactionsQueryService, AbstractRepository()
} }
} }
override suspend fun findAllByPostIdIn(postIds: List<PostId>): List<Reactions> {
return query {
val actorIdsQuery =
ExposedrepositoryReactions.actorId.castTo<String>(VarCharColumnType()).groupConcat(",", true)
ExposedrepositoryReactions.leftJoin(CustomEmojis)
.select(
ExposedrepositoryReactions.postId,
ExposedrepositoryReactions.postId.count(),
ExposedrepositoryReactions.customEmojiId.max(),
ExposedrepositoryReactions.unicodeEmoji.max(),
actorIdsQuery
)
.where { ExposedrepositoryReactions.postId inList postIds.map { it.id } }
.groupBy(ExposedrepositoryReactions.postId)
.map {
Reactions(
it[ExposedrepositoryReactions.postId],
it[ExposedrepositoryReactions.postId.count()].toInt(),
it.getOrNull(CustomEmojis.name) ?: it[ExposedrepositoryReactions.unicodeEmoji],
it.getOrNull(CustomEmojis.domain) ?: UnicodeEmoji.domain.domain,
it.getOrNull(CustomEmojis.url)?.let { it1 -> URI.create(it1) },
it[actorIdsQuery].split(",").mapNotNull { it.toLongOrNull() }
)
}
}
}
override val logger: Logger override val logger: Logger
get() = Companion.logger get() = Companion.logger

View File

@ -1,5 +1,6 @@
package dev.usbharu.hideout.core.infrastructure.timeline package dev.usbharu.hideout.core.infrastructure.timeline
import dev.usbharu.hideout.core.application.model.Reactions
import dev.usbharu.hideout.core.domain.model.actor.Actor import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorId import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.filter.Filter import dev.usbharu.hideout.core.domain.model.filter.Filter
@ -243,8 +244,8 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
val actors = val actors =
getActors( getActors(
timelineObjectList.map { it.postActorId } + timelineObjectList.map { it.postActorId } +
timelineObjectList.mapNotNull { it.repostActorId } + timelineObjectList.mapNotNull { it.repostActorId } +
timelineObjectList.mapNotNull { it.replyActorId } timelineObjectList.mapNotNull { it.replyActorId }
) )
val postMap = posts.associate { post -> val postMap = posts.associate { post ->
@ -253,9 +254,11 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
val mediaMap = getMedias( val mediaMap = getMedias(
posts.flatMap { it.mediaIds } + posts.flatMap { it.mediaIds } +
actors.mapNotNull { it.value.icon } actors.mapNotNull { it.value.icon }
) )
val reactions = getReactions(posts.map { it.id })
return PaginationList( return PaginationList(
timelineObjectList.mapNotNull<TimelineObject, TimelineObjectDetail> { timelineObjectList.mapNotNull<TimelineObject, TimelineObjectDetail> {
val timelineUserDetail = userDetails[it.userDetailId] ?: return@mapNotNull null val timelineUserDetail = userDetails[it.userDetailId] ?: return@mapNotNull null
@ -268,6 +271,7 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
val repost = postMap[it.repostId] val repost = postMap[it.repostId]
val repostMedias = repost?.post?.mediaIds?.mapNotNull { mediaId -> mediaMap[mediaId] } val repostMedias = repost?.post?.mediaIds?.mapNotNull { mediaId -> mediaMap[mediaId] }
val repostActor = actors[it.repostActorId] val repostActor = actors[it.repostActorId]
val reactionsList = reactions[it.postId].orEmpty()
TimelineObjectDetail.of( TimelineObjectDetail.of(
timelineObject = it, timelineObject = it,
timelineUserDetail = timelineUserDetail, timelineUserDetail = timelineUserDetail,
@ -288,7 +292,8 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
filterResult.filter.id, filterResult.filter.id,
filterResult.matchedKeyword filterResult.matchedKeyword
) )
} },
reactionsList = reactionsList
) )
}, },
timelineObjectList.lastOrNull()?.postId, timelineObjectList.lastOrNull()?.postId,
@ -300,5 +305,7 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
protected abstract suspend fun getMedias(mediaIds: List<MediaId>): Map<MediaId, Media> protected abstract suspend fun getMedias(mediaIds: List<MediaId>): Map<MediaId, Media>
protected abstract suspend fun getReactions(postIds: List<PostId>): Map<PostId, List<Reactions>>
protected abstract suspend fun getUserDetails(userDetailIdList: List<UserDetailId>): Map<UserDetailId, UserDetail> protected abstract suspend fun getUserDetails(userDetailIdList: List<UserDetailId>): Map<UserDetailId, UserDetail>
} }

View File

@ -1,5 +1,6 @@
package dev.usbharu.hideout.core.infrastructure.timeline package dev.usbharu.hideout.core.infrastructure.timeline
import dev.usbharu.hideout.core.application.model.Reactions
import dev.usbharu.hideout.core.config.DefaultTimelineStoreConfig import dev.usbharu.hideout.core.config.DefaultTimelineStoreConfig
import dev.usbharu.hideout.core.domain.model.actor.Actor import dev.usbharu.hideout.core.domain.model.actor.Actor
import dev.usbharu.hideout.core.domain.model.actor.ActorId import dev.usbharu.hideout.core.domain.model.actor.ActorId
@ -32,6 +33,7 @@ import dev.usbharu.hideout.core.domain.service.filter.FilterDomainService
import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import dev.usbharu.hideout.core.external.timeline.ReadTimelineOption import dev.usbharu.hideout.core.external.timeline.ReadTimelineOption
import dev.usbharu.hideout.core.query.reactions.ReactionsQueryService
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.time.Instant import java.time.Instant
@ -49,7 +51,8 @@ open class DefaultTimelineStore(
private val userDetailRepository: UserDetailRepository, private val userDetailRepository: UserDetailRepository,
private val actorRepository: ActorRepository, private val actorRepository: ActorRepository,
private val mediaRepository: MediaRepository, private val mediaRepository: MediaRepository,
private val postIPostReadAccessControl: IPostReadAccessControl private val postIPostReadAccessControl: IPostReadAccessControl,
private val reactionsQueryService: ReactionsQueryService,
) : AbstractTimelineStore(idGenerateService) { ) : AbstractTimelineStore(idGenerateService) {
override suspend fun getTimelines(actorId: ActorId): List<Timeline> { override suspend fun getTimelines(actorId: ActorId): List<Timeline> {
return timelineRepository.findByIds( return timelineRepository.findByIds(
@ -157,6 +160,10 @@ open class DefaultTimelineStore(
override suspend fun getMedias(mediaIds: List<MediaId>): Map<MediaId, Media> = override suspend fun getMedias(mediaIds: List<MediaId>): Map<MediaId, Media> =
mediaRepository.findByIds(mediaIds).associateBy { it.id } mediaRepository.findByIds(mediaIds).associateBy { it.id }
override suspend fun getReactions(postIds: List<PostId>): Map<PostId, List<Reactions>> {
return reactionsQueryService.findAllByPostIdIn(postIds).groupBy { PostId(it.postId) }
}
override suspend fun getUserDetails(userDetailIdList: List<UserDetailId>): Map<UserDetailId, UserDetail> = override suspend fun getUserDetails(userDetailIdList: List<UserDetailId>): Map<UserDetailId, UserDetail> =
userDetailRepository.findAllById(userDetailIdList).associateBy { it.id } userDetailRepository.findAllById(userDetailIdList).associateBy { it.id }
} }

View File

@ -5,4 +5,5 @@ import dev.usbharu.hideout.core.domain.model.post.PostId
interface ReactionsQueryService { interface ReactionsQueryService {
suspend fun findAllByPostId(postId: PostId): List<Reactions> suspend fun findAllByPostId(postId: PostId): List<Reactions>
suspend fun findAllByPostIdIn(postIds: List<PostId>): List<Reactions>
} }