feat: リアクションしたかを確認できるように

This commit is contained in:
usbharu 2024-09-08 22:26:50 +09:00
parent e42919ce3c
commit d4aaad3fb6
Signed by: usbharu
GPG Key ID: 6556747BF94EEBC8
19 changed files with 224 additions and 42 deletions

View File

@ -9,6 +9,7 @@ import dev.usbharu.hideout.core.domain.model.media.Media
import dev.usbharu.hideout.core.domain.model.media.MediaRepository import dev.usbharu.hideout.core.domain.model.media.MediaRepository
import dev.usbharu.hideout.core.domain.model.post.PostId import dev.usbharu.hideout.core.domain.model.post.PostId
import dev.usbharu.hideout.core.domain.model.post.PostRepository import dev.usbharu.hideout.core.domain.model.post.PostRepository
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import dev.usbharu.hideout.core.domain.model.support.principal.Principal import dev.usbharu.hideout.core.domain.model.support.principal.Principal
import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl
import dev.usbharu.hideout.core.query.reactions.ReactionsQueryService import dev.usbharu.hideout.core.query.reactions.ReactionsQueryService
@ -23,6 +24,7 @@ class GetPostDetailApplicationService(
private val mediaRepository: MediaRepository, private val mediaRepository: MediaRepository,
private val iPostReadAccessControl: IPostReadAccessControl, private val iPostReadAccessControl: IPostReadAccessControl,
private val reactionsQueryService: ReactionsQueryService, private val reactionsQueryService: ReactionsQueryService,
private val reactionRepository: ReactionRepository,
) : AbstractApplicationService<GetPostDetail, PostDetail>( ) : AbstractApplicationService<GetPostDetail, PostDetail>(
transaction, transaction,
logger logger
@ -42,6 +44,13 @@ class GetPostDetailApplicationService(
val reactions = reactionsQueryService.findAllByPostId(post.id) val reactions = reactionsQueryService.findAllByPostId(post.id)
val favourited = reactionRepository.existsByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
post.id,
principal.actorId,
null,
""
)
return PostDetail.of( return PostDetail.of(
post = post, post = post,
actor = actor, actor = actor,
@ -50,7 +59,8 @@ class GetPostDetailApplicationService(
reply = post.replyId?.let { fetchChild(it, actor, iconMedia, principal) }, reply = post.replyId?.let { fetchChild(it, actor, iconMedia, principal) },
repost = post.repostId?.let { fetchChild(it, actor, iconMedia, principal) }, repost = post.repostId?.let { fetchChild(it, actor, iconMedia, principal) },
moveTo = post.moveTo?.let { fetchChild(it, actor, iconMedia, principal) }, moveTo = post.moveTo?.let { fetchChild(it, actor, iconMedia, principal) },
reactionsList = reactions reactionsList = reactions,
favourited
) )
} }
@ -78,7 +88,8 @@ class GetPostDetailApplicationService(
actor = first, actor = first,
iconMedia = third, iconMedia = third,
mediaList = mediaList, mediaList = mediaList,
reactionsList = emptyList() reactionsList = emptyList(),
favourited = false
) )
} }

View File

