Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
069e682
Add command to show the current PR
jcansdale Aug 9, 2017
c9d38df
Add PullRequestStatusView to SccStatusBar
jcansdale Aug 1, 2017
e6f55fd
Add a simple ViewModel for PullRequestStatusView
jcansdale Aug 1, 2017
8b6cfa2
Wire up property change events
jcansdale Aug 1, 2017
91dcb3d
Make status invisible when not on PR branch
jcansdale Aug 1, 2017
a93a467
Open details view when PR status is clicked on
jcansdale Aug 9, 2017
6d8c441
Add hack to ensure that PR status appears
jcansdale Aug 9, 2017
5f63606
Restore missing using System.Globalization
jcansdale Aug 29, 2017
6f9efbd
Find status bar part on main window
jcansdale Aug 29, 2017
c5f7cf5
Show PR status after solution has loaded
jcansdale Aug 29, 2017
6362790
Fix duplicate using
jcansdale Sep 12, 2017
11b675a
Move PullRequestStatus initialization to own package
jcansdale Sep 12, 2017
c6ee258
Only create PR status view when session changes
jcansdale Sep 12, 2017
6bb88ba
Merge branch 'master' into ui/pr-context
jcansdale Sep 13, 2017
a3585d4
Merge branch 'master' into ui/pr-context
jcansdale Dec 12, 2017
4f0da47
Use Serilog and fix CA error
jcansdale Dec 12, 2017
4b5296c
Merge branch 'master' into ui/pr-context
jcansdale Dec 12, 2017
c185629
Merge branch 'master' into ui/pr-context
jcansdale Dec 15, 2017
1209998
Merge pull request #1102 from github/ui/pr-context
jcansdale Dec 18, 2017
e2d753c
Add usage metrics for ShowCurrentPullRequest
jcansdale Dec 18, 2017
f4b3115
Start a button
donokuda Dec 19, 2017
8e26aad
Use visual studio color keys
donokuda Dec 19, 2017
7e1bcc0
Set up some triggers
donokuda Dec 21, 2017
89ce001
Transparent background by default
donokuda Dec 21, 2017
09c58f3
/shrug
donokuda Dec 21, 2017
68895cd
Set the hover state correctly
donokuda Jan 4, 2018
562bac5
Merge branch 'master' into feature/show-current-pr
jcansdale Jan 4, 2018
4ef8387
Merge branch 'master' into feature/show-current-pr
jcansdale Jan 15, 2018
5a2227e
Merge branch 'fixes/1408-change-branch-event-squashed' into feature/s…
jcansdale Jan 17, 2018
942953d
Merge pull request #1402 from github/ui/show-current-pr-polish
jcansdale Jan 17, 2018
eb941af
Merge branch 'feature/show-current-pr' of https://github.com/github/V…
jcansdale Jan 17, 2018
fa120f0
Merge branch 'fixes/1408-change-branch-event-squashed' into feature/s…
jcansdale Jan 18, 2018
a6fa2a0
Merge branch 'fixes/1408-change-branch-event-squashed' into feature/s…
jcansdale Jan 18, 2018
efd89ce
Merge branch 'fixes/1408-change-branch-event-squashed' into feature/s…
jcansdale Jan 19, 2018
bbd8bd1
Merge branch 'fixes/1408-change-branch-event-squashed' into feature/s…
jcansdale Jan 23, 2018
0ea3eff
Change to using IGitHubServiceProvider
jcansdale Jan 23, 2018
f76d113
Merge branch 'fixes/1408-change-branch-event-squashed' into feature/s…
jcansdale Jan 24, 2018
5ea1f0f
Merge branch 'fixes/1408-change-branch-event-squashed' into feature/s…
jcansdale Jan 25, 2018
ee6d5a8
Merge branch 'fixes/1408-change-branch-event-squashed' into feature/s…
jcansdale Jan 29, 2018
6cb2bcc
Merge branch 'master' into feature/show-current-pr
jcansdale Jan 29, 2018
975816e
Merge branch 'master' into feature/show-current-pr
jcansdale Jan 29, 2018
5206bd1
Merge branch 'fixes/1440-clear-PR-session' into feature/show-current-pr
jcansdale Jan 30, 2018
ad0f8cf
Merge branch 'master' into feature/show-current-pr
jcansdale Feb 6, 2018
41e9187
Merge branch 'master' into feature/show-current-pr
jcansdale Feb 7, 2018
3b54143
Merge branch 'master' into feature/show-current-pr
jcansdale Feb 9, 2018
4503596
Make PR status visible when GitSccProvider is loaded
jcansdale Feb 9, 2018
eb3ec74
Merge branch 'master' into feature/show-current-pr
grokys Feb 9, 2018
04e5422
Rename PullRequestStatusManager to PullRequestStatusBarManager
jcansdale Feb 9, 2018
4ce0d65
Show PR number as well as title on tooltip
jcansdale Feb 9, 2018
e9f0c17
Convert PullRequestStatusPackage to an AsyncPackage
jcansdale Feb 9, 2018
c6750b9
Rename PullRequestStatusPackage to ..StatusBarPackage
jcansdale Feb 9, 2018
dc81286
Lazily initialize when we're on a GitHub repo
jcansdale Feb 9, 2018
20879e7
Pass MainWindow to PullRequestStatusBarManager.Initialize
jcansdale Feb 9, 2018
0039538
Add some xmldoc comments
jcansdale Feb 9, 2018
f356907
Add fix for hanging InitializeAsync
jcansdale Feb 9, 2018
323f330
Ensure consistent task ordering using ContinueWith
jcansdale Feb 23, 2018
5200e72
Rename InitializeTask to PendingTasks
jcansdale Feb 23, 2018
97eaced
Remove redundant initialized flag
jcansdale Feb 23, 2018
1ea222a
Lazy load VSGitExt on Main thread
jcansdale Feb 23, 2018
68d098a
Do initialization asynchronously on Main thread
jcansdale Feb 26, 2018
dcd806f
Add comment about using from background thread
jcansdale Feb 26, 2018
2a3cef5
Factor out GetServiceAsync
jcansdale Feb 26, 2018
66fbbaa
Load VS services asynchronously
jcansdale Feb 26, 2018
16aabcd
Make grammarful English
jcansdale Feb 27, 2018
1205790
Remove dependency on IComponentModel
jcansdale Feb 27, 2018
2df68b1
Merge branch 'master' into feature/show-current-pr
jcansdale Feb 27, 2018
ebbc6a6
Let PR # appear on status bar before solution has loaded
jcansdale Feb 27, 2018
cf2bc71
Merge branch 'fixes/1493-using-ContinueWith' into feature/show-curren…
jcansdale Feb 27, 2018
f23fd24
Expose IVSGitExt as async VS service
jcansdale Feb 27, 2018
921f8c5
Add generic GetServiceAsync to IGitHubServiceProvider
jcansdale Feb 28, 2018
ace5498
Use DTE.Version to choose IVSGitExt implementaiton
jcansdale Feb 28, 2018
738b046
Add some comments
jcansdale Feb 28, 2018
f3865b1
Use a delegate for GetServiceAsync
jcansdale Feb 28, 2018
cc6a112
Move VSGitExtPart into VSGitExtDispatcher
jcansdale Feb 28, 2018
e517687
Move CreateVSGitExt into VSGitExtFactory
jcansdale Feb 28, 2018
b98cc52
Merge VSGitExtDispatcher into VSGitExtFactory
jcansdale Feb 28, 2018
719e624
Move all construction logic into VSGitExtFactory.Create
jcansdale Feb 28, 2018
af9f364
Add comments to factory method
jcansdale Feb 28, 2018
fc06da7
Use ApplicationInfo.GetHostVersionInfo not DTE.Version
jcansdale Feb 28, 2018
ee9ab4a
Simplify the VSGitExt service
jcansdale Mar 2, 2018
0eadf70
Merge pull request #1513 from github/refactor/simplify-VSGitExt
jcansdale Mar 2, 2018
32d2805
Merge pull request #1510 from github/refactor/convert-VSGitExt-to-ser…
jcansdale Mar 2, 2018
338860d
Merge branch 'master' into feature/show-current-pr
jcansdale Mar 2, 2018
c13c597
Merge branch 'master' into feature/show-current-pr
jcansdale Mar 5, 2018
d72ff9b
Use IAsyncServiceProvider rather than delegate
jcansdale Mar 5, 2018
78f9d71
Give alias to ..Shell.Framework rather than ..Shell.Immutable.14
jcansdale Mar 5, 2018
05e74b0
Merge branch 'master' into feature/show-current-pr
jcansdale Mar 5, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 27 additions & 34 deletions src/GitHub.App/ViewModels/GitHubPane/GitHubPaneViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ public sealed class GitHubPaneViewModel : ViewModelBase, IGitHubPaneViewModel, I
readonly ReactiveCommand<Unit> refresh;
readonly ReactiveCommand<Unit> showPullRequests;
readonly ReactiveCommand<object> openInBrowser;
readonly SemaphoreSlim initializing = new SemaphoreSlim(1);
bool initialized;
Task initializeTask;
IViewModel content;
ILocalRepositoryModel localRepository;
string searchQuery;
Expand Down Expand Up @@ -201,39 +200,9 @@ public void Dispose()
}

