mirror of https://github.com/usbharu/Hideout.git
feat: リアクションしたかを確認できるように
This commit is contained in:
parent
e42919ce3c
commit
d4aaad3fb6
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
package dev.usbharu.hideout.core.application.reaction
|
||||||
|
|
||||||
|
data class CreateReaction(val postId: Long, val customEmojiId: Long?, val unicodeEmoji: String)
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package dev.usbharu.hideout.core.domain.service.emoji
|
||||||
|
|
||||||
|
interface UnicodeEmojiService {
|
||||||
|
fun isUnicodeEmoji(emoji: String): Boolean
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() }
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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" }
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue