UnsupportedAbiVersionNotificationPanelProvider: J2K
This commit is contained in:
+161
-214
@@ -14,279 +14,226 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.jetbrains.kotlin.idea.versions;
|
package org.jetbrains.kotlin.idea.versions
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate
|
||||||
import com.google.common.collect.Collections2;
|
import com.google.common.collect.Collections2
|
||||||
import com.intellij.ProjectTopics;
|
import com.intellij.ProjectTopics
|
||||||
import com.intellij.icons.AllIcons;
|
import com.intellij.icons.AllIcons
|
||||||
import com.intellij.openapi.application.ApplicationManager;
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
import com.intellij.openapi.compiler.CompilerManager;
|
import com.intellij.openapi.compiler.CompilerManager
|
||||||
import com.intellij.openapi.components.ServiceManager;
|
import com.intellij.openapi.components.ServiceManager
|
||||||
import com.intellij.openapi.fileEditor.FileEditor;
|
import com.intellij.openapi.fileEditor.FileEditor
|
||||||
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
|
import com.intellij.openapi.fileEditor.OpenFileDescriptor
|
||||||
import com.intellij.openapi.module.Module;
|
import com.intellij.openapi.module.Module
|
||||||
import com.intellij.openapi.module.ModuleUtilCore;
|
import com.intellij.openapi.module.ModuleUtilCore
|
||||||
import com.intellij.openapi.progress.ProcessCanceledException;
|
import com.intellij.openapi.progress.ProcessCanceledException
|
||||||
import com.intellij.openapi.project.DumbService;
|
import com.intellij.openapi.project.DumbService
|
||||||
import com.intellij.openapi.project.IndexNotReadyException;
|
import com.intellij.openapi.project.IndexNotReadyException
|
||||||
import com.intellij.openapi.project.Project;
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.roots.ModuleRootAdapter;
|
import com.intellij.openapi.roots.ModuleRootAdapter
|
||||||
import com.intellij.openapi.roots.ModuleRootEvent;
|
import com.intellij.openapi.roots.ModuleRootEvent
|
||||||
import com.intellij.openapi.roots.libraries.Library;
|
import com.intellij.openapi.roots.libraries.Library
|
||||||
import com.intellij.openapi.ui.popup.JBPopupFactory;
|
import com.intellij.openapi.ui.popup.JBPopupFactory
|
||||||
import com.intellij.openapi.ui.popup.ListPopup;
|
import com.intellij.openapi.ui.popup.ListPopup
|
||||||
import com.intellij.openapi.ui.popup.PopupStep;
|
import com.intellij.openapi.ui.popup.PopupStep
|
||||||
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
|
import com.intellij.openapi.ui.popup.util.BaseListPopupStep
|
||||||
import com.intellij.openapi.util.Computable;
|
import com.intellij.openapi.util.Computable
|
||||||
import com.intellij.openapi.util.Key;
|
import com.intellij.openapi.util.Key
|
||||||
import com.intellij.openapi.util.Ref;
|
import com.intellij.openapi.util.Ref
|
||||||
import com.intellij.openapi.vfs.VfsUtilCore;
|
import com.intellij.openapi.vfs.VfsUtilCore
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
import com.intellij.openapi.vfs.VirtualFile
|
||||||
import com.intellij.ui.EditorNotificationPanel;
|
import com.intellij.ui.EditorNotificationPanel
|
||||||
import com.intellij.ui.EditorNotifications;
|
import com.intellij.ui.EditorNotifications
|
||||||
import com.intellij.util.messages.MessageBusConnection;
|
import com.intellij.util.messages.MessageBusConnection
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.kotlin.idea.KotlinFileType
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.kotlin.idea.configuration.*
|
||||||
import org.jetbrains.kotlin.idea.KotlinFileType;
|
import org.jetbrains.kotlin.idea.framework.JSLibraryStdPresentationProvider
|
||||||
import org.jetbrains.kotlin.idea.configuration.ConfigureKotlinInProjectUtilsKt;
|
import org.jetbrains.kotlin.idea.framework.JavaRuntimePresentationProvider
|
||||||
import org.jetbrains.kotlin.idea.framework.JSLibraryStdPresentationProvider;
|
|
||||||
import org.jetbrains.kotlin.idea.framework.JavaRuntimePresentationProvider;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*
|
||||||
import java.awt.*;
|
import java.awt.*
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat
|
||||||
import java.util.Collection;
|
import java.util.Collections
|
||||||
import java.util.Collections;
|
import java.util.HashSet
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
public class UnsupportedAbiVersionNotificationPanelProvider extends EditorNotifications.Provider<EditorNotificationPanel> {
|
class UnsupportedAbiVersionNotificationPanelProvider(private val project: Project) : EditorNotifications.Provider<EditorNotificationPanel>() {
|
||||||
private static final Key<EditorNotificationPanel> KEY = Key.create("unsupported.abi.version");
|
|
||||||
|
|
||||||
private final Project project;
|
init {
|
||||||
|
val connection = project.messageBus.connect()
|
||||||
public UnsupportedAbiVersionNotificationPanelProvider(@NotNull Project project) {
|
connection.subscribe(ProjectTopics.PROJECT_ROOTS, object : ModuleRootAdapter() {
|
||||||
this.project = project;
|
override fun rootsChanged(event: ModuleRootEvent?) {
|
||||||
MessageBusConnection connection = project.getMessageBus().connect();
|
updateNotifications()
|
||||||
connection.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() {
|
|
||||||
@Override
|
|
||||||
public void rootsChanged(ModuleRootEvent event) {
|
|
||||||
updateNotifications();
|
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
connection.subscribe(DumbService.DUMB_MODE, new DumbService.DumbModeListener() {
|
connection.subscribe(DumbService.DUMB_MODE, object : DumbService.DumbModeListener {
|
||||||
@Override
|
override fun enteredDumbMode() {
|
||||||
public void enteredDumbMode() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void exitDumbMode() {
|
|
||||||
updateNotifications();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
override fun exitDumbMode() {
|
||||||
|
updateNotifications()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
private fun doCreate(badRoots: Collection<VirtualFile>): EditorNotificationPanel {
|
||||||
public static EditorNotificationPanel checkAndCreate(@NotNull Project project) {
|
val answer = ErrorNotificationPanel()
|
||||||
SuppressNotificationState state = ServiceManager.getService(project, SuppressNotificationState.class).getState();
|
|
||||||
if (state != null && state.isSuppressed) {
|
val kotlinLibraries = findAllUsedLibraries(project).keySet()
|
||||||
return null;
|
val badRuntimeLibraries = Collections2.filter(kotlinLibraries) { library ->
|
||||||
|
assert(library != null) { "library should be non null" }
|
||||||
|
val runtimeJar = getLocalJar(JavaRuntimePresentationProvider.getRuntimeJar(library))
|
||||||
|
val jsLibJar = getLocalJar(JSLibraryStdPresentationProvider.getJsStdLibJar(library))
|
||||||
|
badRoots.contains(runtimeJar) || badRoots.contains(jsLibJar)
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection<VirtualFile> badRoots = collectBadRoots(project);
|
|
||||||
if (!badRoots.isEmpty()) {
|
|
||||||
return new UnsupportedAbiVersionNotificationPanelProvider(project).doCreate(badRoots);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EditorNotificationPanel doCreate(final Collection<VirtualFile> badRoots) {
|
|
||||||
EditorNotificationPanel answer = new ErrorNotificationPanel();
|
|
||||||
|
|
||||||
Collection<Library> kotlinLibraries = KotlinRuntimeLibraryUtilKt.findAllUsedLibraries(project).keySet();
|
|
||||||
final Collection<Library> badRuntimeLibraries = Collections2.filter(kotlinLibraries, new Predicate<Library>() {
|
|
||||||
@Override
|
|
||||||
public boolean apply(@Nullable Library library) {
|
|
||||||
assert library != null : "library should be non null";
|
|
||||||
VirtualFile runtimeJar = KotlinRuntimeLibraryUtilKt.getLocalJar(JavaRuntimePresentationProvider.getRuntimeJar(library));
|
|
||||||
VirtualFile jsLibJar = KotlinRuntimeLibraryUtilKt.getLocalJar(JSLibraryStdPresentationProvider.getJsStdLibJar(library));
|
|
||||||
return badRoots.contains(runtimeJar) || badRoots.contains(jsLibJar);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!badRuntimeLibraries.isEmpty()) {
|
if (!badRuntimeLibraries.isEmpty()) {
|
||||||
int otherBadRootsCount = badRoots.size() - badRuntimeLibraries.size();
|
val otherBadRootsCount = badRoots.size - badRuntimeLibraries.size
|
||||||
|
|
||||||
String text = MessageFormat.format("<html><b>{0,choice,0#|1#|1<Some }Kotlin runtime librar{0,choice,0#|1#y|1<ies}</b>" +
|
val text = MessageFormat.format("<html><b>{0,choice,0#|1#|1<Some }Kotlin runtime librar{0,choice,0#|1#y|1<ies}</b>" + "{1,choice,0#|1# and one other jar|1< and {1} other jars} " + "{1,choice,0#has|0<have} an unsupported format</html>",
|
||||||
"{1,choice,0#|1# and one other jar|1< and {1} other jars} " +
|
badRuntimeLibraries.size,
|
||||||
"{1,choice,0#has|0<have} an unsupported format</html>",
|
otherBadRootsCount)
|
||||||
badRuntimeLibraries.size(),
|
|
||||||
otherBadRootsCount);
|
|
||||||
|
|
||||||
String actionLabelText = MessageFormat.format("Update {0,choice,0#|1#|1<all }Kotlin runtime librar{0,choice,0#|1#y|1<ies} ",
|
val actionLabelText = MessageFormat.format("Update {0,choice,0#|1#|1<all }Kotlin runtime librar{0,choice,0#|1#y|1<ies} ",
|
||||||
badRuntimeLibraries.size());
|
badRuntimeLibraries.size)
|
||||||
|
|
||||||
answer.setText(text);
|
answer.setText(text)
|
||||||
answer.createActionLabel(actionLabelText, new Runnable() {
|
answer.createActionLabel(actionLabelText) { updateLibraries(project, badRuntimeLibraries) }
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
KotlinRuntimeLibraryUtilKt.updateLibraries(project, badRuntimeLibraries);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (otherBadRootsCount > 0) {
|
if (otherBadRootsCount > 0) {
|
||||||
createShowPathsActionLabel(answer, "Show all");
|
createShowPathsActionLabel(answer, "Show all")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (badRoots.size() == 1) {
|
else if (badRoots.size == 1) {
|
||||||
final VirtualFile root = badRoots.iterator().next();
|
val root = badRoots.iterator().next()
|
||||||
String presentableName = root.getPresentableName();
|
val presentableName = root.presentableName
|
||||||
answer.setText("<html>Kotlin library <b>'" + presentableName + "'</b> " +
|
answer.setText("<html>Kotlin library <b>'$presentableName'</b> has an unsupported format. Please update the library or the plugin</html>")
|
||||||
"has an unsupported format. Please update the library or the plugin</html>");
|
|
||||||
|
|
||||||
answer.createActionLabel("Go to " + presentableName, new Runnable() {
|
answer.createActionLabel("Go to " + presentableName) { navigateToLibraryRoot(project, root) }
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
navigateToLibraryRoot(project, root);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
answer.setText("Some Kotlin libraries attached to this project have unsupported format. Please update the libraries or the plugin");
|
answer.setText("Some Kotlin libraries attached to this project have unsupported format. Please update the libraries or the plugin")
|
||||||
createShowPathsActionLabel(answer, "Show paths");
|
createShowPathsActionLabel(answer, "Show paths")
|
||||||
}
|
}
|
||||||
return answer;
|
return answer
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void navigateToLibraryRoot(Project project, @NotNull VirtualFile root) {
|
private fun createShowPathsActionLabel(answer: EditorNotificationPanel, labelText: String) {
|
||||||
new OpenFileDescriptor(project, root).navigate(true);
|
val label = Ref<Component>(null)
|
||||||
|
val action = Runnable {
|
||||||
|
DumbService.getInstance(project).tryRunReadActionInSmartMode({
|
||||||
|
val badRoots = collectBadRoots(project)
|
||||||
|
assert(!badRoots.isEmpty()) { "This action should only be called when bad roots are present" }
|
||||||
|
|
||||||
|
val listPopupModel = LibraryRootsPopupModel("Unsupported format", project, badRoots)
|
||||||
|
val popup = JBPopupFactory.getInstance().createListPopup(listPopupModel)
|
||||||
|
popup.showUnderneathOf(label.get())
|
||||||
|
|
||||||
|
null
|
||||||
|
}, "Can't show all paths during index update")
|
||||||
|
}
|
||||||
|
label.set(answer.createActionLabel(labelText, action))
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createShowPathsActionLabel(EditorNotificationPanel answer, String labelText) {
|
override fun getKey(): Key<EditorNotificationPanel> {
|
||||||
final Ref<Component> label = new Ref<Component>(null);
|
return KEY
|
||||||
Runnable action = new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
DumbService.getInstance(project).tryRunReadActionInSmartMode(new Computable<Object>() {
|
|
||||||
@Override
|
|
||||||
public Object compute() {
|
|
||||||
Collection<VirtualFile> badRoots = collectBadRoots(project);
|
|
||||||
assert !badRoots.isEmpty() : "This action should only be called when bad roots are present";
|
|
||||||
|
|
||||||
LibraryRootsPopupModel listPopupModel = new LibraryRootsPopupModel("Unsupported format", project, badRoots);
|
|
||||||
ListPopup popup = JBPopupFactory.getInstance().createListPopup(listPopupModel);
|
|
||||||
popup.showUnderneathOf(label.get());
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, "Can't show all paths during index update");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
label.set(answer.createActionLabel(labelText, action));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
override fun createNotificationPanel(file: VirtualFile, fileEditor: FileEditor): EditorNotificationPanel? {
|
||||||
@Override
|
|
||||||
public Key<EditorNotificationPanel> getKey() {
|
|
||||||
return KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public EditorNotificationPanel createNotificationPanel(@NotNull VirtualFile file, @NotNull FileEditor fileEditor) {
|
|
||||||
try {
|
try {
|
||||||
if (DumbService.isDumb(project)) return null;
|
if (DumbService.isDumb(project)) return null
|
||||||
if (ApplicationManager.getApplication().isUnitTestMode()) return null;
|
if (ApplicationManager.getApplication().isUnitTestMode) return null
|
||||||
if (file.getFileType() != KotlinFileType.INSTANCE) return null;
|
if (file.fileType !== KotlinFileType.INSTANCE) return null
|
||||||
|
|
||||||
if (CompilerManager.getInstance(project).isExcludedFromCompilation(file)) return null;
|
if (CompilerManager.getInstance(project).isExcludedFromCompilation(file)) return null
|
||||||
|
|
||||||
Module module = ModuleUtilCore.findModuleForFile(file, project);
|
val module = ModuleUtilCore.findModuleForFile(file, project) ?: return null
|
||||||
if (module == null) return null;
|
|
||||||
|
|
||||||
if (!ConfigureKotlinInProjectUtilsKt.isModuleConfigured(module)) return null;
|
if (!isModuleConfigured(module)) return null
|
||||||
|
|
||||||
return checkAndCreate(project);
|
return checkAndCreate(project)
|
||||||
}
|
}
|
||||||
catch (ProcessCanceledException e) {
|
catch (e: ProcessCanceledException) {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
catch (IndexNotReadyException e) {
|
catch (e: IndexNotReadyException) {
|
||||||
DumbService.getInstance(project).runWhenSmart(updateNotifications);
|
DumbService.getInstance(project).runWhenSmart(updateNotifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class LibraryRootsPopupModel extends BaseListPopupStep<VirtualFile> {
|
private class LibraryRootsPopupModel(title: String, private val project: Project, roots: Collection<VirtualFile>) : BaseListPopupStep<VirtualFile>(title, roots.toArray<VirtualFile>(arrayOfNulls<VirtualFile>(roots.size))) {
|
||||||
private final Project project;
|
|
||||||
|
|
||||||
public LibraryRootsPopupModel(@NotNull String title, @NotNull Project project, @NotNull Collection<VirtualFile> roots) {
|
override fun getTextFor(root: VirtualFile): String {
|
||||||
super(title, roots.toArray(new VirtualFile[roots.size()]));
|
val relativePath = VfsUtilCore.getRelativePath(root, project.baseDir, '/')
|
||||||
this.project = project;
|
return relativePath ?: root.path
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
override fun getIconFor(aValue: VirtualFile): Icon? {
|
||||||
@Override
|
if (aValue.isDirectory) {
|
||||||
public String getTextFor(VirtualFile root) {
|
return AllIcons.Nodes.Folder
|
||||||
String relativePath = VfsUtilCore.getRelativePath(root, project.getBaseDir(), '/');
|
|
||||||
return relativePath != null ? relativePath : root.getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Icon getIconFor(VirtualFile aValue) {
|
|
||||||
if (aValue.isDirectory()) {
|
|
||||||
return AllIcons.Nodes.Folder;
|
|
||||||
}
|
}
|
||||||
return AllIcons.FileTypes.Archive;
|
return AllIcons.FileTypes.Archive
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
override fun onChosen(selectedValue: VirtualFile?, finalChoice: Boolean): PopupStep<Any>? {
|
||||||
public PopupStep onChosen(VirtualFile selectedValue, boolean finalChoice) {
|
navigateToLibraryRoot(project, selectedValue)
|
||||||
navigateToLibraryRoot(project, selectedValue);
|
return PopupStep.FINAL_CHOICE
|
||||||
return FINAL_CHOICE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
override fun isSpeedSearchEnabled(): Boolean {
|
||||||
public boolean isSpeedSearchEnabled() {
|
return true
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ErrorNotificationPanel extends EditorNotificationPanel {
|
private class ErrorNotificationPanel : EditorNotificationPanel() {
|
||||||
public ErrorNotificationPanel() {
|
init {
|
||||||
myLabel.setIcon(AllIcons.General.Error);
|
myLabel.icon = AllIcons.General.Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Runnable updateNotifications = new Runnable() {
|
private val updateNotifications = Runnable { updateNotifications() }
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
updateNotifications();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private void updateNotifications() {
|
private fun updateNotifications() {
|
||||||
ApplicationManager.getApplication().invokeLater(new Runnable() {
|
ApplicationManager.getApplication().invokeLater {
|
||||||
@Override
|
if (!project.isDisposed) {
|
||||||
public void run() {
|
EditorNotifications.getInstance(project).updateAllNotifications()
|
||||||
if (!project.isDisposed()) {
|
|
||||||
EditorNotifications.getInstance(project).updateAllNotifications();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
companion object {
|
||||||
private static Collection<VirtualFile> collectBadRoots(@NotNull Project project) {
|
private val KEY = Key.create<EditorNotificationPanel>("unsupported.abi.version")
|
||||||
Collection<VirtualFile> badJVMRoots = KotlinRuntimeLibraryUtilKt.getLibraryRootsWithAbiIncompatibleKotlinClasses(project);
|
|
||||||
Collection<VirtualFile> badJSRoots = KotlinRuntimeLibraryUtilKt.getLibraryRootsWithAbiIncompatibleForKotlinJs(project);
|
|
||||||
|
|
||||||
if (badJVMRoots.isEmpty() && badJSRoots.isEmpty()) return Collections.emptyList();
|
fun checkAndCreate(project: Project): EditorNotificationPanel? {
|
||||||
|
val state = ServiceManager.getService(project, SuppressNotificationState::class.java).state
|
||||||
|
if (state != null && state.isSuppressed) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
Collection<VirtualFile> badRoots = new HashSet<VirtualFile>();
|
val badRoots = collectBadRoots(project)
|
||||||
badRoots.addAll(badJVMRoots);
|
if (!badRoots.isEmpty()) {
|
||||||
badRoots.addAll(badJSRoots);
|
return UnsupportedAbiVersionNotificationPanelProvider(project).doCreate(badRoots)
|
||||||
|
}
|
||||||
|
|
||||||
return badRoots;
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToLibraryRoot(project: Project, root: VirtualFile) {
|
||||||
|
OpenFileDescriptor(project, root).navigate(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collectBadRoots(project: Project): Collection<VirtualFile> {
|
||||||
|
val badJVMRoots = getLibraryRootsWithAbiIncompatibleKotlinClasses(project)
|
||||||
|
val badJSRoots = getLibraryRootsWithAbiIncompatibleForKotlinJs(project)
|
||||||
|
|
||||||
|
if (badJVMRoots.isEmpty() && badJSRoots.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
val badRoots = HashSet<VirtualFile>()
|
||||||
|
badRoots.addAll(badJVMRoots)
|
||||||
|
badRoots.addAll(badJSRoots)
|
||||||
|
|
||||||
|
return badRoots
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user