@ -25,7 +25,8 @@ data class PostDetail(
val deleted: Boolean, val deleted: Boolean,
val mediaDetailList: List<MediaDetail>, val mediaDetailList: List<MediaDetail>,
val moveTo: PostDetail?, val moveTo: PostDetail?,
val reactionsList: List<Reactions> val reactionsList: List<Reactions>,
val favourited: Boolean
) { ) {
companion object { companion object {
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -37,7 +38,8 @@ data class PostDetail(
reply: PostDetail? = null, reply: PostDetail? = null,
repost: PostDetail? = null, repost: PostDetail? = null,
moveTo: PostDetail? = null, moveTo: PostDetail? = null,
reactionsList: List<Reactions> reactionsList: List<Reactions>,
favourited: Boolean
): PostDetail { ): PostDetail {
return PostDetail( return PostDetail(
id = post.id.id, id = post.id.id,
@ -56,7 +58,8 @@ data class PostDetail(
deleted = false, deleted = false,
mediaDetailList = mediaList.map { MediaDetail.of(it) }, mediaDetailList = mediaList.map { MediaDetail.of(it) },
moveTo = moveTo, moveTo = moveTo,
reactionsList = reactionsList reactionsList = reactionsList,
favourited = favourited
) )
} }
} }

View File

@ -0,0 +1,3 @@
package dev.usbharu.hideout.core.application.reaction
data class CreateReaction(val postId: Long, val customEmojiId: Long?, val unicodeEmoji: String)

View File

@ -0,0 +1,65 @@
package dev.usbharu.hideout.core.application.reaction
import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.application.shared.LocalUserAbstractApplicationService
import dev.usbharu.hideout.core.application.shared.Transaction
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiRepository
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.PostRepository
import dev.usbharu.hideout.core.domain.model.reaction.Reaction
import dev.usbharu.hideout.core.domain.model.reaction.ReactionId
import dev.usbharu.hideout.core.domain.model.reaction.ReactionRepository
import dev.usbharu.hideout.core.domain.model.support.principal.LocalUser
import dev.usbharu.hideout.core.domain.service.emoji.UnicodeEmojiService
import dev.usbharu.hideout.core.domain.service.post.IPostReadAccessControl
import dev.usbharu.hideout.core.domain.shared.id.IdGenerateService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.time.Instant
@Service
class UserCreateReactionApplicationService(
transaction: Transaction,
private val idGenerateService: IdGenerateService,
private val reactionRepository: ReactionRepository,
private val postReadAccessControl: IPostReadAccessControl,
private val postRepository: PostRepository,
private val customEmojiRepository: CustomEmojiRepository,
private val unicodeEmojiService: UnicodeEmojiService
) :
LocalUserAbstractApplicationService<CreateReaction, Unit>(
transaction, logger
) {
override suspend fun internalExecute(command: CreateReaction, principal: LocalUser) {
val postId = PostId(command.postId)
val post = postRepository.findById(postId) ?: throw IllegalArgumentException("Post $postId not found.")
if (postReadAccessControl.isAllow(post, principal).not()) {
throw PermissionDeniedException()
}
val customEmoji = command.customEmojiId?.let { customEmojiRepository.findById(it) }
val unicodeEmoji = if (unicodeEmojiService.isUnicodeEmoji(command.unicodeEmoji)) {
command.unicodeEmoji
} else {
""
}
val reaction = Reaction.create(
id = ReactionId(idGenerateService.generateId()),
postId = postId,
actorId = principal.actorId,
customEmojiId = customEmoji?.id,
unicodeEmoji = UnicodeEmoji(unicodeEmoji),
createdAt = Instant.now()
)
reactionRepository.save(reaction)
}
companion object {
private val logger = LoggerFactory.getLogger(UserCreateReactionApplicationService::class.java)
}
}

View File

@ -49,6 +49,7 @@ class ReadTimelineApplicationService(
it.replyPostActorIconMedia, it.replyPostActorIconMedia,
it.replyPostMedias.orEmpty(), it.replyPostMedias.orEmpty(),
reactionsList = emptyList(), reactionsList = emptyList(),
favourited = false,
) )
} else { } else {
null null
@ -61,7 +62,8 @@ class ReadTimelineApplicationService(
actor = it.repostPostActor!!, actor = it.repostPostActor!!,
iconMedia = it.repostPostActorIconMedia, iconMedia = it.repostPostActorIconMedia,
mediaList = it.repostPostMedias.orEmpty(), mediaList = it.repostPostMedias.orEmpty(),
reactionsList = emptyList() reactionsList = emptyList(),
favourited = false
) )
} else { } else {
null null
@ -74,7 +76,8 @@ class ReadTimelineApplicationService(
mediaList = it.postMedias, mediaList = it.postMedias,
reply = reply, reply = reply,
repost = repost, repost = repost,
reactionsList = emptyList() reactionsList = emptyList(),
favourited = it.favourited
) )
} }

View File