/// <inheritdoc/>
public async Task InitializeAsync(IServiceProvider paneServiceProvider)
public Task InitializeAsync(IServiceProvider paneServiceProvider)
{
await initializing.WaitAsync();
if (initialized) return;

try
{
await UpdateContent(teamExplorerContext.ActiveRepository);
teamExplorerContext.WhenAnyValue(x => x.ActiveRepository)
.Skip(1)
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(x => UpdateContent(x).Forget());

connectionManager.Connections.CollectionChanged += (_, __) => UpdateContent(LocalRepository).Forget();

BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.pullRequestCommand, showPullRequests);
BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.backCommand, navigator.NavigateBack);
BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.forwardCommand, navigator.NavigateForward);
BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.refreshCommand, refresh);
BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.githubCommand, openInBrowser);

paneServiceProvider.AddCommandHandler(Guids.guidGitHubToolbarCmdSet, PkgCmdIDList.helpCommand,
(_, __) =>
{
browser.OpenUrl(new Uri(GitHubUrls.Documentation));
usageTracker.IncrementCounter(x => x.NumberOfGitHubPaneHelpClicks).Forget();
});
}
finally
{
initialized = true;
initializing.Release();
}
return initializeTask = initializeTask ?? CreateInitializeTask(paneServiceProvider);
}

