[gitlab] IJPL-82619: images uploaded by "/uploads/*" links are rendered in MR notes

* images are rendered in MR timeline entries, diff comments and editor comments (in review mode)

GitOrigin-RevId: e45fa4c7de0436133dcb2dcab020e922d6e5cc95
This commit is contained in:
Valeria Golovina
2025-10-29 22:58:26 +00:00
committed by intellij-monorepo-bot
parent fdd225e48e
commit fabbe79d14
21 changed files with 159 additions and 70 deletions
@@ -36,8 +36,9 @@ class GitLabProjectConnection(
val tokenRefreshFlow: Flow<Unit> = tokenState.drop(1).map { }
val projectData: GitLabProject = GitLabLazyProject(project, scope, apiClient, glMetadata, repo, glProject, currentUser, tokenRefreshFlow)
val imageLoader: GitLabImageLoader = GitLabImageLoader(apiClient, repo.repository.serverPath)
val imageLoader: GitLabImageLoader = GitLabImageLoader(apiClient, repo.repository)
val projectData: GitLabProject = GitLabLazyProject(project, scope, apiClient, glMetadata, repo, glProject, currentUser, tokenRefreshFlow, imageLoader)
val serverVersion: GitLabVersion? = glMetadata?.version
@@ -9,6 +9,7 @@ import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserRestDTO
import org.jetbrains.plugins.gitlab.util.GitLabApiRequestName
import java.awt.Image
import java.net.URI
import java.net.http.HttpResponse
@SinceGitLab("7.0", note = "No exact version")
@@ -33,3 +34,8 @@ suspend fun GitLabApi.loadImage(uri: String): Image {
val request = request(uri).GET().build()
return loadImage(request).body()
}
suspend fun GitLabApi.loadImage(uri: URI): Image {
val request = request(uri).GET().build()
return loadImage(request).body()
}
@@ -2,15 +2,20 @@
package org.jetbrains.plugins.gitlab.data
import com.intellij.collaboration.ui.icon.AsyncImageIconsProvider
import com.intellij.collaboration.ui.html.AsyncHtmlImageLoader
import com.intellij.collaboration.util.resolveRelative
import com.intellij.openapi.diagnostic.logger
import com.intellij.util.IconUtil
import com.intellij.util.ui.ImageUtil
import icons.CollaborationToolsIcons
import org.jetbrains.plugins.gitlab.api.GitLabApi
import org.jetbrains.plugins.gitlab.api.GitLabServerPath
import org.jetbrains.plugins.gitlab.api.GitLabProjectCoordinates
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.api.request.loadImage
import org.jetbrains.plugins.gitlab.api.restApiUri
import java.awt.Image
import java.net.URI
import java.net.URL
import javax.swing.Icon
import kotlin.coroutines.cancellation.CancellationException
@@ -20,25 +25,34 @@ private const val LOADED_GRAVATAR_SIZE: Int = 80
class GitLabImageLoader(
private val apiClient: GitLabApi,
private val server: GitLabServerPath
) : AsyncImageIconsProvider.AsyncImageLoader<GitLabUserDTO> {
private val gitLabProjectCoordinates: GitLabProjectCoordinates
) : AsyncImageIconsProvider.AsyncImageLoader<GitLabUserDTO>, AsyncHtmlImageLoader {
override suspend fun load(key: GitLabUserDTO): Image? {
return key.avatarUrl?.let { avatarUrl ->
val actualUri = when {
avatarUrl.startsWith("http") -> avatarUrl
avatarUrl.startsWith("/avatar") -> "https://secure.gravatar.com$avatarUrl?s=$LOADED_GRAVATAR_SIZE&d=identicon"
else -> server.uri + avatarUrl
else -> gitLabProjectCoordinates.serverPath.uri + avatarUrl
}
try {
apiClient.loadImage(actualUri)
}
catch (ce: CancellationException) {
throw ce
}
catch (e: Exception) {
LOG.warn("Failed to load the avatar image from avatarUrl $avatarUrl with actual URI $actualUri", e)
throw e
load(null, actualUri)
}
}
override suspend fun load(baseUrl: URL?, src: String): Image? {
try {
if (src.startsWith("/uploads/")) {
val uriString = gitLabProjectCoordinates.restApiUri.resolveRelative(src.trimStart('/')).toString()
return apiClient.loadImage(uriString)
}
val uri = URI.create(src)
return apiClient.loadImage(uri)
}
catch (ce: CancellationException) {
throw ce
}
catch (e: Exception) {
LOG.warn("Failed to load the image from src $src", e)
throw e
}
}
@@ -49,4 +63,4 @@ class GitLabImageLoader(
override suspend fun postProcess(image: Image): Image {
return ImageUtil.createCircleImage(ImageUtil.toBufferedImage(image))
}
}
}
@@ -17,6 +17,7 @@ import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.api.dto.GitLabWorkItemDTO.GitLabWidgetDTO.WorkItemWidgetAssignees
import org.jetbrains.plugins.gitlab.api.dto.GitLabWorkItemDTO.WorkItemType
import org.jetbrains.plugins.gitlab.api.request.*
import org.jetbrains.plugins.gitlab.data.GitLabImageLoader
import org.jetbrains.plugins.gitlab.mergerequest.api.dto.GitLabMergeRequestDTO
import org.jetbrains.plugins.gitlab.mergerequest.api.request.loadMergeRequest
import org.jetbrains.plugins.gitlab.mergerequest.api.request.mergeRequestSetReviewers
@@ -67,6 +68,7 @@ class GitLabLazyProject(
private val initialData: GitLabProjectDTO,
private val currentUser: GitLabUserDTO,
private val tokenRefreshFlow: Flow<Unit>,
imageLoader: GitLabImageLoader
) : GitLabProject {
private val cs = parentCs.childScope(javaClass.name)
@@ -84,9 +86,9 @@ class GitLabLazyProject(
}
override val gitLabProjectId: GitLabId = initialData.id
val uploadFileUrlBase: String = projectMapping.repository.serverPath.toString() + "/-/project/" + gitLabProjectId.guessRestId() + "/uploads/"
private val uploadFileUrlBase: String = projectMapping.repository.serverPath.toString() + "/-/project/" + gitLabProjectId.guessRestId() + "/uploads/"
override val contextDataLoader: GitLabContextDataLoader = GitLabContextDataLoader(uploadFileUrlBase)
override val contextDataLoader: GitLabContextDataLoader = GitLabContextDataLoader(imageLoader, uploadFileUrlBase)
override val mergeRequests by lazy {
CachingGitLabProjectMergeRequestsStore(project, cs, api, glMetadata, projectMapping, currentUser, tokenRefreshFlow)
@@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.withContext
import org.jetbrains.plugins.gitlab.GitLabSettings
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.mergerequest.ui.diff.GitLabMergeRequestDiffDiscussionViewModel
import org.jetbrains.plugins.gitlab.mergerequest.ui.diff.GitLabMergeRequestDiffDraftNoteViewModel
import org.jetbrains.plugins.gitlab.mergerequest.ui.diff.GitLabMergeRequestDiffNewDiscussionViewModel
@@ -84,7 +85,7 @@ class GitLabMergeRequestDiffExtension : DiffExtension() {
UnifiedCodeReviewItemPosition(change, leftLine, rightLine)
}
},
rendererFactory = { createRenderer(it, changeVm.avatarIconsProvider) }
rendererFactory = { createRenderer(it, changeVm.avatarIconsProvider, changeVm.contextDataLoader) }
)
}
}
@@ -94,13 +95,14 @@ class GitLabMergeRequestDiffExtension : DiffExtension() {
private fun CoroutineScope.createRenderer(
model: GitLabMergeRequestEditorMappedComponentModel,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader,
): CodeReviewComponentInlayRenderer =
when (model) {
is GitLabMergeRequestEditorMappedComponentModel.Discussion<*> ->
GitLabMergeRequestDiscussionInlayRenderer(this, project, model.vm, avatarIconsProvider,
GitLabMergeRequestDiscussionInlayRenderer(this, project, model.vm, avatarIconsProvider, contextDataLoader,
GitLabStatistics.MergeRequestNoteActionPlace.DIFF)
is GitLabMergeRequestEditorMappedComponentModel.DraftNote<*> ->
GitLabMergeRequestDraftNoteInlayRenderer(this, project, model.vm, avatarIconsProvider,
GitLabMergeRequestDraftNoteInlayRenderer(this, project, model.vm, avatarIconsProvider, contextDataLoader,
GitLabStatistics.MergeRequestNoteActionPlace.DIFF)
is GitLabMergeRequestEditorMappedComponentModel.NewDiscussion<*> ->
GitLabMergeRequestNewDiscussionInlayRenderer(this, project, model.vm, avatarIconsProvider,
@@ -28,6 +28,7 @@ import org.jetbrains.plugins.gitlab.mergerequest.GitLabMergeRequestsPreferences
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequest
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequestChanges
import org.jetbrains.plugins.gitlab.mergerequest.data.loadRevisionsAndParseChanges
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.mergerequest.ui.createDiffDataFlow
import org.jetbrains.plugins.gitlab.mergerequest.ui.diff.*
import org.jetbrains.plugins.gitlab.mergerequest.ui.review.GitLabMergeRequestDiscussionsViewModels
@@ -71,6 +72,7 @@ internal class GitLabMergeRequestDiffProcessorViewModelImpl(
private val mergeRequest: GitLabMergeRequest,
private val discussionsContainer: GitLabMergeRequestDiscussionsViewModels,
private val avatarIconsProvider: IconsProvider<GitLabUserDTO>,
private val contextDataLoader: GitLabContextDataLoader,
) : GitLabMergeRequestDiffViewModel, GitLabMergeRequestReviewViewModelBase(
parentCs.childScope("GitLab Merge Request Diff Review VM"),
currentUser, mergeRequest,
@@ -194,7 +196,8 @@ internal class GitLabMergeRequestDiffProcessorViewModelImpl(
project, this,
mergeRequest, diffData, change,
this@GitLabMergeRequestDiffProcessorViewModelImpl,
discussionsContainer, discussionsViewOption, avatarIconsProvider
discussionsContainer, discussionsViewOption, avatarIconsProvider,
contextDataLoader
)
override fun findNextComment(currentThreadId: String, additionalIsVisible: (String) -> Boolean): String? =
@@ -111,7 +111,9 @@ class GitLabMergeRequestTimelineEditorFactory(private val project: Project, pare
withContext(Dispatchers.Main.immediate) {
timelineVmResult.fold(
onSuccess = {
val timeline = GitLabMergeRequestTimelineComponentFactory.create(project, this, it, projectVm.avatarIconProvider)
val timeline = GitLabMergeRequestTimelineComponentFactory.create(project, this, it,
projectVm.avatarIconProvider,
projectVm.contextDataLoader)
wrapper.setContent(timeline)
wrapper.repaint()
},
@@ -48,6 +48,7 @@ interface GitLabConnectedProjectViewModel {
val accountVm: GitLabAccountViewModel
val listVm: GitLabMergeRequestsListViewModel
val currentMergeRequestReviewVm: Flow<GitLabMergeRequestEditorReviewViewModel?>
val contextDataLoader: GitLabContextDataLoader
fun findMergeRequestDetails(mrIid: String): GitLabMergeRequestDetails?
fun reloadMergeRequestDetails(mergeRequestId: String)
fun getDiffViewModel(mrIid: String): Flow<Result<GitLabMergeRequestDiffViewModel>>
@@ -77,6 +78,8 @@ abstract class GitLabConnectedProjectViewModelBase(
override val avatarIconProvider: IconsProvider<GitLabUserDTO> = CachingIconsProvider(AsyncImageIconsProvider(cs, connection.imageLoader))
override val contextDataLoader: GitLabContextDataLoader = connection.projectData.contextDataLoader
private val projectName: @Nls String = connection.repo.repository.projectPath.name
override val listVm: GitLabMergeRequestsListViewModel = run {
@@ -103,7 +106,8 @@ abstract class GitLabConnectedProjectViewModelBase(
connection.projectData.mergeRequests.getShared(iid)
.transformConsecutiveSuccesses {
mapScoped {
GitLabMergeRequestViewModels(project, this, connection.projectData, avatarIconProvider, it, connection.currentUser,
GitLabMergeRequestViewModels(project, this, connection.projectData, avatarIconProvider,
it, connection.currentUser,
::openMergeRequestDetails, ::openMergeRequestTimeline, ::openMergeRequestDiff)
}
}
@@ -1,6 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.gitlab.mergerequest.ui
import com.intellij.collaboration.ui.html.AsyncHtmlImageLoader
import org.jetbrains.annotations.ApiStatus
/**
@@ -8,5 +9,6 @@ import org.jetbrains.annotations.ApiStatus
*/
@ApiStatus.Internal
class GitLabContextDataLoader(
val imageLoader: AsyncHtmlImageLoader,
val uploadFileUrlBase: String
)
@@ -66,7 +66,8 @@ internal class GitLabMergeRequestViewModels(
}
private val _diffVm by lazy {
GitLabMergeRequestDiffProcessorViewModelImpl(project, cs, currentUser, mergeRequest, discussionsVms, avatarIconProvider).apply {
GitLabMergeRequestDiffProcessorViewModelImpl(project, cs, currentUser, mergeRequest, discussionsVms,
avatarIconProvider, projectData.contextDataLoader).apply {
setup()
}
}
@@ -74,7 +75,8 @@ internal class GitLabMergeRequestViewModels(
val editorReviewVm: GitLabMergeRequestEditorReviewViewModel by lazy {
GitLabMergeRequestEditorReviewViewModel(cs, project, projectData.projectMapping, currentUser, mergeRequest,
discussionsVms, avatarIconProvider, openMergeRequestDetails, openMergeRequestDiff).apply {
discussionsVms, avatarIconProvider, projectData.contextDataLoader,
openMergeRequestDetails, openMergeRequestDiff).apply {
setup()
}
}
@@ -18,6 +18,7 @@ import com.intellij.util.ui.launchOnShow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.ui.comment.*
import org.jetbrains.plugins.gitlab.util.GitLabBundle
import org.jetbrains.plugins.gitlab.util.GitLabStatistics
@@ -32,10 +33,11 @@ internal object GitLabMergeRequestDiffInlayComponentsFactory {
project: Project,
cs: CoroutineScope,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader,
vm: GitLabMergeRequestDiscussionViewModel,
place: GitLabStatistics.MergeRequestNoteActionPlace,
): JComponent =
GitLabDiscussionComponentFactory.create(project, cs, avatarIconsProvider, vm, place).apply {
GitLabDiscussionComponentFactory.create(project, cs, avatarIconsProvider, contextDataLoader, vm, place).apply {
border = JBUI.Borders.empty(CodeReviewCommentUIUtil.getInlayPadding(CodeReviewChatItemUIUtil.ComponentType.COMPACT))
}.apply {
isFocusable = true
@@ -57,10 +59,12 @@ internal object GitLabMergeRequestDiffInlayComponentsFactory {
project: Project,
cs: CoroutineScope,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader,
vm: GitLabNoteViewModel,
place: GitLabStatistics.MergeRequestNoteActionPlace,
): JComponent =
GitLabNoteComponentFactory.create(CodeReviewChatItemUIUtil.ComponentType.COMPACT, project, cs, avatarIconsProvider, vm, place).apply {
GitLabNoteComponentFactory.create(CodeReviewChatItemUIUtil.ComponentType.COMPACT, project, cs, avatarIconsProvider,
contextDataLoader, vm, place).apply {
border = JBUI.Borders.empty(CodeReviewCommentUIUtil.getInlayPadding(CodeReviewChatItemUIUtil.ComponentType.COMPACT))
}.apply {
isFocusable = true
@@ -24,6 +24,7 @@ import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequest
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequestNewDiscussionPosition
import org.jetbrains.plugins.gitlab.mergerequest.data.mapToLocation
import org.jetbrains.plugins.gitlab.mergerequest.diff.GitLabMergeRequestDiffViewModel
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.mergerequest.ui.details.model.GitLabPersistentMergeRequestChangesViewedState
import org.jetbrains.plugins.gitlab.mergerequest.ui.filterInFile
import org.jetbrains.plugins.gitlab.mergerequest.ui.review.GitLabMergeRequestDiscussionsViewModels
@@ -42,6 +43,7 @@ interface GitLabMergeRequestDiffReviewViewModel {
val locationsWithNewDiscussions: StateFlow<Set<DiffLineLocation>>
val avatarIconsProvider: IconsProvider<GitLabUserDTO>
val contextDataLoader: GitLabContextDataLoader
fun nextComment(focused: String, additionalIsVisible: (String) -> Boolean): String?
fun nextComment(cursorLocation: UnifiedCodeReviewItemPosition, additionalIsVisible: (String) -> Boolean): String?
@@ -66,6 +68,7 @@ internal class GitLabMergeRequestDiffReviewViewModelImpl(
private val discussionsContainer: GitLabMergeRequestDiscussionsViewModels,
discussionsViewOption: StateFlow<DiscussionsViewOption>,
override val avatarIconsProvider: IconsProvider<GitLabUserDTO>,
override val contextDataLoader: GitLabContextDataLoader
) : GitLabMergeRequestDiffReviewViewModel {
private val cs = parentCs.childScope(javaClass.name)
@@ -27,6 +27,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.GitLabMergeRequestsPreferences
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabProjectViewModel
import org.jetbrains.plugins.gitlab.util.GitLabStatistics
@@ -103,7 +104,7 @@ internal class GitLabMergeRequestEditorReviewController(private val project: Pro
}
launchNow {
editor.renderInlays(model.inlays, HashingUtil.mappingStrategy(GitLabMergeRequestEditorMappedComponentModel::key)) {
createRenderer(it, fileVm.avatarIconsProvider)
createRenderer(it, fileVm.avatarIconsProvider, fileVm.contextDataLoader)
}
}
@@ -122,13 +123,16 @@ internal class GitLabMergeRequestEditorReviewController(private val project: Pro
private fun CoroutineScope.createRenderer(
inlayModel: GitLabMergeRequestEditorMappedComponentModel,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader
) =
when (inlayModel) {
is GitLabMergeRequestEditorMappedComponentModel.Discussion<*> ->
GitLabMergeRequestDiscussionInlayRenderer(this, project, inlayModel.vm, avatarIconsProvider,
contextDataLoader,
GitLabStatistics.MergeRequestNoteActionPlace.EDITOR)
is GitLabMergeRequestEditorMappedComponentModel.DraftNote<*> ->
GitLabMergeRequestDraftNoteInlayRenderer(this, project, inlayModel.vm, avatarIconsProvider,
contextDataLoader,
GitLabStatistics.MergeRequestNoteActionPlace.EDITOR)
is GitLabMergeRequestEditorMappedComponentModel.NewDiscussion<*> ->
GitLabMergeRequestNewDiscussionInlayRenderer(this, project, inlayModel.vm, avatarIconsProvider,
@@ -29,6 +29,7 @@ import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequest
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequestNewDiscussionPosition
import org.jetbrains.plugins.gitlab.mergerequest.data.mapToLocation
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.mergerequest.ui.filterInFile
import org.jetbrains.plugins.gitlab.mergerequest.ui.review.GitLabMergeRequestDiscussionsViewModels
import org.jetbrains.plugins.gitlab.mergerequest.ui.review.mapToLocation
@@ -54,6 +55,7 @@ interface GitLabMergeRequestEditorReviewFileViewModel {
val newDiscussions: StateFlow<Collection<GitLabMergeRequestEditorNewDiscussionViewModel>>
val avatarIconsProvider: IconsProvider<GitLabUserDTO>
val contextDataLoader: GitLabContextDataLoader
fun lookupNextComment(line: Int, additionalIsVisible: (String) -> Boolean): String?
fun lookupNextComment(noteTrackingId: String, additionalIsVisible: (String) -> Boolean): String?
@@ -79,6 +81,7 @@ internal class GitLabMergeRequestEditorReviewFileViewModelImpl(
private val reviewVm: GitLabMergeRequestEditorReviewViewModel,
discussionsViewOption: StateFlow<DiscussionsViewOption>,
override val avatarIconsProvider: IconsProvider<GitLabUserDTO>,
override val contextDataLoader: GitLabContextDataLoader
) : GitLabMergeRequestEditorReviewFileViewModel {
private val cs = parentCs.childScope(javaClass.name, Dispatchers.Default)
@@ -33,6 +33,7 @@ import org.jetbrains.annotations.ApiStatus
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.GitLabMergeRequestsPreferences
import org.jetbrains.plugins.gitlab.mergerequest.data.GitLabMergeRequest
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.mergerequest.ui.createDiffDataFlow
import org.jetbrains.plugins.gitlab.mergerequest.ui.review.GitLabMergeRequestDiscussionsViewModels
import org.jetbrains.plugins.gitlab.mergerequest.ui.review.GitLabMergeRequestReviewViewModelBase
@@ -52,6 +53,7 @@ class GitLabMergeRequestEditorReviewViewModel internal constructor(
private val mergeRequest: GitLabMergeRequest,
private val discussionsVms: GitLabMergeRequestDiscussionsViewModels,
private val avatarIconsProvider: IconsProvider<GitLabUserDTO>,
private val contextDataLoader: GitLabContextDataLoader,
private val openMergeRequestDetails: (String, GitLabStatistics.ToolWindowOpenTabActionPlace, Boolean) -> Unit,
private val openMergeRequestDiff: (String, Boolean) -> Unit,
) : GitLabMergeRequestReviewViewModelBase(
@@ -251,7 +253,7 @@ class GitLabMergeRequestEditorReviewViewModel internal constructor(
GitLabMergeRequestEditorReviewFileViewModelImpl(
this, project, mergeRequest, changes.selectedItem!!, diffData,
discussionsVms, this@GitLabMergeRequestEditorReviewViewModel,
discussionsViewOption, avatarIconsProvider
discussionsViewOption, avatarIconsProvider, contextDataLoader
).also { vm ->
launchNow {
vm.showDiffRequests.collect { line ->
@@ -7,6 +7,7 @@ import com.intellij.openapi.project.Project
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.mergerequest.ui.diff.GitLabMergeRequestDiffInlayComponentsFactory
import org.jetbrains.plugins.gitlab.ui.comment.GitLabMergeRequestDiscussionViewModel
import org.jetbrains.plugins.gitlab.ui.comment.GitLabNoteViewModel
@@ -18,9 +19,11 @@ class GitLabMergeRequestDiscussionInlayRenderer internal constructor(cs: Corouti
project: Project,
vm: GitLabMergeRequestDiscussionViewModel,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
place: GitLabStatistics.MergeRequestNoteActionPlace)
contextDataLoader: GitLabContextDataLoader,
place: GitLabStatistics.MergeRequestNoteActionPlace,
)
: CodeReviewComponentInlayRenderer(
GitLabMergeRequestDiffInlayComponentsFactory.createDiscussion(project, cs, avatarIconsProvider, vm, place)
GitLabMergeRequestDiffInlayComponentsFactory.createDiscussion(project, cs, avatarIconsProvider, contextDataLoader, vm, place)
)
@ApiStatus.Internal
@@ -28,9 +31,11 @@ class GitLabMergeRequestDraftNoteInlayRenderer internal constructor(cs: Coroutin
project: Project,
vm: GitLabNoteViewModel,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
place: GitLabStatistics.MergeRequestNoteActionPlace)
contextDataLoader: GitLabContextDataLoader,
place: GitLabStatistics.MergeRequestNoteActionPlace,
)
: CodeReviewComponentInlayRenderer(
GitLabMergeRequestDiffInlayComponentsFactory.createDraftNote(project, cs, avatarIconsProvider, vm, place)
GitLabMergeRequestDiffInlayComponentsFactory.createDraftNote(project, cs, avatarIconsProvider, contextDataLoader, vm, place)
)
@ApiStatus.Internal
@@ -20,7 +20,6 @@ import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.colors.EditorColorsManager
import com.intellij.openapi.project.Project
@@ -32,17 +31,16 @@ import com.intellij.ui.PopupHandler
import com.intellij.ui.ScrollPaneFactory
import com.intellij.ui.components.panels.ListLayout
import com.intellij.ui.components.panels.Wrapper
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.JBUI.Borders
import com.intellij.util.ui.StyleSheetUtil
import com.intellij.util.ui.launchOnShow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.jetbrains.annotations.Nls
import org.jetbrains.plugins.gitlab.api.dto.*
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.mergerequest.ui.details.GitLabMergeRequestViewModel
import org.jetbrains.plugins.gitlab.mergerequest.util.addGitLabHyperlinkListener
import org.jetbrains.plugins.gitlab.ui.comment.*
@@ -55,7 +53,8 @@ internal object GitLabMergeRequestTimelineComponentFactory {
fun create(project: Project,
cs: CoroutineScope,
timelineVm: GitLabMergeRequestTimelineViewModel,
avatarIconsProvider: IconsProvider<GitLabUserDTO>
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader
): JComponent {
val titleComponent = GitLabMergeRequestTimelineTitleComponent.create(project, cs, timelineVm).let {
CollaborationToolsUIUtil.wrapWithLimitedSize(it, CodeReviewChatItemUIUtil.TEXT_CONTENT_WIDTH)
@@ -63,10 +62,10 @@ internal object GitLabMergeRequestTimelineComponentFactory {
border = Borders.empty(CodeReviewTimelineUIUtil.HEADER_VERT_PADDING, CodeReviewTimelineUIUtil.ITEM_HOR_PADDING)
}
val descriptionComponent = GitLabMergeRequestTimelineDescriptionComponent
.createComponent(project, cs, timelineVm, avatarIconsProvider)
.createComponent(project, cs, timelineVm, avatarIconsProvider, contextDataLoader)
val timelinePanel = VerticalListPanel(0)
val errorOrTimelineComponent = createErrorOrTimelineComponent(cs, project, avatarIconsProvider, timelineVm, timelinePanel)
val errorOrTimelineComponent = createErrorOrTimelineComponent(cs, project, avatarIconsProvider, contextDataLoader, timelineVm, timelinePanel)
val newNoteField = timelineVm.newNoteVm?.let {
cs.createNewNoteField(project, avatarIconsProvider, it)
@@ -138,6 +137,7 @@ internal object GitLabMergeRequestTimelineComponentFactory {
private fun createErrorOrTimelineComponent(cs: CoroutineScope,
project: Project,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader,
timelineVm: GitLabMergeRequestTimelineViewModel,
timelinePanel: JComponent): JComponent {
val actionManager = ActionManager.getInstance()
@@ -148,7 +148,7 @@ internal object GitLabMergeRequestTimelineComponentFactory {
val timelineItems = MutableStateFlow<List<GitLabMergeRequestTimelineItemViewModel>>(listOf())
val timelineItemContent = ComponentListPanelFactory.createVertical(cs, timelineItems) { item ->
createItemComponent(project, avatarIconsProvider, item)
createItemComponent(project, avatarIconsProvider, contextDataLoader, item)
}
val timelineItemsAndLoadingLabel = VerticalListPanel(gap = 0).apply {
val panel = this
@@ -196,6 +196,7 @@ internal object GitLabMergeRequestTimelineComponentFactory {
private fun CoroutineScope.createItemComponent(project: Project,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader,
item: GitLabMergeRequestTimelineItemViewModel): JComponent =
when (item) {
is GitLabMergeRequestTimelineItemViewModel.Immutable -> {
@@ -208,10 +209,10 @@ internal object GitLabMergeRequestTimelineComponentFactory {
}
}
is GitLabMergeRequestTimelineItemViewModel.Discussion -> {
GitLabMergeRequestTimelineDiscussionComponentFactory.createIn(project, this, item, avatarIconsProvider)
GitLabMergeRequestTimelineDiscussionComponentFactory.createIn(project, this, item, avatarIconsProvider, contextDataLoader)
}
is GitLabMergeRequestTimelineItemViewModel.DraftNote -> {
GitLabMergeRequestTimelineDiscussionComponentFactory.createIn(project, this, item, avatarIconsProvider)
GitLabMergeRequestTimelineDiscussionComponentFactory.createIn(project, this, item, avatarIconsProvider, contextDataLoader)
}
}
@@ -223,8 +224,10 @@ internal object GitLabMergeRequestTimelineComponentFactory {
is GitLabMergeRequestTimelineItemViewModel.StateEvent -> createStateChangeContent(item)
}
private fun createSystemDiscussionContent(project: Project,
item: GitLabMergeRequestTimelineItemViewModel.SystemNote): JComponent {
private fun createSystemDiscussionContent(
project: Project,
item: GitLabMergeRequestTimelineItemViewModel.SystemNote
): JComponent {
val content = item.content
if (content.contains("Compare with previous version")) {
try {
@@ -253,10 +256,11 @@ internal object GitLabMergeRequestTimelineComponentFactory {
StyleSheetUtil.loadStyleSheet("""ul {margin: 0}""")
}
private fun createCommitsListPane(project: Project, commits: @NlsSafe String) = SimpleHtmlPane(noUlGapsStyleSheet, addBrowserListener = false).apply {
setHtmlBody(commits)
addGitLabHyperlinkListener(project)
}
private fun createCommitsListPane(project: Project, commits: @NlsSafe String) =
SimpleHtmlPane(noUlGapsStyleSheet, addBrowserListener = false).apply {
setHtmlBody(commits)
addGitLabHyperlinkListener(project)
}
private fun createLabeledEventContent(item: GitLabMergeRequestTimelineItemViewModel.LabelEvent): JComponent {
val text = when (item.event.actionEnum) {
@@ -13,6 +13,7 @@ import com.intellij.util.ui.UIUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.ui.comment.GitLabNoteComponentFactory.createTextPanel
import org.jetbrains.plugins.gitlab.util.GitLabBundle
import javax.swing.JComponent
@@ -32,10 +33,11 @@ internal object GitLabMergeRequestTimelineDescriptionComponent {
cs: CoroutineScope,
vm: GitLabMergeRequestTimelineViewModel,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader,
): JComponent {
val titlePanel = CodeReviewTimelineUIUtil.createTitleTextPane(vm.author.name, vm.author.webUrl, date = null)
val descriptionTextComponent = createTextPanel(project, cs, vm.descriptionHtml.map { it.ifBlank { noDescriptionHtmlText } },
vm.serverUrl)
vm.serverUrl, contextDataLoader)
return CodeReviewChatItemUIUtil.buildDynamic(CodeReviewChatItemUIUtil.ComponentType.FULL,
@@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.data.filePath
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.mergerequest.ui.emoji.GitLabReactionsComponentFactory
import org.jetbrains.plugins.gitlab.mergerequest.util.localizedMessageOrClassName
import org.jetbrains.plugins.gitlab.ui.comment.GitLabDiscussionComponentFactory
@@ -53,18 +54,20 @@ internal object GitLabMergeRequestTimelineDiscussionComponentFactory {
fun createIn(project: Project,
cs: CoroutineScope,
vm: GitLabMergeRequestTimelineDiscussionViewModel,
avatarIconsProvider: IconsProvider<GitLabUserDTO>): JComponent =
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader): JComponent =
VerticalListPanel().apply {
name = "GitLab Discussion Panel ${vm.id}"
add(createDiscussionItemIn(project, cs, vm, avatarIconsProvider))
add(createRepliesPanelIn(project, cs, vm, avatarIconsProvider))
add(createDiscussionItemIn(project, cs, vm, avatarIconsProvider, contextDataLoader))
add(createRepliesPanelIn(project, cs, vm, avatarIconsProvider, contextDataLoader))
}
fun createIn(project: Project,
cs: CoroutineScope,
vm: GitLabMergeRequestTimelineItemViewModel.DraftNote,
avatarIconsProvider: IconsProvider<GitLabUserDTO>): JComponent {
val contentPanel = createContentIn(project, cs, vm)
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader): JComponent {
val contentPanel = createContentIn(project, cs, vm, contextDataLoader)
val mainItem = CodeReviewChatItemUIUtil.build(ComponentType.FULL,
{ avatarIconsProvider.getIcon(vm.author, it) },
contentPanel) {
@@ -82,8 +85,9 @@ internal object GitLabMergeRequestTimelineDiscussionComponentFactory {
private fun createDiscussionItemIn(project: Project,
cs: CoroutineScope,
vm: GitLabMergeRequestTimelineDiscussionViewModel,
avatarIconsProvider: IconsProvider<GitLabUserDTO>): JComponent {
val contentPanel = createContentIn(project, cs, vm, avatarIconsProvider)
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader): JComponent {
val contentPanel = createContentIn(project, cs, vm, avatarIconsProvider, contextDataLoader)
val mainItem = CodeReviewChatItemUIUtil.buildDynamic(ComponentType.FULL,
{ vm.author.createIconValue(cs, avatarIconsProvider, it) },
contentPanel) {
@@ -104,13 +108,14 @@ internal object GitLabMergeRequestTimelineDiscussionComponentFactory {
private fun createContentIn(project: Project,
cs: CoroutineScope,
vm: GitLabMergeRequestTimelineDiscussionViewModel,
avatarIconsProvider: IconsProvider<GitLabUserDTO>): JPanel {
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader): JPanel {
val mainNoteVm = vm.mainNote
val repliesActionsPanel = createRepliesActionsPanel(cs, avatarIconsProvider, vm).apply {
border = JBUI.Borders.empty(Replies.ActionsFolded.VERTICAL_PADDING, 0)
bindVisibilityIn(cs, vm.repliesFolded)
}
val textPanel = createDiscussionTextPane(project, cs, vm)
val textPanel = createDiscussionTextPane(project, cs, vm, contextDataLoader)
val editVmFlow = mainNoteVm.flatMapLatest { it.actionsVm?.editVm ?: flowOf(null) }
val textContentPanel = EditableComponentFactory.wrapTextComponent(cs, textPanel, editVmFlow) {
@@ -162,8 +167,10 @@ internal object GitLabMergeRequestTimelineDiscussionComponentFactory {
private fun createContentIn(project: Project,
cs: CoroutineScope,
vm: GitLabMergeRequestTimelineItemViewModel.DraftNote): JPanel {
val textPanel = GitLabNoteComponentFactory.createTextPanel(project, cs, vm.bodyHtml, vm.serverUrl)
vm: GitLabMergeRequestTimelineItemViewModel.DraftNote,
contextDataLoader: GitLabContextDataLoader): JPanel {
val textPanel = GitLabNoteComponentFactory.createTextPanel(project, cs, vm.bodyHtml, vm.serverUrl,
contextDataLoader)
val textContentPanel = EditableComponentFactory.wrapTextComponent(cs, textPanel, vm.actionsVm?.editVm ?: flowOf(null)) {
GitLabStatistics.logMrActionExecuted(project, GitLabStatistics.MergeRequestAction.UPDATE_NOTE,
@@ -270,7 +277,10 @@ internal object GitLabMergeRequestTimelineDiscussionComponentFactory {
)
}
private fun createDiscussionTextPane(project: Project, cs: CoroutineScope, vm: GitLabMergeRequestTimelineDiscussionViewModel): JComponent {
private fun createDiscussionTextPane(
project: Project, cs: CoroutineScope, vm: GitLabMergeRequestTimelineDiscussionViewModel,
contextDataLoader: GitLabContextDataLoader
): JComponent {
val collapsedFlow = combine(vm.collapsible, vm.collapsed) { collapsible, collapsed ->
collapsible && collapsed
}
@@ -279,7 +289,7 @@ internal object GitLabMergeRequestTimelineDiscussionComponentFactory {
if (collapsed) mainNote.body else mainNote.bodyHtml
}.flatMapLatest { it }
val textPane = GitLabNoteComponentFactory.createTextPanel(project, cs, textFlow, vm.serverUrl)
val textPane = GitLabNoteComponentFactory.createTextPanel(project, cs, textFlow, vm.serverUrl, contextDataLoader)
val layout = SizeRestrictedSingleComponentLayout()
return JPanel(layout).apply {
name = "Text pane wrapper"
@@ -306,10 +316,14 @@ internal object GitLabMergeRequestTimelineDiscussionComponentFactory {
private fun createRepliesPanelIn(project: Project,
cs: CoroutineScope,
vm: GitLabMergeRequestTimelineDiscussionViewModel,
avatarIconsProvider: IconsProvider<GitLabUserDTO>): JPanel {
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader
): JPanel {
val repliesListPanel = ComponentListPanelFactory.createVertical(cs, vm.replies) { noteVm ->
GitLabNoteComponentFactory.create(ComponentType.FULL_SECONDARY, project, this, avatarIconsProvider, noteVm,
ACTION_PLACE)
GitLabNoteComponentFactory.create(
ComponentType.FULL_SECONDARY, project, this, avatarIconsProvider, contextDataLoader, noteVm,
ACTION_PLACE
)
}
val repliesPanel = VerticalListPanel().apply {
@@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.ui.comment.GitLabMergeRequestDiscussionViewModel.NoteItem
import org.jetbrains.plugins.gitlab.util.GitLabBundle
import org.jetbrains.plugins.gitlab.util.GitLabStatistics
@@ -39,10 +40,12 @@ import javax.swing.Action
import javax.swing.JComponent
internal object GitLabDiscussionComponentFactory {
fun create(
project: Project,
cs: CoroutineScope,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader,
vm: GitLabMergeRequestDiscussionViewModel,
place: GitLabStatistics.MergeRequestNoteActionPlace,
): JComponent {
@@ -57,7 +60,8 @@ internal object GitLabDiscussionComponentFactory {
TimelineThreadCommentsPanel.UNFOLD_BUTTON_VERTICAL_GAP,
0)
}
is NoteItem.Note -> GitLabNoteComponentFactory.create(ComponentType.COMPACT, project, itemCs, avatarIconsProvider, item.vm, place)
is NoteItem.Note -> GitLabNoteComponentFactory.create(ComponentType.COMPACT, project, itemCs, avatarIconsProvider, contextDataLoader,
item.vm, place)
}
}
@@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import org.jetbrains.annotations.Nls
import org.jetbrains.plugins.gitlab.api.dto.GitLabUserDTO
import org.jetbrains.plugins.gitlab.mergerequest.ui.GitLabContextDataLoader
import org.jetbrains.plugins.gitlab.mergerequest.ui.emoji.GitLabReactionsComponentFactory
import org.jetbrains.plugins.gitlab.mergerequest.ui.emoji.GitLabReactionsPickerComponentFactory
import org.jetbrains.plugins.gitlab.mergerequest.ui.emoji.GitLabReactionsViewModel
@@ -37,15 +38,17 @@ import java.net.URL
import javax.swing.JComponent
internal object GitLabNoteComponentFactory {
fun create(
componentType: ComponentType,
project: Project,
cs: CoroutineScope,
avatarIconsProvider: IconsProvider<GitLabUserDTO>,
contextDataLoader: GitLabContextDataLoader,
vm: GitLabNoteViewModel,
place: GitLabStatistics.MergeRequestNoteActionPlace,
): JComponent {
val textPanel = createTextPanel(project, cs, vm.bodyHtml, vm.serverUrl).let { panel ->
val textPanel = createTextPanel(project, cs, vm.bodyHtml, vm.serverUrl, contextDataLoader).let { panel ->
val actionsVm = vm.actionsVm ?: return@let panel
EditableComponentFactory.wrapTextComponent(cs, panel, actionsVm.editVm) {
GitLabStatistics.logMrActionExecuted(project, GitLabStatistics.MergeRequestAction.UPDATE_NOTE, place)
@@ -173,8 +176,11 @@ internal object GitLabNoteComponentFactory {
return button
}
fun createTextPanel(project: Project, cs: CoroutineScope, textFlow: Flow<@Nls String>, baseUrl: URL): JComponent =
SimpleHtmlPane(baseUrl = baseUrl, addBrowserListener = false).apply {
fun createTextPanel(
project: Project, cs: CoroutineScope, textFlow: Flow<@Nls String>, baseUrl: URL,
contextDataLoader: GitLabContextDataLoader
): JComponent =
SimpleHtmlPane(baseUrl = baseUrl, addBrowserListener = false, customImageLoader = contextDataLoader.imageLoader).apply {
bindTextIn(cs, textFlow)
addGitLabHyperlinkListener(project)
}