@ -1,10 +1,19 @@
package dev.usbharu.hideout.core.domain.model.reaction package dev.usbharu.hideout.core.domain.model.reaction
import dev.usbharu.hideout.core.domain.model.actor.ActorId
import dev.usbharu.hideout.core.domain.model.emoji.CustomEmojiId
import dev.usbharu.hideout.core.domain.model.post.PostId import dev.usbharu.hideout.core.domain.model.post.PostId
interface ReactionRepository { interface ReactionRepository {
suspend fun save(reaction: Reaction): Reaction suspend fun save(reaction: Reaction): Reaction
suspend fun findById(reactionId: ReactionId): Reaction? suspend fun findById(reactionId: ReactionId): Reaction?
suspend fun findByPostId(postId: PostId): List<Reaction> suspend fun findByPostId(postId: PostId): List<Reaction>
suspend fun existsByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
postId: PostId,
actorId: ActorId,
customEmojiId: CustomEmojiId?,
unicodeEmoji: String
): Boolean
suspend fun delete(reaction: Reaction) suspend fun delete(reaction: Reaction)
} }

View File

@ -31,7 +31,8 @@ data class TimelineObjectDetail(
val lastUpdateAt: Instant, val lastUpdateAt: Instant,
val hasMediaInRepost: Boolean, val hasMediaInRepost: Boolean,
val warnFilter: List<TimelineObjectWarnFilter>, val warnFilter: List<TimelineObjectWarnFilter>,
val reactionsList: List<Reactions> val reactionsList: List<Reactions>,
val favourited: Boolean
) { ) {
companion object { companion object {
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -51,7 +52,8 @@ data class TimelineObjectDetail(
repostPostActor: Actor?, repostPostActor: Actor?,
repostPostActorIconMedia: Media?, repostPostActorIconMedia: Media?,
warnFilter: List<TimelineObjectWarnFilter>, warnFilter: List<TimelineObjectWarnFilter>,
reactionsList: List<Reactions> reactionsList: List<Reactions>,
favourited: Boolean
): TimelineObjectDetail { ): TimelineObjectDetail {
return TimelineObjectDetail( return TimelineObjectDetail(
id = timelineObject.id, id = timelineObject.id,
@ -73,7 +75,8 @@ data class TimelineObjectDetail(
lastUpdateAt = timelineObject.lastUpdatedAt, lastUpdateAt = timelineObject.lastUpdatedAt,
hasMediaInRepost = timelineObject.hasMediaInRepost, hasMediaInRepost = timelineObject.hasMediaInRepost,
warnFilter = warnFilter, warnFilter = warnFilter,
reactionsList = reactionsList reactionsList = reactionsList,
favourited = favourited
) )
} }
} }

View File

@ -33,8 +33,8 @@ class TimelineObject(
hasMediaInRepost: Boolean, hasMediaInRepost: Boolean,
lastUpdatedAt: Instant, lastUpdatedAt: Instant,
var warnFilters: List<TimelineObjectWarnFilter>, var warnFilters: List<TimelineObjectWarnFilter>,
var favourited: Boolean
) { ) {
var isPureRepost = isPureRepost var isPureRepost = isPureRepost
private set private set
var visibleActors = visibleActors var visibleActors = visibleActors
@ -61,9 +61,9 @@ class TimelineObject(
lastUpdatedAt = Instant.now() lastUpdatedAt = Instant.now()
isPureRepost = isPureRepost =
post.repostId != null && post.repostId != null &&
post.replyId == null && post.replyId == null &&
post.text.isEmpty() && post.text.isEmpty() &&
post.overview?.overview.isNullOrEmpty() post.overview?.overview.isNullOrEmpty()
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) } warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) }
} }
@ -82,7 +82,8 @@ class TimelineObject(
timeline: Timeline, timeline: Timeline,
post: Post, post: Post,
replyActorId: ActorId?, replyActorId: ActorId?,
filterResults: List<FilterResult> filterResults: List<FilterResult>,
favourited: Boolean
): TimelineObject { ): TimelineObject {
return TimelineObject( return TimelineObject(
id = timelineObjectId, id = timelineObjectId,
@ -102,7 +103,8 @@ class TimelineObject(
visibleActors = post.visibleActors.toList(), visibleActors = post.visibleActors.toList(),
hasMediaInRepost = false, hasMediaInRepost = false,
lastUpdatedAt = Instant.now(), lastUpdatedAt = Instant.now(),
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) } warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) },
favourited = favourited
) )
} }
@ -113,7 +115,8 @@ class TimelineObject(
post: Post, post: Post,
replyActorId: ActorId?, replyActorId: ActorId?,
repost: Post, repost: Post,
filterResults: List<FilterResult> filterResults: List<FilterResult>,
favourited: Boolean
): TimelineObject { ): TimelineObject {
require(post.repostId == repost.id) require(post.repostId == repost.id)
@ -130,15 +133,16 @@ class TimelineObject(
repostActorId = repost.actorId, repostActorId = repost.actorId,
visibility = post.visibility, visibility = post.visibility,
isPureRepost = repost.mediaIds.isEmpty() && isPureRepost = repost.mediaIds.isEmpty() &&
repost.overview == null && repost.overview == null &&
repost.content == PostContent.empty && repost.content == PostContent.empty &&
repost.replyId == null, repost.replyId == null,
mediaIds = post.mediaIds, mediaIds = post.mediaIds,
emojiIds = post.emojiIds, emojiIds = post.emojiIds,
visibleActors = post.visibleActors.toList(), visibleActors = post.visibleActors.toList(),
hasMediaInRepost = repost.mediaIds.isNotEmpty(), hasMediaInRepost = repost.mediaIds.isNotEmpty(),
lastUpdatedAt = Instant.now(), lastUpdatedAt = Instant.now(),
warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) } warnFilters = filterResults.map { TimelineObjectWarnFilter(it.filter.id, it.matchedKeyword) },
favourited = favourited
) )
} }
} }