/// <inheritdoc/>
Expand Down Expand Up @@ -307,6 +276,30 @@ public Task ShowPullRequest(string owner, string repo, int number)
x => x.RemoteRepositoryOwner == owner && x.LocalRepository.Name == repo && x.Number == number);
}

async Task CreateInitializeTask(IServiceProvider paneServiceProvider)
{
await UpdateContent(teamExplorerContext.ActiveRepository);
teamExplorerContext.WhenAnyValue(x => x.ActiveRepository)
.Skip(1)
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(x => UpdateContent(x).Forget());

connectionManager.Connections.CollectionChanged += (_, __) => UpdateContent(LocalRepository).Forget();

BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.pullRequestCommand, showPullRequests);
BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.backCommand, navigator.NavigateBack);
BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.forwardCommand, navigator.NavigateForward);
BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.refreshCommand, refresh);
BindNavigatorCommand(paneServiceProvider, PkgCmdIDList.githubCommand, openInBrowser);

paneServiceProvider.AddCommandHandler(Guids.guidGitHubToolbarCmdSet, PkgCmdIDList.helpCommand,
(_, __) =>
{
browser.OpenUrl(new Uri(GitHubUrls.Documentation));
usageTracker.IncrementCounter(x => x.NumberOfGitHubPaneHelpClicks).Forget();
});
}

