feat: リアクションを取り消せるように

This commit is contained in:
usbharu 2024-09-08 23:00:59 +09:00
parent 8c3ff077d8
commit 5eb3bc3704
Signed by: usbharu
GPG Key ID: 6556747BF94EEBC8
9 changed files with 124 additions and 11 deletions

View File

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

View File

@ -0,0 +1,50 @@
package dev.usbharu.hideout.core.application.reaction
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.post.PostId
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 org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
@Service
class UserRemoveReactionApplicationService(
transaction: Transaction,
private val customEmojiRepository: CustomEmojiRepository,
private val reactionRepository: ReactionRepository,
private val unicodeEmojiService: UnicodeEmojiService
) :
LocalUserAbstractApplicationService<RemoveReaction, Unit>(
transaction, logger
) {
override suspend fun internalExecute(command: RemoveReaction, principal: LocalUser) {
val postId = PostId(command.postId)
val customEmoji = command.customEmojiId?.let { customEmojiRepository.findById(it) }
val unicodeEmoji = if (unicodeEmojiService.isUnicodeEmoji(command.unicodeEmoji)) {
command.unicodeEmoji
} else {
""
}
val reaction =
reactionRepository.findByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
postId,
principal.actorId,
customEmoji?.id,
unicodeEmoji
)
?: throw IllegalArgumentException("Reaction $postId ${principal.actorId} ${customEmoji?.id} $unicodeEmoji not found.")
reaction.delete()
reactionRepository.delete(reaction)
}
companion object {
private val logger = LoggerFactory.getLogger(UserRemoveReactionApplicationService::class.java)
}
}

View File

@ -32,6 +32,10 @@ class Reaction(
return id.hashCode() return id.hashCode()
} }
fun delete() {
addDomainEvent(ReactionEventFactory(this).createEvent(ReactionEvent.DELETE))
}
companion object { companion object {
fun create( fun create(
id: ReactionId, id: ReactionId,

View File

@ -15,5 +15,12 @@ interface ReactionRepository {
unicodeEmoji: String unicodeEmoji: String
): Boolean ): Boolean
suspend fun findByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
postId: PostId,
actorId: ActorId,
customEmojiId: CustomEmojiId?,
unicodeEmoji: String
): Reaction?
suspend fun delete(reaction: Reaction) suspend fun delete(reaction: Reaction)
} }

View File

@ -168,7 +168,7 @@ class ExposedPostRepository(
Posts.id eq id.id Posts.id eq id.id
} }
.let(postQueryMapper::map) .let(postQueryMapper::map)
.first() .firstOrNull()
} }
override suspend fun findAllById(ids: List<PostId>): List<Post> { override suspend fun findAllById(ids: List<PostId>): List<Post> {

View File

@ -82,6 +82,21 @@ class ExposedReactionRepository(override val domainEventPublisher: DomainEventPu
} }
} }
override suspend fun findByPostIdAndActorIdAndCustomEmojiIdOrUnicodeEmoji(
postId: PostId,
actorId: ActorId,
customEmojiId: CustomEmojiId?,
unicodeEmoji: String
): Reaction? {
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)))
}.limit(1).singleOrNull()?.toReaction()
}
}
companion object { companion object {
private val logger = LoggerFactory.getLogger(ExposedReactionRepository::class.java) private val logger = LoggerFactory.getLogger(ExposedReactionRepository::class.java)
} }

View File

@ -5,7 +5,9 @@ import dev.usbharu.hideout.core.application.instance.GetLocalInstanceApplication
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.CreateReaction
import dev.usbharu.hideout.core.application.reaction.RemoveReaction
import dev.usbharu.hideout.core.application.reaction.UserCreateReactionApplicationService import dev.usbharu.hideout.core.application.reaction.UserCreateReactionApplicationService
import dev.usbharu.hideout.core.application.reaction.UserRemoveReactionApplicationService
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
@ -19,7 +21,8 @@ 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 private val userCreateReactionApplicationService: UserCreateReactionApplicationService,
private val userRemoveReactionApplicationService: UserRemoveReactionApplicationService
) { ) {
@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 {
@ -48,4 +51,17 @@ class PostsController(
) )
return "redirect:/users/$name/posts/$id" return "redirect:/users/$name/posts/$id"
} }
@PostMapping("/users/{name}/posts/{id}/unfavourite")
suspend fun unfavourite(@PathVariable id: Long, @PathVariable name: String): String {
val principal = springSecurityFormLoginPrincipalContextHolder.getPrincipal()
userRemoveReactionApplicationService.execute(
RemoveReaction(
id,
null,
""
), principal
)
return "redirect:/users/$name/posts/$id"
}
} }

View File

@ -329,5 +329,6 @@ create table if not exists reactions
unicode_emoji varchar(100) not null, unicode_emoji varchar(100) not null,
created_at timestamp not null, created_at timestamp not null,
constraint fk_reactions_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade, constraint fk_reactions_post_id__id foreign key (post_id) references posts (id) on delete cascade on update cascade,
constraint fk_reactions_actor_id__id foreign key (actor_id) references actors (id) on delete cascade on update cascade constraint fk_reactions_actor_id__id foreign key (actor_id) references actors (id) on delete cascade on update cascade,
unique (post_id, actor_id, created_at, unicode_emoji)
); );

View File

@ -44,14 +44,27 @@
<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"*/-->
<form method="post" th:action="@{/users/a/posts/{id}/favourite(id=${post.id})}"> <th:block th:if="${post.favourited}">
<a th:href="${'/publish?reply_to=' + post.id}">Reply</a> <form method="post" th:action="@{/users/a/posts/{id}/unfavourite(id=${post.id})}">
<input type="submit" value="❤"> <a th:href="${'/publish?reply_to=' + post.id}">Reply</a>
<a th:href="${post.apId}"> <input type="submit" value="[❤]">
<time th:datetime="${post.createdAt}" <a th:href="${post.apId}">
th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}"></time> <time th:datetime="${post.createdAt}"
</a> th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}"></time>
</form> </a>
</form>
</th:block>
<th:block th:unless="${post.favourited}">
<form method="post" th:action="@{/users/a/posts/{id}/favourite(id=${post.id})}">
<a th:href="${'/publish?reply_to=' + post.id}">Reply</a>
<input type="submit" value="❤">
<a th:href="${post.apId}">
<time th:datetime="${post.createdAt}"
th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}"></time>
</a>
</form>
</th:block>
</div> </div>