View File

@ -0,0 +1,5 @@
package dev.usbharu.hideout.core.domain.service.emoji
interface UnicodeEmojiService {
fun isUnicodeEmoji(emoji: String): Boolean
}

View File

@ -0,0 +1,12 @@
package dev.usbharu.hideout.core.infrastructure.emojikt
import Emojis
import dev.usbharu.hideout.core.domain.service.emoji.UnicodeEmojiService
import org.springframework.stereotype.Service
@Service
class EmojiKtUnicodeEmojiService : UnicodeEmojiService {
override fun isUnicodeEmoji(emoji: String): Boolean {
return Emojis.allEmojis.singleOrNull { it.char == emoji } != null
}
}

View File

@ -46,7 +46,7 @@ class ExposedReactionsQueryService : ReactionsQueryService, AbstractRepository()
ExposedrepositoryReactions.postId, ExposedrepositoryReactions.postId,
ExposedrepositoryReactions.postId.count(), ExposedrepositoryReactions.postId.count(),
ExposedrepositoryReactions.customEmojiId.max(), ExposedrepositoryReactions.customEmojiId.max(),
ExposedrepositoryReactions.unicodeEmoji.max(), ExposedrepositoryReactions.unicodeEmoji.max<String, String>(),
actorIdsQuery actorIdsQuery
) )
.where { ExposedrepositoryReactions.postId inList postIds.map { it.id } } .where { ExposedrepositoryReactions.postId inList postIds.map { it.id } }
@ -55,7 +55,8 @@ class ExposedReactionsQueryService : ReactionsQueryService, AbstractRepository()
Reactions( Reactions(
it[ExposedrepositoryReactions.postId], it[ExposedrepositoryReactions.postId],
it[ExposedrepositoryReactions.postId.count()].toInt(), it[ExposedrepositoryReactions.postId.count()].toInt(),
it.getOrNull(CustomEmojis.name) ?: it[ExposedrepositoryReactions.unicodeEmoji], it.getOrNull(CustomEmojis.name)
?: it[ExposedrepositoryReactions.unicodeEmoji.max<String, String>()]!!,
it.getOrNull(CustomEmojis.domain) ?: UnicodeEmoji.domain.domain, it.getOrNull(CustomEmojis.domain) ?: UnicodeEmoji.domain.domain,
it.getOrNull(CustomEmojis.url)?.let { it1 -> URI.create(it1) }, it.getOrNull(CustomEmojis.url)?.let { it1 -> URI.create(it1) },
it[actorIdsQuery].split(",").mapNotNull { it.toLongOrNull() } it[actorIdsQuery].split(",").mapNotNull { it.toLongOrNull() }

View File

@ -42,10 +42,10 @@ class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractReposi
.select(Posts.columns) .select(Posts.columns)
.where { .where {
Posts.visibility eq Visibility.PUBLIC.name or Posts.visibility eq Visibility.PUBLIC.name or
(Posts.visibility eq Visibility.UNLISTED.name) or (Posts.visibility eq Visibility.UNLISTED.name) or
(Posts.visibility eq Visibility.DIRECT.name and (PostsVisibleActors.actorId eq principal.actorId.id)) or (Posts.visibility eq Visibility.DIRECT.name and (PostsVisibleActors.actorId eq principal.actorId.id)) or
(Posts.visibility eq Visibility.FOLLOWERS.name and (Relationships.blocking eq false and (relationshipsAlias[Relationships.following] eq true))) or (Posts.visibility eq Visibility.FOLLOWERS.name and (Relationships.blocking eq false and (relationshipsAlias[Relationships.following] eq true))) or
(Posts.actorId eq principal.actorId.id) (Posts.actorId eq principal.actorId.id)
} }
.alias("authorized_table") .alias("authorized_table")
} }
@ -61,6 +61,10 @@ class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractReposi
.leftJoin(iconMedia, { Actors.icon }, { iconMedia[Media.id] }) .leftJoin(iconMedia, { Actors.icon }, { iconMedia[Media.id] })
.leftJoin(PostsMedia, { authorizedQuery[Posts.id] }, { PostsMedia.postId }) .leftJoin(PostsMedia, { authorizedQuery[Posts.id] }, { PostsMedia.postId })
.leftJoin(Media, { PostsMedia.mediaId }, { Media.id }) .leftJoin(Media, { PostsMedia.mediaId }, { Media.id })
.leftJoin(Reactions,
{ authorizedQuery[Posts.id] },
{ Reactions.postId },
{ Reactions.id isDistinctFrom principal.actorId.id })
.selectAll() .selectAll()
.where { authorizedQuery[Posts.id] inList idList.map { it.id } } .where { authorizedQuery[Posts.id] inList idList.map { it.id } }
.groupBy { it[authorizedQuery[Posts.id]] } .groupBy { it[authorizedQuery[Posts.id]] }
@ -69,7 +73,7 @@ class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractReposi
toPostDetail(it.first(), authorizedQuery, iconMedia).copy( toPostDetail(it.first(), authorizedQuery, iconMedia).copy(
mediaDetailList = it.mapNotNull { resultRow -> mediaDetailList = it.mapNotNull { resultRow ->
resultRow.toMediaOrNull()?.let { it1 -> MediaDetail.of(it1) } resultRow.toMediaOrNull()?.let { it1 -> MediaDetail.of(it1) }
} }, favourited = it.any { it.getOrNull(Reactions.actorId) != null }
) )
} }
} }
@ -101,7 +105,8 @@ class ExposedUserTimelineQueryService : UserTimelineQueryService, AbstractReposi
deleted = it[authorizedQuery[Posts.deleted]], deleted = it[authorizedQuery[Posts.deleted]],
mediaDetailList = emptyList(), mediaDetailList = emptyList(),
moveTo = null, moveTo = null,
emptyList() emptyList(),
favourited = false
) )
} }