OleMenuCommand BindNavigatorCommand<T>(IServiceProvider paneServiceProvider, int commandId, ReactiveCommand<T> command)
{
Guard.ArgumentNotNull(paneServiceProvider, nameof(paneServiceProvider));
Expand Down
1 change: 1 addition & 0 deletions src/GitHub.Exports/Models/UsageModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public struct UsageModel
public int NumberOfPRDetailsNavigateToEditor { get; set; }
public int NumberOfPRReviewDiffViewInlineCommentOpen { get; set; }
public int NumberOfPRReviewDiffViewInlineCommentPost { get; set; }
public int NumberOfShowCurrentPullRequest { get; set; }

public UsageModel Clone(bool includeWeekly, bool includeMonthly)
{
Expand Down
2 changes: 1 addition & 1 deletion src/GitHub.Exports/Services/IVSUIContextFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ public VSUIContextChangedEventArgs(bool activated)
public interface IVSUIContext
{
bool IsActive { get; }
event EventHandler<VSUIContextChangedEventArgs> UIContextChanged;
void WhenActivated(Action action);
}
}
1 change: 1 addition & 0 deletions src/GitHub.Exports/Settings/Guids.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public static class Guids
public const string StartPagePackageId = "3b764d23-faf7-486f-94c7-b3accc44a70e";
public const string CodeContainerProviderId = "6CE146CB-EF57-4F2C-A93F-5BA685317660";
public const string InlineReviewsPackageId = "248325BE-4A2D-4111-B122-E7D59BF73A35";
public const string PullRequestStatusPackageId = "5121BEC6-1088-4553-8453-0DDC7C8E2238";
public const string TeamExplorerWelcomeMessage = "C529627F-8AA6-4FDB-82EB-4BFB7DB753C3";
public const string LoginManagerId = "7BA2071A-790A-4F95-BE4A-0EEAA5928AAF";

Expand Down
10 changes: 10 additions & 0 deletions src/GitHub.InlineReviews/GitHub.InlineReviews.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,12 @@
<Compile Include="Glyph\GlyphMargin.cs" />
<Compile Include="Glyph\GlyphMarginVisualManager.cs" />
<Compile Include="Glyph\IGlyphFactory.cs" />
<Compile Include="PullRequestStatusBarPackage.cs" />
<Compile Include="InlineReviewsPackage.cs" />
<Compile Include="Models\InlineCommentThreadModel.cs" />
<Compile Include="Models\PullRequestSessionLiveFile.cs" />
<Compile Include="Models\PullRequestSessionFile.cs" />
<Compile Include="Services\PullRequestStatusBarManager.cs" />
<Compile Include="Tags\MouseEnterAndLeaveEventRouter.cs" />
<Compile Include="Peek\InlineCommentPeekableItem.cs" />
<Compile Include="Peek\InlineCommentPeekableItemSource.cs" />
Expand Down Expand Up @@ -124,6 +126,7 @@
<Compile Include="ViewModels\IPullRequestCommentsViewModel.cs" />
<Compile Include="ViewModels\IssueCommentThreadViewModel.cs" />
<Compile Include="ViewModels\PullRequestCommentsViewModel.cs" />
<Compile Include="ViewModels\PullRequestStatusViewModel.cs" />
<Compile Include="Views\DiffCommentThreadView.xaml.cs">
<DependentUpon>DiffCommentThreadView.xaml</DependentUpon>
</Compile>
Expand Down Expand Up @@ -161,6 +164,9 @@
<Compile Include="Views\CommentView.xaml.cs">
<DependentUpon>CommentView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\PullRequestStatusView.xaml.cs">
<DependentUpon>PullRequestStatusView.xaml</DependentUpon>
</Compile>
<Compile Include="VisualStudioExtensions.cs" />
</ItemGroup>
<ItemGroup>
Expand Down Expand Up @@ -443,6 +449,10 @@
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\PullRequestStatusView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Analyzer Include="..\..\packages\SerilogAnalyzer.0.12.0.0\analyzers\dotnet\cs\SerilogAnalyzer.dll" />
Expand Down
2 changes: 1 addition & 1 deletion src/GitHub.InlineReviews/InlineReviewsPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace GitHub.InlineReviews
[Guid(Guids.InlineReviewsPackageId)]
[ProvideAutoLoad(UIContextGuids80.SolutionExists)]
[ProvideMenuResource("Menus.ctmenu", 1)]
[ProvideToolWindow(typeof(PullRequestCommentsPane), DocumentLikeTool=true)]
[ProvideToolWindow(typeof(PullRequestCommentsPane), DocumentLikeTool = true)]
public class InlineReviewsPackage : Package
{
protected override void Initialize()
Expand Down
29 changes: 29 additions & 0 deletions src/GitHub.InlineReviews/PullRequestStatusBarPackage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System;
using System.Threading;
using System.Runtime.InteropServices;
using GitHub.Services;
using GitHub.VisualStudio;
using GitHub.InlineReviews.Services;
using Microsoft.VisualStudio.Shell;
using Task = System.Threading.Tasks.Task;

namespace GitHub.InlineReviews
{
[Guid(Guids.PullRequestStatusPackageId)]
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[ProvideAutoLoad(Guids.GitSccProviderId, PackageAutoLoadFlags.BackgroundLoad)]
public class PullRequestStatusBarPackage : AsyncPackage
{
/// <summary>
/// Initialize the PR status UI on Visual Studio's status bar.
/// </summary>
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
var usageTracker = (IUsageTracker)await GetServiceAsync(typeof(IUsageTracker));
var serviceProvider = (IGitHubServiceProvider)await GetServiceAsync(typeof(IGitHubServiceProvider));
var gitExt = (IVSGitExt)await GetServiceAsync(typeof(IVSGitExt));

new PullRequestStatusBarManager(gitExt, usageTracker, serviceProvider);
}
}
}
169 changes: 169 additions & 0 deletions src/GitHub.InlineReviews/Services/PullRequestStatusBarManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.ComponentModel;
using System.ComponentModel.Composition;
using GitHub.InlineReviews.Views;
using GitHub.InlineReviews.ViewModels;
using GitHub.Services;
using GitHub.VisualStudio;
using GitHub.Models;
using GitHub.Logging;
using GitHub.Extensions;
using Serilog;

namespace GitHub.InlineReviews.Services
{
public class PullRequestStatusBarManager
{
static readonly ILogger log = LogManager.ForContext<PullRequestStatusBarManager>();
const string StatusBarPartName = "PART_SccStatusBarHost";

readonly IVSGitExt gitExt;
readonly IUsageTracker usageTracker;
readonly IGitHubServiceProvider serviceProvider;

IPullRequestSessionManager pullRequestSessionManager;

[ImportingConstructor]
public PullRequestStatusBarManager(IVSGitExt gitExt, IUsageTracker usageTracker, IGitHubServiceProvider serviceProvider)
{
this.gitExt = gitExt;
this.usageTracker = usageTracker;
this.serviceProvider = serviceProvider;

OnActiveRepositoriesChanged();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be called twice!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be fixed in #1512.

gitExt.ActiveRepositoriesChanged += OnActiveRepositoriesChanged;
}

void OnActiveRepositoriesChanged()
{
if (gitExt.ActiveRepositories.Count > 0)
{
gitExt.ActiveRepositoriesChanged -= OnActiveRepositoriesChanged;
Application.Current.Dispatcher.Invoke(() => StartShowingStatus());
}
}

void StartShowingStatus()
{
try
{
// Create just in time on Main thread.
pullRequestSessionManager = serviceProvider.GetService<IPullRequestSessionManager>();

RefreshCurrentSession();
pullRequestSessionManager.PropertyChanged += PullRequestSessionManager_PropertyChanged;
}
catch (Exception e)
{
log.Error(e, "Error initializing");
}
}

void PullRequestSessionManager_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(PullRequestSessionManager.CurrentSession))
{
RefreshCurrentSession();
}
}

void RefreshCurrentSession()
{
var pullRequest = pullRequestSessionManager.CurrentSession?.PullRequest;
var viewModel = pullRequest != null ? CreatePullRequestStatusViewModel(pullRequest) : null;
ShowStatus(viewModel);
}

PullRequestStatusViewModel CreatePullRequestStatusViewModel(IPullRequestModel pullRequest)
{
var dte = serviceProvider.TryGetService<EnvDTE.DTE>();
var command = new RaisePullRequestCommand(dte, usageTracker);
var pullRequestStatusViewModel = new PullRequestStatusViewModel(command);
pullRequestStatusViewModel.Number = pullRequest.Number;
pullRequestStatusViewModel.Title = pullRequest.Title;
return pullRequestStatusViewModel;
}

void ShowStatus(PullRequestStatusViewModel pullRequestStatusViewModel = null)
{
var statusBar = FindSccStatusBar(Application.Current.MainWindow);
if (statusBar != null)
{
var githubStatusBar = Find<PullRequestStatusView>(statusBar);
if (githubStatusBar != null)
{
// Replace to ensure status shows up.
statusBar.Items.Remove(githubStatusBar);
}

if (pullRequestStatusViewModel != null)
{
githubStatusBar = new PullRequestStatusView { DataContext = pullRequestStatusViewModel };
statusBar.Items.Insert(0, githubStatusBar);
}
}
}

static T Find<T>(StatusBar statusBar)
{
foreach (var item in statusBar.Items)
{
if (item is T)
{
return (T)item;
}
}

return default(T);
}

StatusBar FindSccStatusBar(Window mainWindow)
{
var contentControl = mainWindow?.Template?.FindName(StatusBarPartName, mainWindow) as ContentControl;
return contentControl?.Content as StatusBar;
}

class RaisePullRequestCommand : ICommand
{
readonly string guid = Guids.guidGitHubCmdSetString;
readonly int id = PkgCmdIDList.showCurrentPullRequestCommand;

readonly EnvDTE.DTE dte;
readonly IUsageTracker usageTracker;

internal RaisePullRequestCommand(EnvDTE.DTE dte, IUsageTracker usageTracker)
{
this.dte = dte;
this.usageTracker = usageTracker;
}

public bool CanExecute(object parameter) => true;

public void Execute(object parameter)
{
try
{
object customIn = null;
object customOut = null;
dte?.Commands.Raise(guid, id, ref customIn, ref customOut);
}
catch (Exception e)
{
log.Error(e, "Couldn't raise {Guid}:{ID}", guid, id);
}

usageTracker.IncrementCounter(x => x.NumberOfShowCurrentPullRequest).Forget();
}

public event EventHandler CanExecuteChanged
{
add { }
remove { }
}
}
}
}
Loading