View File

@ -32,6 +32,7 @@ class ExposedReactionRepository(override val domainEventPublisher: DomainEventPu
it[Reactions.actorId] = reaction.actorId.id it[Reactions.actorId] = reaction.actorId.id
it[Reactions.customEmojiId] = reaction.customEmojiId?.emojiId it[Reactions.customEmojiId] = reaction.customEmojiId?.emojiId
it[Reactions.unicodeEmoji] = reaction.unicodeEmoji.name it[Reactions.unicodeEmoji] = reaction.unicodeEmoji.name
it[Reactions.createdAt] = reaction.createdAt
} }
onComplete { onComplete {
update(reaction) update(reaction)
@ -56,6 +57,20 @@ class ExposedReactionRepository(override val domainEventPublisher: DomainEventPu
} }
} }
override suspend fun existsByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
postId: PostId,
actorId: ActorId,
customEmojiId: CustomEmojiId?,
unicodeEmoji: String
): Boolean {
return query {
Reactions.selectAll().where {
Reactions.postId.eq(postId.id).and(Reactions.actorId eq actorId.id)
.and((Reactions.customEmojiId eq customEmojiId?.emojiId or (Reactions.unicodeEmoji eq unicodeEmoji)))
}.empty().not()
}
}
override suspend fun delete(reaction: Reaction) { override suspend fun delete(reaction: Reaction) {
return query { return query {
Reactions.deleteWhere { Reactions.deleteWhere {

View File

@ -133,7 +133,8 @@ data class SpringDataMongoTimelineObject(
val visibleActors: List<Long>, val visibleActors: List<Long>,
val hasMediaInRepost: Boolean, val hasMediaInRepost: Boolean,
val lastUpdatedAt: Long, val lastUpdatedAt: Long,
val warnFilters: List<SpringDataMongoTimelineObjectWarnFilter> val warnFilters: List<SpringDataMongoTimelineObjectWarnFilter>,
val favourited: Boolean
) { ) {
fun toTimelineObject(): TimelineObject { fun toTimelineObject(): TimelineObject {
@ -155,7 +156,8 @@ data class SpringDataMongoTimelineObject(
visibleActors = visibleActors.map { ActorId(it) }, visibleActors = visibleActors.map { ActorId(it) },
hasMediaInRepost = hasMediaInRepost, hasMediaInRepost = hasMediaInRepost,
lastUpdatedAt = Instant.ofEpochSecond(lastUpdatedAt), lastUpdatedAt = Instant.ofEpochSecond(lastUpdatedAt),
warnFilters = warnFilters.map { it.toTimelineObjectWarnFilter() } warnFilters = warnFilters.map { it.toTimelineObjectWarnFilter() },
favourited = favourited
) )
} }
@ -179,7 +181,8 @@ data class SpringDataMongoTimelineObject(
visibleActors = timelineObject.visibleActors.map { it.id }, visibleActors = timelineObject.visibleActors.map { it.id },
hasMediaInRepost = timelineObject.hasMediaInRepost, hasMediaInRepost = timelineObject.hasMediaInRepost,
lastUpdatedAt = timelineObject.lastUpdatedAt.epochSecond, lastUpdatedAt = timelineObject.lastUpdatedAt.epochSecond,
warnFilters = timelineObject.warnFilters.map { SpringDataMongoTimelineObjectWarnFilter.of(it) } warnFilters = timelineObject.warnFilters.map { SpringDataMongoTimelineObjectWarnFilter.of(it) },
favourited = timelineObject.favourited
) )
} }
} }

View File

@ -76,7 +76,8 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
post = post, post = post,
replyActorId = replyActorId, replyActorId = replyActorId,
repost = repost, repost = repost,
filterResults = applyFilters.filterResults filterResults = applyFilters.filterResults,
favourited = false
) )
} }
@ -85,7 +86,8 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
timeline = timeline, timeline = timeline,
post = post, post = post,
replyActorId = replyActorId, replyActorId = replyActorId,
filterResults = applyFilters.filterResults filterResults = applyFilters.filterResults,
favourited = false
) )
} }
@ -293,7 +295,8 @@ abstract class AbstractTimelineStore(private val idGenerateService: IdGenerateSe
filterResult.matchedKeyword filterResult.matchedKeyword
) )
}, },
reactionsList = reactionsList reactionsList = reactionsList,
favourited = it.favourited
) )
}, },
timelineObjectList.lastOrNull()?.postId, timelineObjectList.lastOrNull()?.postId,

View File

@ -4,18 +4,22 @@ import dev.usbharu.hideout.core.application.exception.PermissionDeniedException
import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplicationService
import dev.usbharu.hideout.core.application.post.GetPostDetail import dev.usbharu.hideout.core.application.post.GetPostDetail
import dev.usbharu.hideout.core.application.post.GetPostDetailApplicationService import dev.usbharu.hideout.core.application.post.GetPostDetailApplicationService
import dev.usbharu.hideout.core.application.reaction.CreateReaction
import dev.usbharu.hideout.core.application.reaction.UserCreateReactionApplicationService
import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder import dev.usbharu.hideout.core.infrastructure.springframework.SpringSecurityFormLoginPrincipalContextHolder
import org.springframework.security.access.AccessDeniedException import org.springframework.security.access.AccessDeniedException
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.ui.Model import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
@Controller @Controller
class PostsController( class PostsController(
private val getPostDetailApplicationService: GetPostDetailApplicationService, private val getPostDetailApplicationService: GetPostDetailApplicationService,
private val springSecurityFormLoginPrincipalContextHolder: SpringSecurityFormLoginPrincipalContextHolder, private val springSecurityFormLoginPrincipalContextHolder: SpringSecurityFormLoginPrincipalContextHolder,
private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService private val getLocalInstanceApplicationService: GetLocalInstanceApplicationService,
private val userCreateReactionApplicationService: UserCreateReactionApplicationService
) { ) {
@GetMapping("/users/{name}/posts/{id}") @GetMapping("/users/{name}/posts/{id}")
suspend fun postById(@PathVariable id: Long, model: Model): String { suspend fun postById(@PathVariable id: Long, model: Model): String {
@ -31,4 +35,17 @@ class PostsController(
return "postById" return "postById"
} }
@PostMapping("/users/{name}/posts/{id}/favourite")
suspend fun favourite(@PathVariable id: Long, @PathVariable name: String): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
userCreateReactionApplicationService.execute(
CreateReaction(
id,
null,
""
), principal
)
return "redirect:/users/$name/posts/$id"
}
} }

View File

@ -44,10 +44,14 @@
<div class="post-controller" th:fragment="single-post-controller(post)"> <div class="post-controller" th:fragment="single-post-controller(post)">
<!--/*@thymesVar id="post" type="dev.usbharu.hideout.core.application.post.PostDetail"*/--> <!--/*@thymesVar id="post" type="dev.usbharu.hideout.core.application.post.PostDetail"*/-->
<a th:href="${'/publish?reply_to=' + post.id}">Reply</a> <form method="post" th:action="@{/users/a/posts/{id}/favourite(id=${post.id})}">
<a th:href="${post.apId}"> <a th:href="${'/publish?reply_to=' + post.id}">Reply</a>
<time th:datetime="${post.createdAt}" th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}"></time> <input type="submit" value="❤">
</a> <a th:href="${post.apId}">
<time th:datetime="${post.createdAt}"
th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}"></time>
</a>
</form>
</div> </div>

View File

@ -0,0 +1,16 @@
package dev.usbharu.hideout.core.infrastructure.emojikt
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
class EmojiKtUnicodeEmojiServiceTest {
@ParameterizedTest
@ValueSource(strings = ["", "👱", "☠️", "⁉️"])
fun 絵文字の判定ができる(s: String) {
assertTrue(EmojiKtUnicodeEmojiService().isUnicodeEmoji(s))
}
}

View File

@ -103,7 +103,7 @@ mongodb-kotlin-coroutine = { module = "org.mongodb:mongodb-driver-kotlin-corouti
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "5.4.0" } mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "5.4.0" }
http-signature = { module = "dev.usbharu:http-signature", version = "1.0.0" } http-signature = { module = "dev.usbharu:http-signature", version = "1.0.0" }
emoji-kt = { module = "dev.usbharu:emoji-kt", version = "2.0.0" } emoji-kt = { module = "dev.usbharu:emoji-kt", version = "2.0.1" }
logback-ecs-encoder = { module = "co.elastic.logging:logback-ecs-encoder", version = "1.6.0" } logback-ecs-encoder = { module = "co.elastic.logging:logback-ecs-encoder", version = "1.6.0" }