Repository: CodeHubApp/CodeHub Branch: master Commit: 7c7df03cdee9 Files: 459 Total size: 28.5 MB Directory structure: gitextract_r1oivz_j/ ├── .gitignore ├── CodeHub/ │ ├── CodeHub.csproj │ ├── Octicons.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── WebViews/ │ │ ├── CommentsModel.cs │ │ ├── CommentsWebView.cs │ │ ├── CommentsWebView.cshtml │ │ ├── DiffCommentModel.cs │ │ ├── DiffModel.cs │ │ ├── DiffWebView.cs │ │ ├── DiffWebView.cshtml │ │ ├── MarkdownModel.cs │ │ ├── MarkdownWebView.cs │ │ ├── MarkdownWebView.cshtml │ │ ├── SyntaxHighlighterModel.cs │ │ ├── SyntaxHighlighterWebView.cs │ │ ├── SyntaxHighlighterWebView.cshtml │ │ ├── UpgradeDetailsModel.cs │ │ ├── UpgradeDetailsWebView.cs │ │ └── UpgradeDetailsWebView.cshtml │ └── packages.config ├── CodeHub.Core/ │ ├── App.cs │ ├── CodeHub.Core.csproj │ ├── Data/ │ │ ├── Account.cs │ │ ├── ImgurResponse.cs │ │ ├── Language.cs │ │ ├── LanguageRepository.cs │ │ └── TrendingRepository.cs │ ├── Extensions/ │ │ ├── CommandExtensions.cs │ │ ├── ExceptionExtensions.cs │ │ ├── GitHubClientExtensions.cs │ │ ├── ObservableExtensions.cs │ │ ├── ReactiveListExtensions.cs │ │ ├── StringExtensions.cs │ │ └── TaskExtensions.cs │ ├── Filters/ │ │ ├── BaseIssuesFilterModel.cs │ │ ├── IssuesFilterModel.cs │ │ ├── MyIssuesFilterModel.cs │ │ └── NotificationsFilterModel.cs │ ├── Interactions.cs │ ├── Messages/ │ │ ├── GistAddMessage.cs │ │ ├── IssueAddMessage.cs │ │ ├── IssueEditMessage.cs │ │ ├── LogoutMessage.cs │ │ ├── NotificationCountMessage.cs │ │ ├── PullRequestEditMessage.cs │ │ ├── SelectIssueLabelsMessage.cs │ │ ├── SelectedAssignedToMessage.cs │ │ ├── SelectedMilestoneMessage.cs │ │ └── SourceEditMessage.cs │ ├── PresentationValues.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── Secrets.cs │ ├── Services/ │ │ ├── AccountsService.cs │ │ ├── ApplicationService.cs │ │ ├── IAccountsService.cs │ │ ├── IAlertDialogService.cs │ │ ├── IApplicationService.cs │ │ ├── IFeaturesService.cs │ │ ├── IImgurService.cs │ │ ├── IMarkdownService.cs │ │ ├── IMessageService.cs │ │ ├── INetworkActivityService.cs │ │ ├── IPushNotificationsService.cs │ │ ├── IViewModelTxService.cs │ │ ├── ImgurService.cs │ │ ├── LoginService.cs │ │ ├── MessageService.cs │ │ └── ViewModelTxService.cs │ ├── Settings.cs │ ├── Utils/ │ │ ├── CustomObservableCollection.cs │ │ ├── DateTimeExtensions.cs │ │ ├── Emojis.cs │ │ ├── FilterGroup.cs │ │ ├── GitHubAvatar.cs │ │ ├── GitHubExtensions.cs │ │ ├── GitHubList.cs │ │ ├── OctokitClientFactory.cs │ │ ├── OctokitNetworkClient.cs │ │ ├── RepositoryIdentifier.cs │ │ ├── ViewModelExtensions.cs │ │ └── WeakReferenceExtensions.cs │ ├── ViewModels/ │ │ ├── Accounts/ │ │ │ ├── AddAccountViewModel.cs │ │ │ └── OAuthLoginViewModel.cs │ │ ├── App/ │ │ │ ├── FeedbackComposerViewModel.cs │ │ │ ├── FeedbackItemViewModel.cs │ │ │ ├── FeedbackViewModel.cs │ │ │ ├── MenuViewModel.cs │ │ │ ├── StartupViewModel.cs │ │ │ └── SupportViewModel.cs │ │ ├── BaseViewModel.cs │ │ ├── Changesets/ │ │ │ ├── ChangesetViewModel.cs │ │ │ ├── ChangesetsViewModel.cs │ │ │ └── CommitsViewModel.cs │ │ ├── CollectionViewModel.cs │ │ ├── Events/ │ │ │ ├── BaseEventsViewModel.cs │ │ │ ├── NewsViewModel.cs │ │ │ ├── OrganizationEventsViewModel.cs │ │ │ ├── RepositoryEventsViewModel.cs │ │ │ └── UserEventsViewModel.cs │ │ ├── FilterGroup.cs │ │ ├── FilterModel.cs │ │ ├── FilterableCollectionViewModel.cs │ │ ├── Gists/ │ │ │ ├── GistCreateViewModel.cs │ │ │ ├── GistItemViewModel.cs │ │ │ ├── GistViewModel.cs │ │ │ └── GistsViewModel.cs │ │ ├── ICanGoToViewModel.cs │ │ ├── IFilterableViewModel.cs │ │ ├── IListViewModel.cs │ │ ├── ILoadableViewModel.cs │ │ ├── IProvidesSearchKeyword.cs │ │ ├── Issues/ │ │ │ ├── BaseIssuesViewModel.cs │ │ │ ├── IssueAddViewModel.cs │ │ │ ├── IssueAssignedToViewModel.cs │ │ │ ├── IssueEditViewModel.cs │ │ │ ├── IssueLabelsViewModel.cs │ │ │ ├── IssueMilestonesViewModel.cs │ │ │ ├── IssueModifyViewModel.cs │ │ │ ├── IssueViewModel.cs │ │ │ ├── IssuesViewModel.cs │ │ │ └── MyIssuesViewModel.cs │ │ ├── LoadableViewModel.cs │ │ ├── MarkdownAccessoryViewModel.cs │ │ ├── Notifications/ │ │ │ └── NotificationsViewModel.cs │ │ ├── Organizations/ │ │ │ ├── OrganizationViewModel.cs │ │ │ ├── OrganizationsViewModel.cs │ │ │ └── TeamsViewModel.cs │ │ ├── PullRequests/ │ │ │ ├── PullRequestCommitsViewModel.cs │ │ │ ├── PullRequestFilesViewModel.cs │ │ │ ├── PullRequestViewModel.cs │ │ │ └── PullRequestsViewModel.cs │ │ ├── Repositories/ │ │ │ ├── RepositoriesViewModel.cs │ │ │ ├── RepositoryItemViewModel.cs │ │ │ ├── RepositoryViewModel.cs │ │ │ └── TrendingRepositoriesViewModel.cs │ │ ├── Search/ │ │ │ ├── ExploreViewModel.cs │ │ │ ├── RepositoryExploreViewModel.cs │ │ │ └── UserExploreViewModel.cs │ │ ├── Source/ │ │ │ └── EditSourceViewModel.cs │ │ ├── Users/ │ │ │ ├── UserItemViewModel.cs │ │ │ ├── UserViewModel.cs │ │ │ └── UsersViewModel.cs │ │ ├── ViewModelExtensions.cs │ │ └── WebBrowserViewModel.cs │ └── packages.config ├── CodeHub.iOS/ │ ├── AkavacheSqliteLinkerOverride.cs │ ├── AppDelegate.cs │ ├── CodeHub.iOS.csproj │ ├── CodeHubIcon.psd │ ├── DialogElements/ │ │ ├── BooleanElement.cs │ │ ├── ChangesetElement.cs │ │ ├── CommentElement.cs │ │ ├── CommitElement.cs │ │ ├── DummyInputElement.cs │ │ ├── Element.cs │ │ ├── EntryElement.cs │ │ ├── ExpandingInputElement.cs │ │ ├── HtmlElement.cs │ │ ├── IElementSizing.cs │ │ ├── IssueElement.cs │ │ ├── LabelElement.cs │ │ ├── LoadMoreElement.cs │ │ ├── MenuElement.cs │ │ ├── MilestoneElement.cs │ │ ├── MultilinedElement.cs │ │ ├── NewsFeedElement.cs │ │ ├── OwnerDrawnElement.cs │ │ ├── PaginateElement.cs │ │ ├── PullRequestElement.cs │ │ ├── RootElement.cs │ │ ├── Section.cs │ │ ├── SplitButtonElement.cs │ │ ├── SplitViewElement.cs │ │ ├── StringElement.cs │ │ └── UserElement.cs │ ├── Entitlements.plist │ ├── Images/ │ │ └── Images.cs │ ├── Info.plist │ ├── Launch.storyboard │ ├── LinkerPleaseInclude.cs │ ├── OcticonExtensions.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── Resources/ │ │ └── Images.xcassets/ │ │ ├── AppIcons.appiconset/ │ │ │ └── Contents.json │ │ ├── Avatar.imageset/ │ │ │ └── Contents.json │ │ └── UnknownUser.imageset/ │ │ └── Contents.json │ ├── Services/ │ │ ├── AlertDialogService.cs │ │ ├── FeaturesService.cs │ │ ├── InAppPurchaseService.cs │ │ ├── MarkdownService.cs │ │ ├── NetworkActivityService.cs │ │ └── PushNotificationsService.cs │ ├── Setup.cs │ ├── TableViewCells/ │ │ ├── CommitCellView.cs │ │ ├── CommitCellView.designer.cs │ │ ├── CommitCellView.xib │ │ ├── FeedbackCellView.cs │ │ ├── FeedbackCellView.designer.cs │ │ ├── FeedbackCellView.xib │ │ ├── GistCellView.cs │ │ ├── GistCellView.designer.cs │ │ ├── GistCellView.xib │ │ ├── IssueCellView.cs │ │ ├── IssueCellView.designer.cs │ │ ├── IssueCellView.xib │ │ ├── MilestoneTableViewCell.cs │ │ ├── MultilinedCellView.cs │ │ ├── MultilinedCellView.designer.cs │ │ ├── MultilinedCellView.xib │ │ ├── NewsCellView.cs │ │ ├── NewsCellView.designer.cs │ │ ├── NewsCellView.xib │ │ ├── PullRequestCellView.cs │ │ ├── PullRequestCellView.designer.cs │ │ ├── PullRequestCellView.xib │ │ ├── RepositoryCellView.cs │ │ ├── RepositoryCellView.designer.cs │ │ ├── RepositoryCellView.xib │ │ └── UserTableViewCell.cs │ ├── TableViewSources/ │ │ ├── DialogTableViewSource.cs │ │ ├── FeedbackTableViewSource.cs │ │ ├── GistTableViewSource.cs │ │ ├── ReactiveTableViewSource.cs │ │ ├── RepositoryTableViewSource.cs │ │ └── UserTableViewSource.cs │ ├── Theme.cs │ ├── TouchViewPresenter.cs │ ├── Transitions/ │ │ └── SlideDownTransition.cs │ ├── UrlRouterProvider.cs │ ├── Utilities/ │ │ ├── EasyLayout.cs │ │ ├── Graphics.cs │ │ ├── Hud.cs │ │ ├── NetworkActivity.cs │ │ ├── ReactiveCommandExtensions.cs │ │ ├── ShaType.cs │ │ ├── UIImageExtensions.cs │ │ ├── UIImageViewExtensions.cs │ │ ├── UIKitExtensions.cs │ │ ├── UIWindowExtensions.cs │ │ └── ViewControllerExtensions.cs │ ├── ViewControllers/ │ │ ├── Accounts/ │ │ │ ├── AccountsViewController.cs │ │ │ ├── AddAccountView.xib │ │ │ ├── AddAccountViewController.cs │ │ │ ├── AddAccountViewController.designer.cs │ │ │ ├── NewAccountViewController.cs │ │ │ └── OAuthLoginViewController.cs │ │ ├── Application/ │ │ │ ├── EnterpriseSupportViewController.cs │ │ │ ├── FeedbackComposerViewController.cs │ │ │ ├── FeedbackViewController.cs │ │ │ ├── MenuViewController.cs │ │ │ ├── StartupViewController.cs │ │ │ ├── SupportViewController.cs │ │ │ └── UpgradeViewController.cs │ │ ├── BaseDialogViewController.cs │ │ ├── BaseViewController.cs │ │ ├── BaseWebViewController.cs │ │ ├── Composer.cs │ │ ├── DialogViewController.cs │ │ ├── Events/ │ │ │ ├── BaseEventsViewController.cs │ │ │ ├── NewsViewController.cs │ │ │ ├── OrganizationEventsViewController.cs │ │ │ ├── RepositoryEventsViewController.cs │ │ │ └── UserEventsViewController.cs │ │ ├── FilterViewController.cs │ │ ├── Filters/ │ │ │ ├── IssueMilestonesFilterViewController.cs │ │ │ ├── IssuesFilterViewController.cs │ │ │ └── MyIssuesFilterViewController.cs │ │ ├── Gists/ │ │ │ ├── GistCreateViewController.cs │ │ │ ├── GistEditViewController.cs │ │ │ ├── GistFileModifyViewController.cs │ │ │ ├── GistFileViewController.cs │ │ │ ├── GistViewController.cs │ │ │ └── GistsViewController.cs │ │ ├── MarkdownComposerViewController.cs │ │ ├── MessageComposerViewController.cs │ │ ├── MultipleChoiceViewController.cs │ │ ├── Organizations/ │ │ │ ├── OrganizationViewController.cs │ │ │ ├── OrganizationsViewController.cs │ │ │ └── TeamsViewController.cs │ │ ├── PullRequests/ │ │ │ └── PullRequestDiffViewController.cs │ │ ├── Repositories/ │ │ │ ├── LanguagesViewController.cs │ │ │ ├── PrivateRepositoryViewController.cs │ │ │ ├── PrivateRepositoryViewController.designer.cs │ │ │ ├── PrivateRepositoryViewController.xib │ │ │ ├── ReadmeViewController.cs │ │ │ ├── RepositoriesViewController.cs │ │ │ ├── RepositoryViewController.cs │ │ │ └── TrendingRepositoriesViewController.cs │ │ ├── Search/ │ │ │ ├── ExploreViewController.cs │ │ │ ├── RepositoryExploreViewController.cs │ │ │ └── UserExploreViewController.cs │ │ ├── Settings/ │ │ │ ├── DefaultStartupViewController.cs │ │ │ ├── SettingsViewController.cs │ │ │ ├── SyntaxHighlightExample │ │ │ └── SyntaxHighlighterViewController.cs │ │ ├── Source/ │ │ │ ├── AddSourceViewController.cs │ │ │ ├── BranchesAndTagsViewController.cs │ │ │ ├── BranchesViewController.cs │ │ │ ├── CommitDiffViewController.cs │ │ │ ├── FileSourceViewController.cs │ │ │ ├── SourceTreeViewController.cs │ │ │ └── TagsViewController.cs │ │ ├── TableViewController.cs │ │ ├── ThemedNavigationController.cs │ │ ├── Users/ │ │ │ ├── UserViewController.cs │ │ │ └── UsersViewController.cs │ │ ├── ViewModelCollectionDrivenDialogViewController.cs │ │ ├── ViewModelDrivenDialogViewController.cs │ │ ├── Walkthrough/ │ │ │ ├── AboutViewController.cs │ │ │ ├── AboutViewController.designer.cs │ │ │ ├── AboutViewController.xib │ │ │ ├── CardPageViewController.cs │ │ │ ├── CardPageViewController.designer.cs │ │ │ ├── CardPageViewController.xib │ │ │ ├── FeedbackViewController.cs │ │ │ ├── FeedbackViewController.designer.cs │ │ │ ├── FeedbackViewController.xib │ │ │ ├── GoProViewController.cs │ │ │ ├── GoProViewController.designer.cs │ │ │ ├── GoProViewController.xib │ │ │ ├── OrgViewController.cs │ │ │ ├── OrgViewController.designer.cs │ │ │ ├── OrgViewController.xib │ │ │ ├── PromoteViewController.cs │ │ │ ├── PromoteViewController.designer.cs │ │ │ ├── PromoteViewController.xib │ │ │ ├── WelcomePageViewController.cs │ │ │ ├── WelcomeViewController.cs │ │ │ ├── WelcomeViewController.designer.cs │ │ │ └── WelcomeViewController.xib │ │ └── WebBrowserViewController.cs │ ├── Views/ │ │ ├── AddRemoveView.cs │ │ ├── BlurredAlertView.cs │ │ ├── ButtonAccessoryView.cs │ │ ├── EmptyListView.cs │ │ ├── ExtendedUITextView.cs │ │ ├── ImageAndTitleHeaderView.cs │ │ ├── Issues/ │ │ │ ├── BaseIssuesView.cs │ │ │ ├── IssueAddView.cs │ │ │ ├── IssueAssignedToView.cs │ │ │ ├── IssueEditView.cs │ │ │ ├── IssueLabelsView.cs │ │ │ ├── IssueMilestonesView.cs │ │ │ ├── IssueView.cs │ │ │ ├── IssuesView.cs │ │ │ └── MyIssuesView.cs │ │ ├── LoadingIndicatorView.cs │ │ ├── MarkdownAccessoryView.cs │ │ ├── MenuSectionView.cs │ │ ├── MilestoneView.cs │ │ ├── NotificationsView.cs │ │ ├── ProfileButton.cs │ │ ├── ProgressBarView.cs │ │ ├── PullRequests/ │ │ │ ├── PullRequestCommitsView.cs │ │ │ ├── PullRequestFilesView.cs │ │ │ ├── PullRequestView.cs │ │ │ └── PullRequestsView.cs │ │ ├── RetryListView.cs │ │ ├── ScrollingToolbarView.cs │ │ ├── SlideUpTitleView.cs │ │ ├── Source/ │ │ │ ├── ChangesetView.cs │ │ │ ├── ChangesetsView.cs │ │ │ ├── CommitsView.cs │ │ │ └── EditSourceView.cs │ │ ├── SourceTitleView.cs │ │ ├── TrendingTitleButton.cs │ │ └── UILabelWithLinks.cs │ ├── WebResources/ │ │ ├── highlight-LICENSE │ │ ├── highlight-line-numbers-LICENSE │ │ ├── highlight.pack.js │ │ ├── marked.js │ │ └── styles/ │ │ ├── agate.css │ │ ├── androidstudio.css │ │ ├── arduino-light.css │ │ ├── arta.css │ │ ├── ascetic.css │ │ ├── atelier-cave-dark.css │ │ ├── atelier-cave-light.css │ │ ├── atelier-dune-dark.css │ │ ├── atelier-dune-light.css │ │ ├── atelier-estuary-dark.css │ │ ├── atelier-estuary-light.css │ │ ├── atelier-forest-dark.css │ │ ├── atelier-forest-light.css │ │ ├── atelier-heath-dark.css │ │ ├── atelier-heath-light.css │ │ ├── atelier-lakeside-dark.css │ │ ├── atelier-lakeside-light.css │ │ ├── atelier-plateau-dark.css │ │ ├── atelier-plateau-light.css │ │ ├── atelier-savanna-dark.css │ │ ├── atelier-savanna-light.css │ │ ├── atelier-seaside-dark.css │ │ ├── atelier-seaside-light.css │ │ ├── atelier-sulphurpool-dark.css │ │ ├── atelier-sulphurpool-light.css │ │ ├── atom-one-dark.css │ │ ├── atom-one-light.css │ │ ├── brown-paper.css │ │ ├── codepen-embed.css │ │ ├── color-brewer.css │ │ ├── darcula.css │ │ ├── dark.css │ │ ├── darkula.css │ │ ├── default.css │ │ ├── docco.css │ │ ├── dracula.css │ │ ├── far.css │ │ ├── foundation.css │ │ ├── github-gist.css │ │ ├── github.css │ │ ├── googlecode.css │ │ ├── grayscale.css │ │ ├── gruvbox-dark.css │ │ ├── gruvbox-light.css │ │ ├── hopscotch.css │ │ ├── hybrid.css │ │ ├── idea.css │ │ ├── ir-black.css │ │ ├── kimbie.dark.css │ │ ├── kimbie.light.css │ │ ├── magula.css │ │ ├── mono-blue.css │ │ ├── monokai-sublime.css │ │ ├── monokai.css │ │ ├── obsidian.css │ │ ├── ocean.css │ │ ├── paraiso-dark.css │ │ ├── paraiso-light.css │ │ ├── pojoaque.css │ │ ├── purebasic.css │ │ ├── qtcreator_dark.css │ │ ├── qtcreator_light.css │ │ ├── railscasts.css │ │ ├── rainbow.css │ │ ├── routeros.css │ │ ├── school-book.css │ │ ├── solarized-dark.css │ │ ├── solarized-light.css │ │ ├── sunburst.css │ │ ├── tomorrow-night-blue.css │ │ ├── tomorrow-night-bright.css │ │ ├── tomorrow-night-eighties.css │ │ ├── tomorrow-night.css │ │ ├── tomorrow.css │ │ ├── vs.css │ │ ├── vs2015.css │ │ ├── xcode.css │ │ ├── xt256.css │ │ └── zenburn.css │ ├── XCallback/ │ │ ├── XCallbackProvider.cs │ │ └── XCallbackQuery.cs │ ├── iTunesArtwork │ ├── iTunesArtwork@2x │ └── packages.config ├── CodeHub.iOS.sln ├── CodeHub.psd ├── PRIVATE-POLICY.md └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ */bin */obj *.suo *.userprefs packages .vs ================================================ FILE: CodeHub/CodeHub.csproj ================================================ Debug AnyCPU {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7} {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} true Library CodeHub CodeHub v4.5 Profile111 true full false bin\Debug DEBUG; prompt 4 true bin\Release prompt 4 UpgradeDetailsWebView.cshtml SyntaxHighlighterWebView.cshtml CommentsWebView.cshtml MarkdownWebView.cshtml DiffWebView.cshtml RazorTemplatePreprocessor UpgradeDetailsWebView.cs RazorTemplatePreprocessor SyntaxHighlighterWebView.cs RazorTemplatePreprocessor CommentsWebView.cs RazorTemplatePreprocessor MarkdownWebView.cs RazorTemplatePreprocessor DiffWebView.cs ..\packages\Humanizer.Core.2.2.0\lib\netstandard1.0\Humanizer.dll ================================================ FILE: CodeHub/Octicons.cs ================================================ namespace CodeHub { public class Octicon { public char CharacterCode { get; private set; } public Octicon(char characterCode) { CharacterCode = characterCode; } public static implicit operator Octicon(char d) { return new Octicon(d); } public static Octicon Heart = (char)9829; public static Octicon Zap = (char)9889; public static Octicon LightBulb = (char)61440; public static Octicon Repo = (char)61441; public static Octicon RepoForked = (char)61442; public static Octicon RepoPush = (char)61445; public static Octicon RepoPull = (char)61446; public static Octicon Book = (char)61447; public static Octicon Octoface = (char)61448; public static Octicon GitPullRequest = (char)61449; public static Octicon MarkGithub = (char)61450; public static Octicon CloudDownload = (char)61451; public static Octicon CloudUpload = (char)61452; public static Octicon Keyboard = (char)61453; public static Octicon Gist = (char)61454; public static Octicon FileCode = (char)61456; public static Octicon FileText = (char)61457; public static Octicon FileMedia = (char)61458; public static Octicon FileZip = (char)61459; public static Octicon FilePdf = (char)61460; public static Octicon Tag = (char)61461; public static Octicon FileDirectory = (char)61462; public static Octicon FileSubmodule = (char)61463; public static Octicon Person = (char)61464; public static Octicon Jersey = (char)61465; public static Octicon GitCommit = (char)61471; public static Octicon GitBranch = (char)61472; public static Octicon GitMerge = (char)61475; public static Octicon Mirror = (char)61476; public static Octicon IssueOpened = (char)61478; public static Octicon IssueReopened = (char)61479; public static Octicon IssueClosed = (char)61480; public static Octicon Star = (char)61482; public static Octicon Comment = (char)61483; public static Octicon Question = (char)61484; public static Octicon Alert = (char)61485; public static Octicon Search = (char)61486; public static Octicon Gear = (char)61487; public static Octicon RadioTower = (char)61488; public static Octicon Tools = (char)61489; public static Octicon SignOut = (char)61490; public static Octicon Rocket = (char)61491; public static Octicon Rss = (char)61492; public static Octicon Clippy = (char)61493; public static Octicon SignIn = (char)61494; public static Octicon Organization = (char)61495; public static Octicon DeviceMobile = (char)61496; public static Octicon Unfold = (char)61497; public static Octicon Check = (char)61498; public static Octicon Mail = (char)61499; public static Octicon MailRead = (char)61500; public static Octicon ArrowUp = (char)61501; public static Octicon ArrowRight = (char)61502; public static Octicon ArrowDown = (char)61503; public static Octicon ArrowLeft = (char)61504; public static Octicon Pin = (char)61505; public static Octicon Gift = (char)61506; public static Octicon Graph = (char)61507; public static Octicon TriangleLeft = (char)61508; public static Octicon CreditCard = (char)61509; public static Octicon Clock = (char)61510; public static Octicon Ruby = (char)61511; public static Octicon Broadcast = (char)61512; public static Octicon Key = (char)61513; public static Octicon RepoForcePush = (char)61514; public static Octicon RepoClone = (char)61516; public static Octicon Diff = (char)61517; public static Octicon Eye = (char)61518; public static Octicon CommentDiscussion = (char)61519; public static Octicon MailReply = (char)61521; public static Octicon PrimitiveDot = (char)61522; public static Octicon PrimitiveSquare = (char)61523; public static Octicon DeviceCamera = (char)61526; public static Octicon DeviceCameraVideo = (char)61527; public static Octicon Pencil = (char)61528; public static Octicon Info = (char)61529; public static Octicon TriangleRight = (char)61530; public static Octicon TriangleDown = (char)61531; public static Octicon Link = (char)61532; public static Octicon Plus = (char)61533; public static Octicon ThreeBars = (char)61534; public static Octicon Code = (char)61535; public static Octicon Location = (char)61536; public static Octicon ListUnordered = (char)61537; public static Octicon ListOrdered = (char)61538; public static Octicon Quote = (char)61539; public static Octicon Versions = (char)61540; public static Octicon ColorMode = (char)61541; public static Octicon ScreenFull = (char)61542; public static Octicon ScreenNormal = (char)61543; public static Octicon Calendar = (char)61544; public static Octicon Beer = (char)61545; public static Octicon Lock = (char)61546; public static Octicon DiffAdded = (char)61547; public static Octicon DiffRemoved = (char)61548; public static Octicon DiffModified = (char)61549; public static Octicon DiffRenamed = (char)61550; public static Octicon HorizontalRule = (char)61552; public static Octicon ArrowSmallRight = (char)61553; public static Octicon JumpDown = (char)61554; public static Octicon JumpUp = (char)61555; public static Octicon MoveLeft = (char)61556; public static Octicon Milestone = (char)61557; public static Octicon Checklist = (char)61558; public static Octicon Megaphone = (char)61559; public static Octicon ChevronRight = (char)61560; public static Octicon Bookmark = (char)61563; public static Octicon Settings = (char)61564; public static Octicon Dashboard = (char)61565; public static Octicon History = (char)61566; public static Octicon LinkExternal = (char)61567; public static Octicon Mute = (char)61568; public static Octicon X = (char)61569; public static Octicon CircleSlash = (char)61572; public static Octicon Pulse = (char)61573; public static Octicon Sync = (char)61575; public static Octicon Telescope = (char)61576; public static Octicon Microscope = (char)61577; public static Octicon AlignmentAlign = (char)61578; public static Octicon AlignmentUnalign = (char)61579; public static Octicon GistSecret = (char)61580; public static Octicon Home = (char)61581; public static Octicon AlignmentAlignedTo = (char)61582; public static Octicon Stop = (char)61583; public static Octicon Bug = (char)61585; public static Octicon LogoGithub = (char)61586; public static Octicon FileBinary = (char)61588; public static Octicon Database = (char)61590; public static Octicon Server = (char)61591; public static Octicon DiffIgnored = (char)61593; public static Octicon Ellipsis = (char)61594; public static Octicon NoNewline = (char)61596; public static Octicon Hubot = (char)61597; public static Octicon Hourglass = (char)61598; public static Octicon ArrowSmallUp = (char)61599; public static Octicon ArrowSmallDown = (char)61600; public static Octicon ArrowSmallLeft = (char)61601; public static Octicon ChevronUp = (char)61602; public static Octicon ChevronDown = (char)61603; public static Octicon ChevronLeft = (char)61604; public static Octicon JumpLeft = (char)61605; public static Octicon JumpRight = (char)61606; public static Octicon MoveUp = (char)61607; public static Octicon MoveDown = (char)61608; public static Octicon MoveRight = (char)61609; public static Octicon TriangleUp = (char)61610; public static Octicon GitCompare = (char)61612; public static Octicon Podium = (char)61615; public static Octicon FileSymlinkFile = (char)61616; public static Octicon FileSymlinkDirectory = (char)61617; public static Octicon Squirrel = (char)61618; public static Octicon Globe = (char)61622; public static Octicon Unmute = (char)61626; public static Octicon PlaybackPause = (char)61627; public static Octicon PlaybackRewind = (char)61628; public static Octicon PlaybackFastForward = (char)61629; public static Octicon Mention = (char)61630; public static Octicon PlaybackPlay = (char)61631; public static Octicon Puzzle = (char)61632; public static Octicon Package = (char)61636; public static Octicon Browser = (char)61637; public static Octicon Split = (char)61638; public static Octicon Steps = (char)61639; public static Octicon Terminal = (char)61640; public static Octicon Markdown = (char)61641; public static Octicon Dash = (char)61642; public static Octicon Fold = (char)61644; public static Octicon Inbox = (char)61647; public static Octicon Trashcan = (char)61648; public static Octicon Paintcan = (char)61649; public static Octicon Flame = (char)61650; public static Octicon Briefcase = (char)61651; public static Octicon Plug = (char)61652; public static Octicon CircuitBoard = (char)61654; public static Octicon MortarBoard = (char)61655; public static Octicon Law = (char)61656; public static Octicon DeviceDesktop = (char)62076; } } ================================================ FILE: CodeHub/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; // Information about this assembly is defined by the following attributes. // Change them to the values specific to your project. [assembly: AssemblyTitle("CodeHub")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("")] [assembly: AssemblyCopyright("(c) Dillon Buchanan")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. [assembly: AssemblyVersion("1.0.*")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. //[assembly: AssemblyDelaySign(false)] //[assembly: AssemblyKeyFile("")] ================================================ FILE: CodeHub/WebViews/CommentsModel.cs ================================================ using System.Collections.Generic; using System; using System.Linq; using Humanizer; namespace CodeHub.WebViews { public class Comment { public string AvatarUrl { get; } public string Name { get; } public DateTimeOffset Date { get; } public string DateString => Date.Humanize(); public string Body { get; } public Comment(string avatar, string name, string body, DateTimeOffset date) { AvatarUrl = avatar; Name = name; Body = body; Date = date; } } public class CommentsModel { public IList Comments { get; } public int FontSize { get; } public CommentsModel(IEnumerable comments, int fontSize) { Comments = (comments ?? Enumerable.Empty()).ToList(); FontSize = fontSize; } } } ================================================ FILE: CodeHub/WebViews/CommentsWebView.cs ================================================ #pragma warning disable 1591 // ------------------------------------------------------------------------------ // // This code was generated by a tool. // Mono Runtime Version: 4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // // ------------------------------------------------------------------------------ namespace CodeHub.WebViews { using System; using System.Collections.Generic; using System.Linq; using System.Text; [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorTemplatePreprocessor", "2.6.0.0")] public partial class CommentsWebView : CommentsWebViewBase { #line hidden #line 1 "CommentsWebView.cshtml" public CommentsModel Model { get; set; } #line default #line hidden public override void Execute() { WriteLiteral("\n\n\n\n\n\n \n"); #line 194 "CommentsWebView.cshtml" #line default #line hidden #line 194 "CommentsWebView.cshtml" foreach (var comment in Model.Comments) { #line default #line hidden WriteLiteral(" \n ("", comment.AvatarUrl #line default #line hidden , false) ); WriteLiteral(">\n
\n

"); #line 199 "CommentsWebView.cshtml" Write(comment.Name); #line default #line hidden WriteLiteral("

\n

"); #line 200 "CommentsWebView.cshtml" Write(comment.DateString); #line default #line hidden WriteLiteral("

\n "); #line 201 "CommentsWebView.cshtml" WriteLiteral(comment.Body); #line default #line hidden WriteLiteral("
\n \n \n"); #line 204 "CommentsWebView.cshtml" } #line default #line hidden WriteLiteral(" \n\nExecutes the template and returns the output as a string. /// The template output. public string GenerateString () { using (var sw = new System.IO.StringWriter ()) { Generate (sw); return sw.ToString (); } } // This method is OPTIONAL, you may choose to implement Write and WriteLiteral without use of __razor_writer // and provide another means of invoking Execute. // /// Executes the template, writing to the provided text writer. /// The TextWriter to which to write the template output. public void Generate (System.IO.TextWriter writer) { __razor_writer = writer; Execute (); __razor_writer = null; } // This method is REQUIRED, but you may choose to implement it differently // /// Writes a literal value to the template output without HTML escaping it. /// The literal value. protected void WriteLiteral (string value) { __razor_writer.Write (value); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// Writes a literal value to the TextWriter without HTML escaping it. /// The TextWriter to which to write the literal. /// The literal value. protected static void WriteLiteralTo (System.IO.TextWriter writer, string value) { writer.Write (value); } // This method is REQUIRED, but you may choose to implement it differently // /// Writes a value to the template output, HTML escaping it if necessary. /// The value. /// The value may be a Action, as returned by Razor helpers. protected void Write (object value) { WriteTo (__razor_writer, value); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// Writes an object value to the TextWriter, HTML escaping it if necessary. /// The TextWriter to which to write the value. /// The value. /// The value may be a Action, as returned by Razor helpers. protected static void WriteTo (System.IO.TextWriter writer, object value) { if (value == null) return; var write = value as Action; if (write != null) { write (writer); return; } //NOTE: a more sophisticated implementation would write safe and pre-escaped values directly to the //instead of double-escaping. See System.Web.IHtmlString in ASP.NET 4.0 for an example of this. writer.Write(System.Net.WebUtility.HtmlEncode (value.ToString ())); } // This method is REQUIRED, but you may choose to implement it differently // /// /// Conditionally writes an attribute to the template output. /// /// The name of the attribute. /// The prefix of the attribute. /// The suffix of the attribute. /// Attribute values, each specifying a prefix, value and whether it's a literal. protected void WriteAttribute (string name, string prefix, string suffix, params Tuple[] values) { WriteAttributeTo (__razor_writer, name, prefix, suffix, values); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// /// Conditionally writes an attribute to a TextWriter. /// /// The TextWriter to which to write the attribute. /// The name of the attribute. /// The prefix of the attribute. /// The suffix of the attribute. /// Attribute values, each specifying a prefix, value and whether it's a literal. ///Used by Razor helpers to write attributes. protected static void WriteAttributeTo (System.IO.TextWriter writer, string name, string prefix, string suffix, params Tuple[] values) { // this is based on System.Web.WebPages.WebPageExecutingBase // Copyright (c) Microsoft Open Technologies, Inc. // Licensed under the Apache License, Version 2.0 if (values.Length == 0) { // Explicitly empty attribute, so write the prefix and suffix writer.Write (prefix); writer.Write (suffix); return; } bool first = true; bool wroteSomething = false; for (int i = 0; i < values.Length; i++) { Tuple attrVal = values [i]; string attPrefix = attrVal.Item1; object value = attrVal.Item2; bool isLiteral = attrVal.Item3; if (value == null) { // Nothing to write continue; } // The special cases here are that the value we're writing might already be a string, or that the // value might be a bool. If the value is the bool 'true' we want to write the attribute name instead // of the string 'true'. If the value is the bool 'false' we don't want to write anything. // // Otherwise the value is another object (perhaps an IHtmlString), and we'll ask it to format itself. string stringValue; bool? boolValue = value as bool?; if (boolValue == true) { stringValue = name; } else if (boolValue == false) { continue; } else { stringValue = value as string; } if (first) { writer.Write (prefix); first = false; } else { writer.Write (attPrefix); } if (isLiteral) { writer.Write (stringValue ?? value); } else { WriteTo (writer, stringValue ?? value); } wroteSomething = true; } if (wroteSomething) { writer.Write (suffix); } } // This method is REQUIRED. The generated Razor subclass will override it with the generated code. // ///Executes the template, writing output to the Write and WriteLiteral methods.. ///Not intended to be called directly. Call the Generate method instead. public abstract void Execute (); } } #pragma warning restore 1591 ================================================ FILE: CodeHub/WebViews/CommentsWebView.cshtml ================================================ @model CommentsModel
@foreach (var comment in Model.Comments) {

@comment.Name

@comment.DateString

@{WriteLiteral(comment.Body);}
}
Chunks { get; } public List FileComments { get; } public int FontSize { get; } public DiffModel( IEnumerable patchLines, IEnumerable comments, int fontSize) { FontSize = fontSize; var diffComments = comments.GroupBy(x => x.LineFrom).ToDictionary(x => x.Key ?? -1, x => x.ToList()); FileComments = diffComments.ContainsKey(-1) ? diffComments[-1] : new List(); Chunks = ParsePatchLines(patchLines, diffComments).ToList(); } static IEnumerable ParsePatchLines(IEnumerable patchLines, Dictionary> comments) { int baseLine = 0; int newLine = 0; string contextLine = null; LinkedList lines = null; foreach (var patchLine in patchLines.Select((line, idx) => new { line, idx })) { var line = patchLine.line; var idx = patchLine.idx; if (line.StartsWith("@@", StringComparison.Ordinal)) { if (lines != null) yield return new Context(contextLine, lines); lines = new LinkedList(); var match = ContextRegex.Match(line); int.TryParse(match.Groups[1].Value, out baseLine); int.TryParse(match.Groups[2].Value, out newLine); contextLine = line; continue; } if (lines == null) continue; comments.TryGetValue(idx, out List lineComments); if (line.StartsWith("+", StringComparison.Ordinal)) { lines.AddLast(new Line(baseLine, newLine, LineEquality.Insert, line.Substring(1), idx, lineComments)); newLine++; } else if (line.StartsWith("-", StringComparison.Ordinal)) { lines.AddLast(new Line(baseLine, newLine, LineEquality.Delete, line.Substring(1), idx, lineComments)); baseLine++; } else if (line.StartsWith("\\", StringComparison.Ordinal)) { continue; } else { lines.AddLast(new Line(baseLine, newLine, LineEquality.Equal, line.Substring(1), idx, lineComments)); baseLine++; newLine++; } } if (lines != null) yield return new Context(contextLine, lines); } public class Context { public string Content { get; } public List Lines { get; } public Context(string content, IEnumerable lines) { Content = content; Lines = lines.ToList(); } } public class Line { public int? BaseLine { get; } public int? NewLine { get; } public LineEquality LineEquality { get; } public string Content { get; } public List>> CommentSets { get; } public int Index { get; } public Line(int? baseLine, int? newLine, LineEquality lineEquality, string content, int index, IEnumerable comments) { BaseLine = baseLine; NewLine = newLine; LineEquality = lineEquality; Content = content; Index = index; CommentSets = (comments ?? Enumerable.Empty()) .GroupBy(x => x.GroupId ?? index) .ToDictionary(x => x.Key, x => x.ToList()) .ToList(); } } public enum LineEquality { Equal, Insert, Delete } } } ================================================ FILE: CodeHub/WebViews/DiffWebView.cs ================================================ #pragma warning disable 1591 //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace CodeHub.WebViews { using System; using System.Collections.Generic; using System.Linq; using System.Text; [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorTemplatePreprocessor", "2.6.0.0")] public partial class DiffWebView : DiffWebViewBase { #line hidden #line 1 "DiffWebView.cshtml" public DiffModel Model { get; set; } #line default #line hidden public override void Execute() { WriteLiteral("\n\n \n \n \n \n\t\n \n \n \n \t\n"); #line 270 "DiffWebView.cshtml" #line default #line hidden #line 270 "DiffWebView.cshtml" foreach (var chunk in Model.Chunks) { #line default #line hidden WriteLiteral("\t \n ...\n ...\n "); #line 275 "DiffWebView.cshtml" Write(chunk.Content); #line default #line hidden WriteLiteral("\n \n"); #line 277 "DiffWebView.cshtml" foreach (var line in chunk.Lines) { if (line.LineEquality == DiffModel.LineEquality.Equal) { #line default #line hidden WriteLiteral(" ("", "lineClick(", true) #line 282 "DiffWebView.cshtml" , Tuple.Create ("", line.Index #line default #line hidden , false) , Tuple.Create ("", ",", true) #line 282 "DiffWebView.cshtml" , Tuple.Create (" ", line.NewLine #line default #line hidden , false) , Tuple.Create ("", ")", true) ); WriteLiteral(">\n "); #line 283 "DiffWebView.cshtml" Write(line.BaseLine); #line default #line hidden WriteLiteral("\n "); #line 284 "DiffWebView.cshtml" Write(line.NewLine); #line default #line hidden WriteLiteral("\n \n  \n "); #line 287 "DiffWebView.cshtml" Write(line.Content); #line default #line hidden WriteLiteral("\n \n \n"); #line 290 "DiffWebView.cshtml" } else if (line.LineEquality == DiffModel.LineEquality.Insert) { #line default #line hidden WriteLiteral(" ("", "lineClick(", true) #line 293 "DiffWebView.cshtml" , Tuple.Create ("", line.Index #line default #line hidden , false) , Tuple.Create ("", ",", true) #line 293 "DiffWebView.cshtml" , Tuple.Create (" ", line.NewLine #line default #line hidden , false) , Tuple.Create ("", ")", true) ); WriteLiteral(">\n \n "); #line 295 "DiffWebView.cshtml" Write(line.NewLine); #line default #line hidden WriteLiteral("\n \n +\n "); #line 298 "DiffWebView.cshtml" Write(line.Content); #line default #line hidden WriteLiteral("\n \n \n"); #line 301 "DiffWebView.cshtml" } else if (line.LineEquality == DiffModel.LineEquality.Delete) { #line default #line hidden WriteLiteral(" ("", "lineClick(", true) #line 304 "DiffWebView.cshtml" , Tuple.Create ("", line.Index #line default #line hidden , false) , Tuple.Create ("", ",", true) #line 304 "DiffWebView.cshtml" , Tuple.Create (" ", line.BaseLine #line default #line hidden , false) , Tuple.Create ("", ")", true) ); WriteLiteral(">\n "); #line 305 "DiffWebView.cshtml" Write(line.BaseLine); #line default #line hidden WriteLiteral("\n \n \n -\n "); #line 309 "DiffWebView.cshtml" Write(line.Content); #line default #line hidden WriteLiteral("\n \n \n"); #line 312 "DiffWebView.cshtml" } foreach (var commentSet in line.CommentSets) { #line default #line hidden WriteLiteral(" \t\t \n \n \n"); #line 319 "DiffWebView.cshtml" #line default #line hidden #line 319 "DiffWebView.cshtml" foreach (var comment in commentSet.Value) { #line default #line hidden WriteLiteral(" \n ("", comment.AvatarUrl #line default #line hidden , false) ); WriteLiteral(" class=\"avatar\""); WriteLiteral(" width=\"28\""); WriteLiteral(" height=\"28\""); WriteLiteral(" />\n \n

"); #line 324 "DiffWebView.cshtml" Write(comment.Username); #line default #line hidden WriteLiteral(" "); #line 324 "DiffWebView.cshtml" Write(comment.Date); #line default #line hidden WriteLiteral("

\n \n"); #line 326 "DiffWebView.cshtml" #line default #line hidden #line 326 "DiffWebView.cshtml" WriteLiteral(comment.Body); #line default #line hidden WriteLiteral("\n \n \n " + "\n"); #line 330 "DiffWebView.cshtml" } #line default #line hidden WriteLiteral(" \n ("", "replyTo(", true) #line 332 "DiffWebView.cshtml" , Tuple.Create ("", commentSet.Key #line default #line hidden , false) , Tuple.Create ("", ")", true) ); WriteLiteral(">Reply\n \n \n \n \n"); #line 337 "DiffWebView.cshtml" } } } #line default #line hidden WriteLiteral(" \n \n \n"); } } // NOTE: this is the default generated helper class. You may choose to extract it to a separate file // in order to customize it or share it between multiple templates, and specify the template's base // class via the @inherits directive. public abstract class DiffWebViewBase { // This field is OPTIONAL, but used by the default implementation of Generate, Write, WriteAttribute and WriteLiteral // System.IO.TextWriter __razor_writer; // This method is OPTIONAL // /// Executes the template and returns the output as a string. /// The template output. public string GenerateString () { using (var sw = new System.IO.StringWriter ()) { Generate (sw); return sw.ToString (); } } // This method is OPTIONAL, you may choose to implement Write and WriteLiteral without use of __razor_writer // and provide another means of invoking Execute. // /// Executes the template, writing to the provided text writer. /// The TextWriter to which to write the template output. public void Generate (System.IO.TextWriter writer) { __razor_writer = writer; Execute (); __razor_writer = null; } // This method is REQUIRED, but you may choose to implement it differently // /// Writes a literal value to the template output without HTML escaping it. /// The literal value. protected void WriteLiteral (string value) { __razor_writer.Write (value); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// Writes a literal value to the TextWriter without HTML escaping it. /// The TextWriter to which to write the literal. /// The literal value. protected static void WriteLiteralTo (System.IO.TextWriter writer, string value) { writer.Write (value); } // This method is REQUIRED, but you may choose to implement it differently // /// Writes a value to the template output, HTML escaping it if necessary. /// The value. /// The value may be a Action, as returned by Razor helpers. protected void Write (object value) { WriteTo (__razor_writer, value); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// Writes an object value to the TextWriter, HTML escaping it if necessary. /// The TextWriter to which to write the value. /// The value. /// The value may be a Action, as returned by Razor helpers. protected static void WriteTo (System.IO.TextWriter writer, object value) { if (value == null) return; var write = value as Action; if (write != null) { write (writer); return; } //NOTE: a more sophisticated implementation would write safe and pre-escaped values directly to the //instead of double-escaping. See System.Web.IHtmlString in ASP.NET 4.0 for an example of this. writer.Write(System.Net.WebUtility.HtmlEncode (value.ToString ())); } // This method is REQUIRED, but you may choose to implement it differently // /// /// Conditionally writes an attribute to the template output. /// /// The name of the attribute. /// The prefix of the attribute. /// The suffix of the attribute. /// Attribute values, each specifying a prefix, value and whether it's a literal. protected void WriteAttribute (string name, string prefix, string suffix, params Tuple[] values) { WriteAttributeTo (__razor_writer, name, prefix, suffix, values); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// /// Conditionally writes an attribute to a TextWriter. /// /// The TextWriter to which to write the attribute. /// The name of the attribute. /// The prefix of the attribute. /// The suffix of the attribute. /// Attribute values, each specifying a prefix, value and whether it's a literal. ///Used by Razor helpers to write attributes. protected static void WriteAttributeTo (System.IO.TextWriter writer, string name, string prefix, string suffix, params Tuple[] values) { // this is based on System.Web.WebPages.WebPageExecutingBase // Copyright (c) Microsoft Open Technologies, Inc. // Licensed under the Apache License, Version 2.0 if (values.Length == 0) { // Explicitly empty attribute, so write the prefix and suffix writer.Write (prefix); writer.Write (suffix); return; } bool first = true; bool wroteSomething = false; for (int i = 0; i < values.Length; i++) { Tuple attrVal = values [i]; string attPrefix = attrVal.Item1; object value = attrVal.Item2; bool isLiteral = attrVal.Item3; if (value == null) { // Nothing to write continue; } // The special cases here are that the value we're writing might already be a string, or that the // value might be a bool. If the value is the bool 'true' we want to write the attribute name instead // of the string 'true'. If the value is the bool 'false' we don't want to write anything. // // Otherwise the value is another object (perhaps an IHtmlString), and we'll ask it to format itself. string stringValue; bool? boolValue = value as bool?; if (boolValue == true) { stringValue = name; } else if (boolValue == false) { continue; } else { stringValue = value as string; } if (first) { writer.Write (prefix); first = false; } else { writer.Write (attPrefix); } if (isLiteral) { writer.Write (stringValue ?? value); } else { WriteTo (writer, stringValue ?? value); } wroteSomething = true; } if (wroteSomething) { writer.Write (suffix); } } // This method is REQUIRED. The generated Razor subclass will override it with the generated code. // ///Executes the template, writing output to the Write and WriteLiteral methods.. ///Not intended to be called directly. Call the Generate method instead. public abstract void Execute (); } } #pragma warning restore 1591 ================================================ FILE: CodeHub/WebViews/DiffWebView.cshtml ================================================ @model DiffModel @foreach (var chunk in Model.Chunks) { foreach (var line in chunk.Lines) { if (line.LineEquality == DiffModel.LineEquality.Equal) { } else if (line.LineEquality == DiffModel.LineEquality.Insert) { } else if (line.LineEquality == DiffModel.LineEquality.Delete) { } foreach (var commentSet in line.CommentSets) { } } }
... ... @chunk.Content
@line.BaseLine @line.NewLine   @line.Content
@line.NewLine + @line.Content
@line.BaseLine - @line.Content
@foreach (var comment in commentSet.Value) {

@comment.Username @comment.Date

@{WriteLiteral(comment.Body);}
}
================================================ FILE: CodeHub/WebViews/MarkdownModel.cs ================================================ namespace CodeHub.WebViews { public class MarkdownModel { public string Body { get; } public int FontSize { get; } public bool ContinuousResize { get; } public MarkdownModel(string body, int fontSize, bool continuousResize = false) { Body = body; FontSize = fontSize; ContinuousResize = continuousResize; } } } ================================================ FILE: CodeHub/WebViews/MarkdownWebView.cs ================================================ #pragma warning disable 1591 //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace CodeHub.WebViews { using System; using System.Collections.Generic; using System.Linq; using System.Text; [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorTemplatePreprocessor", "2.6.0.0")] public partial class MarkdownWebView : MarkdownWebViewBase { #line hidden #line 1 "MarkdownWebView.cshtml" public MarkdownModel Model { get; set; } #line default #line hidden public override void Execute() { WriteLiteral("\n\n "); #line 104 "MarkdownWebView.cshtml" if (Model.ContinuousResize) { #line default #line hidden WriteLiteral(@" "); #line 110 "MarkdownWebView.cshtml" } #line default #line hidden WriteLiteral("Readme\n"); #line 112 "MarkdownWebView.cshtml" WriteLiteral(Model.Body); #line default #line hidden WriteLiteral("\n"); } } // NOTE: this is the default generated helper class. You may choose to extract it to a separate file // in order to customize it or share it between multiple templates, and specify the template's base // class via the @inherits directive. public abstract class MarkdownWebViewBase { // This field is OPTIONAL, but used by the default implementation of Generate, Write, WriteAttribute and WriteLiteral // System.IO.TextWriter __razor_writer; // This method is OPTIONAL // /// Executes the template and returns the output as a string. /// The template output. public string GenerateString () { using (var sw = new System.IO.StringWriter ()) { Generate (sw); return sw.ToString (); } } // This method is OPTIONAL, you may choose to implement Write and WriteLiteral without use of __razor_writer // and provide another means of invoking Execute. // /// Executes the template, writing to the provided text writer. /// The TextWriter to which to write the template output. public void Generate (System.IO.TextWriter writer) { __razor_writer = writer; Execute (); __razor_writer = null; } // This method is REQUIRED, but you may choose to implement it differently // /// Writes a literal value to the template output without HTML escaping it. /// The literal value. protected void WriteLiteral (string value) { __razor_writer.Write (value); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// Writes a literal value to the TextWriter without HTML escaping it. /// The TextWriter to which to write the literal. /// The literal value. protected static void WriteLiteralTo (System.IO.TextWriter writer, string value) { writer.Write (value); } // This method is REQUIRED, but you may choose to implement it differently // /// Writes a value to the template output, HTML escaping it if necessary. /// The value. /// The value may be a Action, as returned by Razor helpers. protected void Write (object value) { WriteTo (__razor_writer, value); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// Writes an object value to the TextWriter, HTML escaping it if necessary. /// The TextWriter to which to write the value. /// The value. /// The value may be a Action, as returned by Razor helpers. protected static void WriteTo (System.IO.TextWriter writer, object value) { if (value == null) return; var write = value as Action; if (write != null) { write (writer); return; } //NOTE: a more sophisticated implementation would write safe and pre-escaped values directly to the //instead of double-escaping. See System.Web.IHtmlString in ASP.NET 4.0 for an example of this. writer.Write(System.Net.WebUtility.HtmlEncode (value.ToString ())); } // This method is REQUIRED, but you may choose to implement it differently // /// /// Conditionally writes an attribute to the template output. /// /// The name of the attribute. /// The prefix of the attribute. /// The suffix of the attribute. /// Attribute values, each specifying a prefix, value and whether it's a literal. protected void WriteAttribute (string name, string prefix, string suffix, params Tuple[] values) { WriteAttributeTo (__razor_writer, name, prefix, suffix, values); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// /// Conditionally writes an attribute to a TextWriter. /// /// The TextWriter to which to write the attribute. /// The name of the attribute. /// The prefix of the attribute. /// The suffix of the attribute. /// Attribute values, each specifying a prefix, value and whether it's a literal. ///Used by Razor helpers to write attributes. protected static void WriteAttributeTo (System.IO.TextWriter writer, string name, string prefix, string suffix, params Tuple[] values) { // this is based on System.Web.WebPages.WebPageExecutingBase // Copyright (c) Microsoft Open Technologies, Inc. // Licensed under the Apache License, Version 2.0 if (values.Length == 0) { // Explicitly empty attribute, so write the prefix and suffix writer.Write (prefix); writer.Write (suffix); return; } bool first = true; bool wroteSomething = false; for (int i = 0; i < values.Length; i++) { Tuple attrVal = values [i]; string attPrefix = attrVal.Item1; object value = attrVal.Item2; bool isLiteral = attrVal.Item3; if (value == null) { // Nothing to write continue; } // The special cases here are that the value we're writing might already be a string, or that the // value might be a bool. If the value is the bool 'true' we want to write the attribute name instead // of the string 'true'. If the value is the bool 'false' we don't want to write anything. // // Otherwise the value is another object (perhaps an IHtmlString), and we'll ask it to format itself. string stringValue; bool? boolValue = value as bool?; if (boolValue == true) { stringValue = name; } else if (boolValue == false) { continue; } else { stringValue = value as string; } if (first) { writer.Write (prefix); first = false; } else { writer.Write (attPrefix); } if (isLiteral) { writer.Write (stringValue ?? value); } else { WriteTo (writer, stringValue ?? value); } wroteSomething = true; } if (wroteSomething) { writer.Write (suffix); } } // This method is REQUIRED. The generated Razor subclass will override it with the generated code. // ///Executes the template, writing output to the Write and WriteLiteral methods.. ///Not intended to be called directly. Call the Generate method instead. public abstract void Execute (); } } #pragma warning restore 1591 ================================================ FILE: CodeHub/WebViews/MarkdownWebView.cshtml ================================================ @model MarkdownModel @if (Model.ContinuousResize) { } Readme @{WriteLiteral(Model.Body);} ================================================ FILE: CodeHub/WebViews/SyntaxHighlighterModel.cs ================================================ namespace CodeHub.WebViews { public class SyntaxHighlighterModel { public string Content { get; } public string Theme { get; } public string Language { get; } public int FontSize { get; } public string Viewport { get; } public SyntaxHighlighterModel(string content, string theme, int fontSize, bool shouldZoom, bool lockWidth = false, string file = null) { Content = content; Theme = theme; FontSize = fontSize; var scale = shouldZoom ? 1.0m : 0.4m; var width = lockWidth ? "width=device-width" : string.Empty; Viewport = $"minimum-scale={scale} maximum-scale=4.0 {width}"; if (file != null) Language = CalculateLanguage(System.IO.Path.GetExtension(file)); } private static string CalculateLanguage(string extension) { if (extension != null) { switch (extension.ToLower().Trim('.', ' ')) { case "rb": case "erb": return "ruby"; case "go": return "go"; case "cs": return "cs"; case "fs": return "fsharp"; case "py": return "python"; case "js": return "javascript"; case "css": return "css"; } } return string.Empty; } } } ================================================ FILE: CodeHub/WebViews/SyntaxHighlighterWebView.cs ================================================ #pragma warning disable 1591 //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace CodeHub.WebViews { using System; using System.Collections.Generic; using System.Linq; using System.Text; [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorTemplatePreprocessor", "2.6.0.0")] public partial class SyntaxHighlighterWebView : SyntaxHighlighterWebViewBase { #line hidden #line 1 "SyntaxHighlighterWebView.cshtml" public SyntaxHighlighterModel Model { get; set; } #line default #line hidden public override void Execute() { WriteLiteral("\n\n\n\n (" ", Model.Viewport #line default #line hidden , false) ); WriteLiteral(">\n ("", "WebResources/styles/", true) #line 7 "SyntaxHighlighterWebView.cshtml" , Tuple.Create ("", Model.Theme #line default #line hidden , false) , Tuple.Create ("", ".css", true) ); WriteLiteral(" />\n\n
 ("", Model.Language

#line default
#line hidden
, false)
);
WriteLiteral(">");


#line 84 "SyntaxHighlighterWebView.cshtml"
                                            Write(Model.Content);


#line default
#line hidden
WriteLiteral("
\n\n "); } } // NOTE: this is the default generated helper class. You may choose to extract it to a separate file // in order to customize it or share it between multiple templates, and specify the template's base // class via the @inherits directive. public abstract class SyntaxHighlighterWebViewBase { // This field is OPTIONAL, but used by the default implementation of Generate, Write, WriteAttribute and WriteLiteral // System.IO.TextWriter __razor_writer; // This method is OPTIONAL // /// Executes the template and returns the output as a string. /// The template output. public string GenerateString () { using (var sw = new System.IO.StringWriter ()) { Generate (sw); return sw.ToString (); } } // This method is OPTIONAL, you may choose to implement Write and WriteLiteral without use of __razor_writer // and provide another means of invoking Execute. // /// Executes the template, writing to the provided text writer. /// The TextWriter to which to write the template output. public void Generate (System.IO.TextWriter writer) { __razor_writer = writer; Execute (); __razor_writer = null; } // This method is REQUIRED, but you may choose to implement it differently // /// Writes a literal value to the template output without HTML escaping it. /// The literal value. protected void WriteLiteral (string value) { __razor_writer.Write (value); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// Writes a literal value to the TextWriter without HTML escaping it. /// The TextWriter to which to write the literal. /// The literal value. protected static void WriteLiteralTo (System.IO.TextWriter writer, string value) { writer.Write (value); } // This method is REQUIRED, but you may choose to implement it differently // /// Writes a value to the template output, HTML escaping it if necessary. /// The value. /// The value may be a Action, as returned by Razor helpers. protected void Write (object value) { WriteTo (__razor_writer, value); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// Writes an object value to the TextWriter, HTML escaping it if necessary. /// The TextWriter to which to write the value. /// The value. /// The value may be a Action, as returned by Razor helpers. protected static void WriteTo (System.IO.TextWriter writer, object value) { if (value == null) return; var write = value as Action; if (write != null) { write (writer); return; } //NOTE: a more sophisticated implementation would write safe and pre-escaped values directly to the //instead of double-escaping. See System.Web.IHtmlString in ASP.NET 4.0 for an example of this. writer.Write(System.Net.WebUtility.HtmlEncode (value.ToString ())); } // This method is REQUIRED, but you may choose to implement it differently // /// /// Conditionally writes an attribute to the template output. /// /// The name of the attribute. /// The prefix of the attribute. /// The suffix of the attribute. /// Attribute values, each specifying a prefix, value and whether it's a literal. protected void WriteAttribute (string name, string prefix, string suffix, params Tuple[] values) { WriteAttributeTo (__razor_writer, name, prefix, suffix, values); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// /// Conditionally writes an attribute to a TextWriter. /// /// The TextWriter to which to write the attribute. /// The name of the attribute. /// The prefix of the attribute. /// The suffix of the attribute. /// Attribute values, each specifying a prefix, value and whether it's a literal. ///Used by Razor helpers to write attributes. protected static void WriteAttributeTo (System.IO.TextWriter writer, string name, string prefix, string suffix, params Tuple[] values) { // this is based on System.Web.WebPages.WebPageExecutingBase // Copyright (c) Microsoft Open Technologies, Inc. // Licensed under the Apache License, Version 2.0 if (values.Length == 0) { // Explicitly empty attribute, so write the prefix and suffix writer.Write (prefix); writer.Write (suffix); return; } bool first = true; bool wroteSomething = false; for (int i = 0; i < values.Length; i++) { Tuple attrVal = values [i]; string attPrefix = attrVal.Item1; object value = attrVal.Item2; bool isLiteral = attrVal.Item3; if (value == null) { // Nothing to write continue; } // The special cases here are that the value we're writing might already be a string, or that the // value might be a bool. If the value is the bool 'true' we want to write the attribute name instead // of the string 'true'. If the value is the bool 'false' we don't want to write anything. // // Otherwise the value is another object (perhaps an IHtmlString), and we'll ask it to format itself. string stringValue; bool? boolValue = value as bool?; if (boolValue == true) { stringValue = name; } else if (boolValue == false) { continue; } else { stringValue = value as string; } if (first) { writer.Write (prefix); first = false; } else { writer.Write (attPrefix); } if (isLiteral) { writer.Write (stringValue ?? value); } else { WriteTo (writer, stringValue ?? value); } wroteSomething = true; } if (wroteSomething) { writer.Write (suffix); } } // This method is REQUIRED. The generated Razor subclass will override it with the generated code. // ///Executes the template, writing output to the Write and WriteLiteral methods.. ///Not intended to be called directly. Call the Generate method instead. public abstract void Execute (); } } #pragma warning restore 1591 ================================================ FILE: CodeHub/WebViews/SyntaxHighlighterWebView.cshtml ================================================ @model SyntaxHighlighterModel
@Model.Content
================================================ FILE: CodeHub/WebViews/UpgradeDetailsModel.cs ================================================ namespace CodeHub.WebViews { public class UpgradeDetailsModel { public string Price { get; } public bool IsPurchased { get; } public UpgradeDetailsModel(string price, bool isPurchased) { Price = price; IsPurchased = isPurchased; } } } ================================================ FILE: CodeHub/WebViews/UpgradeDetailsWebView.cs ================================================ #pragma warning disable 1591 //------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ namespace CodeHub.WebViews { using System; using System.Collections.Generic; using System.Linq; using System.Text; [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorTemplatePreprocessor", "2.6.0.0")] public partial class UpgradeDetailsWebView : UpgradeDetailsWebViewBase { #line hidden #line 1 "UpgradeDetailsWebView.cshtml" public UpgradeDetailsModel Model { get; set; } #line default #line hidden public override void Execute() { WriteLiteral("\n\n Pro Version

CodeHub Pro gives you access the all the great features below:

  • Private Repositories
  • \n
  • Enterprise Support
  • \n
  • Push Notifications
  • \n
\n\n

\n Before you buy please tak" + "e a look at the detailed description for each feature below. \n If you\'re " + "unsure about any aspect of the Pro version, please feel free to \n contact me!\n

\n\n"); #line 163 "UpgradeDetailsWebView.cshtml" #line default #line hidden #line 163 "UpgradeDetailsWebView.cshtml" if (Model.IsPurchased) { #line default #line hidden WriteLiteral("

\n Pro Already Enabled!\n

\n"); #line 168 "UpgradeDetailsWebView.cshtml" } else { if (Model.Price != null) { #line default #line hidden WriteLiteral("

\n Purchase CodeHub Pro for "); #line 174 "UpgradeDetailsWebView.cshtml" Write(Model.Price); #line default #line hidden WriteLiteral("\n

\n"); WriteLiteral(" \n Click here to restore a previous purchase.\n

\n"); #line 179 "UpgradeDetailsWebView.cshtml" } } #line default #line hidden WriteLiteral("\n

Feature Details

\n

Below are the features that are available wh" + "en purchasing CodeHub Pro.

\n\n Private Repositories

While CodeHub is free for all public projects, private repositories are only available with CodeHub Pro. The Pro edition allows access to all your private repositories and any private repositories any of your organizations have. Access to your private repositories is completely unrestricted. With CodeHub Pro, anything you are able to do with your open source repositories you are also capable of doing with your private repositories. Even more, access to private repositories, with CodeHub Pro, is applied to all your CodeHub accounts.

Enterprise Support

With CodeHub Pro, access to GitHub Enterprise instances becomes available for use. Enterprise support allows for unlimited accounts, in CodeHub, accessing unlimited Enterprise instances - there are no restrictions! You can authenticate with your GitHub Enterprise instance in two ways: Basic Auth, or OAuth token. Unlike authentication with GitHub.com, there is no avilable web application flow, which means that you will be prompted for your credentials in a view that is native to CodeHub, not GitHub. This method, also known as Basic Auth, is safe. The username and password pair is NOT saved on the device nor is it ever sent anywhere but GitHub. Even more, the username and password is exchanged for an OAuth token on GitHub so you may revoke access at any time.

If you feel unsafe authenticating with a username and password pair, you may choose to generate your own OAuth token on your enterprise instance and use it in CodeHub directly. This completely removes the need to type in your username and password within CodeHub.

Push Notifications\n

Note: Push Notifications are only available fo" + "r GitHub.com accounts. Details below

\n\n

Push notifications allow you to take your typical GitHub notifications and push them directly to your mobile device. Users receive notifications for conversations in repositories they watch including:

  • Issues and their comments
  • Pull Requests and their comments
  • Comments on any commits

Notifications are also sent for conversations in unwatched repositories when the user is involved including:

  • Mentions
  • Issue assignments
  • Commits the user authors or commits
  • Any discussion in which the user actively participates

Notifications are generated via a service owned and operated by Dillon Buchanan, creator of CodeHub. These notifications are harvested from GitHub.com using a OAuth identification token generated just for querying notifications on behalf of the user. This service, aptly called CodeHub-Push, is also open source and available for viewing on GitHub.

As mentioned above, push notifications are only available for GitHub.com accounts. Push notifications for GitHub enterprise accounts are not available. This limitation is due to the fact that the notification service, CodeHub-Push, cannot retrieve information from a enterprise instance. External access to a enterprise instance is almost always impossible due to network limitations as well as permission. For this reason, push notifications are unavailable for enterprise users - sorry.

"); } } // NOTE: this is the default generated helper class. You may choose to extract it to a separate file // in order to customize it or share it between multiple templates, and specify the template's base // class via the @inherits directive. public abstract class UpgradeDetailsWebViewBase { // This field is OPTIONAL, but used by the default implementation of Generate, Write, WriteAttribute and WriteLiteral // System.IO.TextWriter __razor_writer; // This method is OPTIONAL // /// Executes the template and returns the output as a string. /// The template output. public string GenerateString () { using (var sw = new System.IO.StringWriter ()) { Generate (sw); return sw.ToString (); } } // This method is OPTIONAL, you may choose to implement Write and WriteLiteral without use of __razor_writer // and provide another means of invoking Execute. // /// Executes the template, writing to the provided text writer. /// The TextWriter to which to write the template output. public void Generate (System.IO.TextWriter writer) { __razor_writer = writer; Execute (); __razor_writer = null; } // This method is REQUIRED, but you may choose to implement it differently // /// Writes a literal value to the template output without HTML escaping it. /// The literal value. protected void WriteLiteral (string value) { __razor_writer.Write (value); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// Writes a literal value to the TextWriter without HTML escaping it. /// The TextWriter to which to write the literal. /// The literal value. protected static void WriteLiteralTo (System.IO.TextWriter writer, string value) { writer.Write (value); } // This method is REQUIRED, but you may choose to implement it differently // /// Writes a value to the template output, HTML escaping it if necessary. /// The value. /// The value may be a Action, as returned by Razor helpers. protected void Write (object value) { WriteTo (__razor_writer, value); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// Writes an object value to the TextWriter, HTML escaping it if necessary. /// The TextWriter to which to write the value. /// The value. /// The value may be a Action, as returned by Razor helpers. protected static void WriteTo (System.IO.TextWriter writer, object value) { if (value == null) return; var write = value as Action; if (write != null) { write (writer); return; } //NOTE: a more sophisticated implementation would write safe and pre-escaped values directly to the //instead of double-escaping. See System.Web.IHtmlString in ASP.NET 4.0 for an example of this. writer.Write(System.Net.WebUtility.HtmlEncode (value.ToString ())); } // This method is REQUIRED, but you may choose to implement it differently // /// /// Conditionally writes an attribute to the template output. /// /// The name of the attribute. /// The prefix of the attribute. /// The suffix of the attribute. /// Attribute values, each specifying a prefix, value and whether it's a literal. protected void WriteAttribute (string name, string prefix, string suffix, params Tuple[] values) { WriteAttributeTo (__razor_writer, name, prefix, suffix, values); } // This method is REQUIRED if the template contains any Razor helpers, but you may choose to implement it differently // /// /// Conditionally writes an attribute to a TextWriter. /// /// The TextWriter to which to write the attribute. /// The name of the attribute. /// The prefix of the attribute. /// The suffix of the attribute. /// Attribute values, each specifying a prefix, value and whether it's a literal. ///Used by Razor helpers to write attributes. protected static void WriteAttributeTo (System.IO.TextWriter writer, string name, string prefix, string suffix, params Tuple[] values) { // this is based on System.Web.WebPages.WebPageExecutingBase // Copyright (c) Microsoft Open Technologies, Inc. // Licensed under the Apache License, Version 2.0 if (values.Length == 0) { // Explicitly empty attribute, so write the prefix and suffix writer.Write (prefix); writer.Write (suffix); return; } bool first = true; bool wroteSomething = false; for (int i = 0; i < values.Length; i++) { Tuple attrVal = values [i]; string attPrefix = attrVal.Item1; object value = attrVal.Item2; bool isLiteral = attrVal.Item3; if (value == null) { // Nothing to write continue; } // The special cases here are that the value we're writing might already be a string, or that the // value might be a bool. If the value is the bool 'true' we want to write the attribute name instead // of the string 'true'. If the value is the bool 'false' we don't want to write anything. // // Otherwise the value is another object (perhaps an IHtmlString), and we'll ask it to format itself. string stringValue; bool? boolValue = value as bool?; if (boolValue == true) { stringValue = name; } else if (boolValue == false) { continue; } else { stringValue = value as string; } if (first) { writer.Write (prefix); first = false; } else { writer.Write (attPrefix); } if (isLiteral) { writer.Write (stringValue ?? value); } else { WriteTo (writer, stringValue ?? value); } wroteSomething = true; } if (wroteSomething) { writer.Write (suffix); } } // This method is REQUIRED. The generated Razor subclass will override it with the generated code. // ///Executes the template, writing output to the Write and WriteLiteral methods.. ///Not intended to be called directly. Call the Generate method instead. public abstract void Execute (); } } #pragma warning restore 1591 ================================================ FILE: CodeHub/WebViews/UpgradeDetailsWebView.cshtml ================================================ @model UpgradeDetailsModel Pro Version

CodeHub Pro gives you access the all the great features below:

Before you buy please take a look at the detailed description for each feature below. If you're unsure about any aspect of the Pro version, please feel free to contact me!

@if (Model.IsPurchased) {

Pro Already Enabled!

} else { if (Model.Price != null) {

Purchase CodeHub Pro for @Model.Price

Click here to restore a previous purchase.

} }

Feature Details

Below are the features that are available when purchasing CodeHub Pro.

Private Repositories

While CodeHub is free for all public projects, private repositories are only available with CodeHub Pro. The Pro edition allows access to all your private repositories and any private repositories any of your organizations have. Access to your private repositories is completely unrestricted. With CodeHub Pro, anything you are able to do with your open source repositories you are also capable of doing with your private repositories. Even more, access to private repositories, with CodeHub Pro, is applied to all your CodeHub accounts.

Enterprise Support

With CodeHub Pro, access to GitHub Enterprise instances becomes available for use. Enterprise support allows for unlimited accounts, in CodeHub, accessing unlimited Enterprise instances - there are no restrictions! You can authenticate with your GitHub Enterprise instance in two ways: Basic Auth, or OAuth token. Unlike authentication with GitHub.com, there is no avilable web application flow, which means that you will be prompted for your credentials in a view that is native to CodeHub, not GitHub. This method, also known as Basic Auth, is safe. The username and password pair is NOT saved on the device nor is it ever sent anywhere but GitHub. Even more, the username and password is exchanged for an OAuth token on GitHub so you may revoke access at any time.

If you feel unsafe authenticating with a username and password pair, you may choose to generate your own OAuth token on your enterprise instance and use it in CodeHub directly. This completely removes the need to type in your username and password within CodeHub.

Push Notifications

Note: Push Notifications are only available for GitHub.com accounts. Details below

Push notifications allow you to take your typical GitHub notifications and push them directly to your mobile device. Users receive notifications for conversations in repositories they watch including:

  • Issues and their comments
  • Pull Requests and their comments
  • Comments on any commits

Notifications are also sent for conversations in unwatched repositories when the user is involved including:

  • Mentions
  • Issue assignments
  • Commits the user authors or commits
  • Any discussion in which the user actively participates

Notifications are generated via a service owned and operated by Dillon Buchanan, creator of CodeHub. These notifications are harvested from GitHub.com using a OAuth identification token generated just for querying notifications on behalf of the user. This service, aptly called CodeHub-Push, is also open source and available for viewing on GitHub.

As mentioned above, push notifications are only available for GitHub.com accounts. Push notifications for GitHub enterprise accounts are not available. This limitation is due to the fact that the notification service, CodeHub-Push, cannot retrieve information from a enterprise instance. External access to a enterprise instance is almost always impossible due to network limitations as well as permission. For this reason, push notifications are unavailable for enterprise users - sorry.

================================================ FILE: CodeHub/packages.config ================================================  ================================================ FILE: CodeHub.Core/App.cs ================================================ using System.Net; using MvvmCross.Core.ViewModels; namespace CodeHub.Core { /// /// Define the App type. /// public class App : MvxApplication { /// /// Initializes this instance. /// public override void Initialize() { ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; } } } ================================================ FILE: CodeHub.Core/CodeHub.Core.csproj ================================================  Debug AnyCPU {B7970173-9022-466B-B57A-7AB1E1F3145F} {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Library CodeHub.Core Resources CodeHub.Core Xamarin.iOS v1.0 true full false bin\iPhone\Debug DEBUG prompt 4 false true iPhone Developer none true bin\iPhone\Release prompt 4 false iPhone Developer ..\packages\Rx-Interfaces.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Interfaces.dll ..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll ..\packages\Rx-Linq.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Linq.dll ..\packages\Rx-PlatformServices.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll ..\packages\Splat.1.6.2\lib\Xamarin.iOS10\Splat.dll ..\packages\GitHubClient.1.0.15\lib\portable45-net45+win8+wpa81\GitHubSharp.dll ..\packages\PCLStorage.1.0.2\lib\portable-Xamarin.iOS+Xamarin.Mac\PCLStorage.dll ..\packages\PCLStorage.1.0.2\lib\portable-Xamarin.iOS+Xamarin.Mac\PCLStorage.Abstractions.dll ..\packages\akavache.core.5.0.0\lib\Xamarin.iOS10\Akavache.dll ..\packages\akavache.sqlite3.5.0.0\lib\Portable-Net45+Win8+WP8+Wpa81\Akavache.Sqlite3.dll ..\packages\MvvmCross.Platform.4.4.0\lib\Xamarin.iOS10\MvvmCross.Platform.dll ..\packages\MvvmCross.Platform.4.4.0\lib\Xamarin.iOS10\MvvmCross.Platform.iOS.dll ..\packages\MvvmCross.Core.4.4.0\lib\Xamarin.iOS10\MvvmCross.Core.dll ..\packages\MvvmCross.Core.4.4.0\lib\Xamarin.iOS10\MvvmCross.iOS.dll ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Binding.dll ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Binding.iOS.dll ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Localization.dll ..\packages\Microsoft.Net.Http.2.2.29\lib\Xamarin.iOS10\System.Net.Http.Extensions.dll ..\packages\Microsoft.Net.Http.2.2.29\lib\Xamarin.iOS10\System.Net.Http.Primitives.dll ..\packages\Humanizer.Core.2.2.0\lib\netstandard1.0\Humanizer.dll ..\packages\reactiveui-core.7.4.0\lib\Xamarin.iOS10\ReactiveUI.dll ..\packages\Newtonsoft.Json.10.0.3\lib\netstandard1.3\Newtonsoft.Json.dll ..\packages\SQLitePCLRaw.core.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.core.dll ..\packages\SQLitePCLRaw.lib.e_sqlite3.ios_unified.static.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.lib.e_sqlite3.dll ..\packages\SQLitePCLRaw.provider.internal.ios_unified.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.provider.internal.dll ..\packages\SQLitePCLRaw.bundle_e_sqlite3.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.batteries_e_sqlite3.dll ..\packages\SQLitePCLRaw.bundle_e_sqlite3.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.batteries_v2.dll ..\packages\Xam.Plugins.Settings.3.1.1\lib\Xamarin.iOS10\Plugin.Settings.Abstractions.dll ..\packages\Xam.Plugins.Settings.3.1.1\lib\Xamarin.iOS10\Plugin.Settings.dll ..\packages\Octokit.0.29.0\lib\netstandard1.1\Octokit.dll ..\packages\Plugin.Permissions.2.2.1\lib\Xamarin.iOS10\Plugin.Permissions.Abstractions.dll ..\packages\Plugin.Permissions.2.2.1\lib\Xamarin.iOS10\Plugin.Permissions.dll ..\packages\Xam.Plugin.Media.3.1.3\lib\Xamarin.iOS10\Plugin.Media.Abstractions.dll ..\packages\Xam.Plugin.Media.3.1.3\lib\Xamarin.iOS10\Plugin.Media.dll ================================================ FILE: CodeHub.Core/Data/Account.cs ================================================ using System.Collections.Generic; using Newtonsoft.Json; namespace CodeHub.Core.Data { public class Account { public string Id => Username + Domain; public string OAuth { get; set; } public string Password { get; set; } public string Domain { get; set; } public string WebDomain { get; set; } public bool IsEnterprise { get; set; } public bool ShowOrganizationsInEvents { get; set; } = true; public bool ExpandOrganizations { get; set; } = true; public bool ShowRepositoryDescriptionInList { get; set; } = true; public bool? IsPushNotificationsEnabled { get; set; } public string Username { get; set; } public string AvatarUrl { get; set; } public string DefaultStartupView { get; set; } public string CodeEditTheme { get; set; } = "idea"; private List _pinnedRepositories = new List(); public List PinnedRepositories { get { return _pinnedRepositories ?? new List(); } set { _pinnedRepositories = value ?? new List(); } } private Dictionary _filters = new Dictionary(); public Dictionary Filters { get { return _filters ?? new Dictionary(); } set { _filters = value ?? new Dictionary(); } } } public static class AccountExtensions { public static T GetFilter(this Account account, string key) where T : class, new() { Filter filter = null; if (account.Filters?.TryGetValue(key, out filter) == false) return default(T); return filter?.GetData() ?? new T(); } public static void SetFilter(this Account account, string key, object filter) { var f = new Filter(); f.SetData(filter); if (account.Filters == null) account.Filters = new Dictionary(); account.Filters[key] = f; } } public class PinnedRepository { public string Owner { get; set; } public string Slug { get; set; } public string Name { get; set; } public string ImageUri { get; set; } } public class Filter { public string RawData { get; set; } } public static class FilterExtensions { public static T GetData(this Filter filter) where T : new() { try { return JsonConvert.DeserializeObject(filter.RawData); } catch { return default(T); } } public static void SetData(this Filter filter, object o) { filter.RawData = JsonConvert.SerializeObject(o); } } } ================================================ FILE: CodeHub.Core/Data/ImgurResponse.cs ================================================ namespace CodeHub.Core.Data { public class ImgurResponse { public ImgurDataModel Data { get; set; } public bool Success { get; set; } public class ImgurDataModel { public string Link { get; set; } } } } ================================================ FILE: CodeHub.Core/Data/Language.cs ================================================ using System.Diagnostics; namespace CodeHub.Core.Data { [DebuggerDisplay("{Name}")] public class Language { public Language() { } public Language(string name, string slug) { Name = name; Slug = slug; } public string Name { get; set; } public string Slug { get; set; } public override bool Equals(object obj) { if (obj == null) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != typeof(Language)) return false; var other = (Language)obj; return Name == other.Name && Slug == other.Slug; } public override int GetHashCode() { unchecked { return (Name != null ? Name.GetHashCode() : 0) ^ (Slug != null ? Slug.GetHashCode() : 0); } } } } ================================================ FILE: CodeHub.Core/Data/LanguageRepository.cs ================================================ using System.Threading.Tasks; using System.Collections.Generic; using System.Net.Http; using GitHubSharp; namespace CodeHub.Core.Data { public class LanguageRepository { public static Language DefaultLanguage = new Language("All Languages", null); private const string LanguagesUrl = "http://trending.codehub-app.com/v2/languages"; public async Task> GetLanguages() { var client = new HttpClient(); var serializer = new SimpleJsonSerializer(); var msg = await client.GetAsync(LanguagesUrl).ConfigureAwait(false); var content = await msg.Content.ReadAsStringAsync().ConfigureAwait(false); return serializer.Deserialize>(content); } } } ================================================ FILE: CodeHub.Core/Data/TrendingRepository.cs ================================================ using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Net.Http; namespace CodeHub.Core.Data { public interface ITrendingRepository { Task> GetTrendingRepositories(string since, string language = null); } public class TrendingRepository : ITrendingRepository { private const string TrendingUrl = "http://trending.codehub-app.com/v2/trending"; public async Task> GetTrendingRepositories(string since, string language = null) { var query = "?since=" + Uri.EscapeDataString(since); if (!string.IsNullOrEmpty(language)) query += string.Format("&language={0}", Uri.EscapeDataString(language)); var client = new HttpClient(); var serializer = new Octokit.Internal.SimpleJsonSerializer(); var msg = await client.GetAsync(TrendingUrl + query).ConfigureAwait(false); var content = await msg.Content.ReadAsStringAsync().ConfigureAwait(false); return serializer.Deserialize>(content); } } } ================================================ FILE: CodeHub.Core/Extensions/CommandExtensions.cs ================================================ using System; using System.Reactive.Linq; // ReSharper disable once CheckNamespace namespace ReactiveUI { public static class ReactiveCommandExtensions { public static IDisposable ExecuteNow( this ReactiveCommand cmd, TParam param = default(TParam)) => cmd.CanExecute.Take(1).Where(x => x).Select(_ => param).InvokeReactiveCommand(cmd); public static IDisposable InvokeReactiveCommand( this IObservable obs, ReactiveCommand cmd) => obs.InvokeCommand(cmd); public static IDisposable InvokeReactiveCommand( this IObservable obs, CombinedReactiveCommand cmd) => obs.InvokeCommand(cmd); } } ================================================ FILE: CodeHub.Core/Extensions/ExceptionExtensions.cs ================================================ namespace System { public static class ExceptionExtensions { public static Exception GetInnerException(this Exception This) { var ex = This; while (ex.InnerException != null) ex = ex.InnerException; return ex; } } } ================================================ FILE: CodeHub.Core/Extensions/GitHubClientExtensions.cs ================================================ using System; using System.Collections.Generic; using CodeHub.Core.Utils; namespace Octokit { public static class GitHubClientExtensions { public static GitHubList RetrieveList( this GitHubClient client, Uri uri, IDictionary parameters = null) { return new GitHubList(client, uri, parameters); } } } ================================================ FILE: CodeHub.Core/Extensions/ObservableExtensions.cs ================================================ // Analysis disable once CheckNamespace namespace System { using System; using System.Windows.Input; using System.Reactive.Disposables; using System.Reactive.Linq; public static class ObservableExtensions { public static IDisposable BindCommand(this IObservable @this, ICommand command) { return command == null ? Disposable.Empty : @this.Where(x => command.CanExecute(x)).Subscribe(x => command.Execute(x)); } public static IDisposable SubscribeError(this IObservable @this, Action onError) { return @this.Subscribe(_ => { }, onError); } } } ================================================ FILE: CodeHub.Core/Extensions/ReactiveListExtensions.cs ================================================ using System.Collections.Generic; // ReSharper disable once CheckNamespace namespace ReactiveUI { public static class ReactiveListExtensions { public static void Reset(this IReactiveList @this, IEnumerable items) { using (@this.SuppressChangeNotifications()) { @this.Clear(); @this.AddRange(items); } } } } ================================================ FILE: CodeHub.Core/Extensions/StringExtensions.cs ================================================ // Analysis disable once CheckNamespace namespace System { public static class StringExtensions { public static bool ContainsKeyword(this string @this, string keyword) { if (string.IsNullOrEmpty(keyword)) return true; if (string.IsNullOrEmpty(@this)) return false; return @this.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0; } } } ================================================ FILE: CodeHub.Core/Extensions/TaskExtensions.cs ================================================ using System; using System.Threading.Tasks; using System.Reactive.Threading.Tasks; using System.Reactive.Linq; using ReactiveUI; // Analysis disable once CheckNamespace public static class TaskExtensions { public static Task WithTimeout(this Task task, TimeSpan timeout) { return task.ToObservable().Timeout(timeout).ToTask(); } public static Task WithTimeout(this Task task, TimeSpan timeout) { return task.ToObservable().Timeout(timeout).ToTask(); } public static IDisposable ToBackground(this Task task, Action action) { return task.ToObservable() .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(action, HandleError); } public static IDisposable ToBackground(this Task task) { return task.ToObservable() .Subscribe(a => {}, HandleError); } public static IDisposable ToBackground(this Task task) { return task.ToObservable() .Subscribe(a => {}, HandleError); } public static IDisposable ToBackground(this Task task, Action action) { return task.ToObservable() .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => action(), HandleError); } private static void HandleError(Exception e) { System.Diagnostics.Debug.WriteLine("Unable to process background task: " + e.Message); } } ================================================ FILE: CodeHub.Core/Filters/BaseIssuesFilterModel.cs ================================================ using CodeHub.Core.ViewModels; namespace CodeHub.Core.Filters { public abstract class BaseIssuesFilterModel : FilterModel { public bool Ascending { get; set; } public Sort SortType { get; set; } protected BaseIssuesFilterModel() { SortType = Sort.None; Ascending = false; } public enum Sort { None, Created, Updated, Comments } } } ================================================ FILE: CodeHub.Core/Filters/IssuesFilterModel.cs ================================================ using System; namespace CodeHub.Core.Filters { public class IssuesFilterModel : BaseIssuesFilterModel { public string Labels { get; set; } public bool Open { get; set; } public DateTime? Since { get; set; } public string Mentioned { get; set; } public string Creator { get; set; } public string Assignee { get; set; } public MilestoneKeyValue Milestone { get; set; } public class MilestoneKeyValue { public string Name { get; set; } public bool IsMilestone { get; set; } public string Value { get; set; } } public IssuesFilterModel() { Ascending = false; Open = true; } /// /// Predefined 'Open' filter /// public static IssuesFilterModel CreateOpenFilter() { return new IssuesFilterModel { Open = true }; } /// /// Predefined 'Closed' filter /// public static IssuesFilterModel CreateClosedFilter() { return new IssuesFilterModel { Open = false }; } /// /// Predefined 'Mine' filter /// /// The mine filter. /// Username. public static IssuesFilterModel CreateMineFilter(string username) { return new IssuesFilterModel { Assignee = username, Open = true }; } public override IssuesFilterModel Clone() { return (IssuesFilterModel)this.MemberwiseClone(); } public override bool Equals(object obj) { if (obj == null) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != typeof(IssuesFilterModel)) return false; var other = (IssuesFilterModel)obj; return Ascending == other.Ascending && Labels == other.Labels && Open == other.Open && Since == other.Since && SortType == other.SortType && Mentioned == other.Mentioned && Creator == other.Creator && Assignee == other.Assignee && Milestone == other.Milestone; } public override int GetHashCode() { unchecked { return Ascending.GetHashCode() ^ (Labels != null ? Labels.GetHashCode() : 0) ^ Open.GetHashCode() ^ Since.GetHashCode() ^ SortType.GetHashCode() ^ (Mentioned != null ? Mentioned.GetHashCode() : 0) ^ (Creator != null ? Creator.GetHashCode() : 0) ^ (Assignee != null ? Assignee.GetHashCode() : 0) ^ (Milestone != null ? Milestone.GetHashCode() : 0); } } } } ================================================ FILE: CodeHub.Core/Filters/MyIssuesFilterModel.cs ================================================ using System; using System.ComponentModel; namespace CodeHub.Core.Filters { public class MyIssuesFilterModel : BaseIssuesFilterModel { public string Labels { get; set; } public Filter FilterType { get; set; } public bool Open { get; set; } public DateTime? Since { get; set; } public MyIssuesFilterModel() { Open = true; FilterType = Filter.All; } /// /// Predefined 'Open' filter /// public static MyIssuesFilterModel CreateOpenFilter() { return new MyIssuesFilterModel { FilterType = Filter.All, Open = true }; } /// /// Predefined 'Closed' filter /// public static MyIssuesFilterModel CreateClosedFilter() { return new MyIssuesFilterModel { FilterType = Filter.All, Open = false }; } public override MyIssuesFilterModel Clone() { return (MyIssuesFilterModel)this.MemberwiseClone(); } public override bool Equals(object obj) { if (obj == null) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != typeof(MyIssuesFilterModel)) return false; var other = (MyIssuesFilterModel)obj; return Ascending == other.Ascending && Labels == other.Labels && FilterType == other.FilterType && Open == other.Open && Since == other.Since && SortType == other.SortType; } public override int GetHashCode() { unchecked { return Ascending.GetHashCode() ^ (Labels != null ? Labels.GetHashCode() : 0) ^ FilterType.GetHashCode() ^ Open.GetHashCode() ^ Since.GetHashCode() ^ SortType.GetHashCode(); } } public enum Filter { [Description("Assigned To You")] Assigned, [Description("Created By You")] Created, [Description("Mentioning You")] Mentioned, [Description("Issues Subscribed To")] Subscribed, All } } } ================================================ FILE: CodeHub.Core/Filters/NotificationsFilterModel.cs ================================================ using CodeHub.Core.ViewModels; namespace CodeHub.Core.Filters { public class NotificationsFilterModel : FilterModel { public bool All { get; set; } public bool Participating { get; set; } public NotificationsFilterModel() { All = false; Participating = false; } public override bool Equals(object obj) { if (obj == null) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != typeof(NotificationsFilterModel)) return false; var other = (NotificationsFilterModel)obj; return All == other.All && Participating == other.Participating; } public override int GetHashCode() { unchecked { return All.GetHashCode() ^ Participating.GetHashCode(); } } public override NotificationsFilterModel Clone() { return (NotificationsFilterModel)this.MemberwiseClone(); } public static NotificationsFilterModel CreateUnreadFilter() { return new NotificationsFilterModel { All = false, Participating = false }; } public static NotificationsFilterModel CreateParticipatingFilter() { return new NotificationsFilterModel { All = false, Participating = true }; } public static NotificationsFilterModel CreateAllFilter() { return new NotificationsFilterModel { All = true, Participating = false }; } } } ================================================ FILE: CodeHub.Core/Interactions.cs ================================================ using System; using System.Reactive; using System.Threading.Tasks; using ReactiveUI; namespace CodeHub.Core { public class UserError { public string Title { get; } public string Message { get; } public UserError(string message, Exception exception = null) : this("Error", message, exception) { } public UserError(string title, string message, Exception exception = null) { Title = title; Message = exception == null ? message : $"{message} {ExceptionMessage(exception)}"; } private static string ExceptionMessage(Exception exception) { if (exception is TaskCanceledException) return "The request timed out waiting for the server to respond."; else return exception?.Message; } } public static class Interactions { public static readonly Interaction Errors = new Interaction(); } } ================================================ FILE: CodeHub.Core/Messages/GistAddMessage.cs ================================================ namespace CodeHub.Core.Messages { public class GistAddMessage { public Octokit.Gist Gist { get; } public GistAddMessage(Octokit.Gist gist) { Gist = gist; } } } ================================================ FILE: CodeHub.Core/Messages/IssueAddMessage.cs ================================================ using GitHubSharp.Models; namespace CodeHub.Core.Messages { public class IssueAddMessage { public IssueModel Issue { get; } public IssueAddMessage(IssueModel issue) { Issue = issue; } } } ================================================ FILE: CodeHub.Core/Messages/IssueEditMessage.cs ================================================ using GitHubSharp.Models; namespace CodeHub.Core.Messages { public class IssueEditMessage { public IssueModel Issue { get; } public IssueEditMessage(IssueModel issue) { Issue = issue; } } } ================================================ FILE: CodeHub.Core/Messages/LogoutMessage.cs ================================================ namespace CodeHub.Core.Messages { public class LogoutMessage { } } ================================================ FILE: CodeHub.Core/Messages/NotificationCountMessage.cs ================================================ namespace CodeHub.Core.Messages { public class NotificationCountMessage { public int Count { get; } public NotificationCountMessage(int count) { Count = count; } } } ================================================ FILE: CodeHub.Core/Messages/PullRequestEditMessage.cs ================================================ using GitHubSharp.Models; namespace CodeHub.Core.Messages { public class PullRequestEditMessage { public PullRequestModel PullRequest { get; } public PullRequestEditMessage(PullRequestModel pullRequest) { PullRequest = pullRequest; } } } ================================================ FILE: CodeHub.Core/Messages/SelectIssueLabelsMessage.cs ================================================ using System.Collections.Generic; using System.Linq; using GitHubSharp.Models; namespace CodeHub.Core.Messages { public class SelectIssueLabelsMessage { public LabelModel[] Labels { get; } public SelectIssueLabelsMessage(IEnumerable labels) { Labels = labels.ToArray(); } } } ================================================ FILE: CodeHub.Core/Messages/SelectedAssignedToMessage.cs ================================================ using GitHubSharp.Models; namespace CodeHub.Core.Messages { public class SelectedAssignedToMessage { public BasicUserModel User { get; } public SelectedAssignedToMessage(BasicUserModel user) { User = user; } } } ================================================ FILE: CodeHub.Core/Messages/SelectedMilestoneMessage.cs ================================================ using GitHubSharp.Models; namespace CodeHub.Core.Messages { public class SelectedMilestoneMessage { public MilestoneModel Milestone { get; } public SelectedMilestoneMessage(MilestoneModel milestone) { Milestone = milestone; } } } ================================================ FILE: CodeHub.Core/Messages/SourceEditMessage.cs ================================================ using GitHubSharp.Models; namespace CodeHub.Core.Messages { public class SourceEditMessage { public string OldSha; public string Data; public ContentUpdateModel Update; } } ================================================ FILE: CodeHub.Core/PresentationValues.cs ================================================ namespace CodeHub.Core { public class PresentationValues { /// /// Some sort of presentation value to indicate the view that we're switching to belongs at the root of the slideout /// public const string SlideoutRootPresentation = "__Slideout_Root_Presentation__"; } } ================================================ FILE: CodeHub.Core/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. [assembly: AssemblyTitle("CodeHub.Core")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("CodeHub.Core")] [assembly: AssemblyCopyright("Copyright © 2013")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("71b12732-c50c-4058-91d9-0be37f71af21")] // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] ================================================ FILE: CodeHub.Core/Secrets.cs ================================================ using System; namespace CodeHub.Core { public static class Secrets { public static string GithubOAuthId { get { throw new InvalidOperationException("You must get your own Key"); } } public static string GithubOAuthSecret { get { throw new InvalidOperationException("You must get your own Secret"); } } public static string ErrorReportingKey { get { throw new InvalidOperationException("You must have a valid error reporting key"); } } } } ================================================ FILE: CodeHub.Core/Services/AccountsService.cs ================================================ using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; using System.Threading.Tasks; using Akavache; using CodeHub.Core.Data; namespace CodeHub.Core.Services { public class AccountsService : IAccountsService { public Task GetActiveAccount() => Get(Settings.DefaultAccount); public Task SetActiveAccount(Account account) { Settings.DefaultAccount = account == null ? null : GetKey(account); return Task.FromResult(false); } public Task> GetAccounts() => BlobCache.UserAccount.GetAllObjects() .Select(x => x.OrderBy(y => y.Username).AsEnumerable()) .ToTask(); public Task Save(Account account) => BlobCache.UserAccount.InsertObject(GetKey(account), account).ToTask(); public Task Remove(Account account) => BlobCache.UserAccount.Invalidate(GetKey(account)).ToTask(); public Task Get(string domain, string username) => Get(GetKey(username, domain)); public Task Get(string key) => BlobCache.UserAccount.GetObject(key) .Catch(Observable.Return(null)) .ToTask(); private string GetKey(Account account) => GetKey(account.Username, account.Domain); private string GetKey(string username, string domain) => "account_" + username + domain; } } ================================================ FILE: CodeHub.Core/Services/ApplicationService.cs ================================================ using CodeHub.Core.Data; using GitHubSharp; using System; using System.Threading.Tasks; using CodeHub.Core.Utilities; namespace CodeHub.Core.Services { public class ApplicationService : IApplicationService { private readonly IAccountsService _accountsService; public Client Client { get; private set; } public Octokit.GitHubClient GitHubClient { get; private set; } public Account Account { get; private set; } public Action ActivationAction { get; set; } public ApplicationService(IAccountsService accountsService) { _accountsService = accountsService; } public void DeactivateUser() { _accountsService.SetActiveAccount(null).Wait(); Account = null; Client = null; } public void SetUserActivationAction(Action action) { if (Account != null) action(); else ActivationAction = action; } public Task UpdateActiveAccount() => _accountsService.Save(Account); public async Task LoginAccount(Account account) { var domain = account.Domain ?? Client.DefaultApi; Client client = null; Octokit.Credentials credentials = null; if (!string.IsNullOrEmpty(account.OAuth)) { client = Client.BasicOAuth(account.OAuth, domain); credentials = new Octokit.Credentials(account.OAuth); } else if (account.IsEnterprise || !string.IsNullOrEmpty(account.Password)) { client = Client.Basic(account.Username, account.Password, domain); credentials = new Octokit.Credentials(account.Username, account.Password); } var octoClient = OctokitClientFactory.Create(new Uri(domain), credentials); var user = await octoClient.User.Current(); account.Username = user.Login; account.AvatarUrl = user.AvatarUrl; client.Username = user.Login; await _accountsService.Save(account); await _accountsService.SetActiveAccount(account); Account = account; Client = client; GitHubClient = octoClient; } } } ================================================ FILE: CodeHub.Core/Services/IAccountsService.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using CodeHub.Core.Data; namespace CodeHub.Core.Services { public interface IAccountsService { Task GetActiveAccount(); Task SetActiveAccount(Account account); Task> GetAccounts(); Task Save(Account account); Task Remove(Account account); Task Get(string domain, string username); } } ================================================ FILE: CodeHub.Core/Services/IAlertDialogService.cs ================================================ using System; using System.Threading.Tasks; using System.Reactive.Disposables; using ReactiveUI; namespace CodeHub.Core.Services { public interface IAlertDialogService { Task PromptYesNo(string title, string message); Task Alert(string title, string message); Task PromptTextBox(string title, string message, string defaultValue, string okTitle); void Show(string text); void Hide(); } public static class AlertDialogServiceExtensions { public static IDisposable Activate(this IAlertDialogService @this, string text) { @this.Show(text); return Disposable.Create(@this.Hide); } public static IDisposable Activate(this IAlertDialogService @this, IObservable observable, string text) { return observable.Subscribe(x => { if (x) @this.Show(text); else @this.Hide(); }); } public static IDisposable Activate(this IAlertDialogService @this, ReactiveCommand command, string text) { return command.IsExecuting.Subscribe(x => { if (x) @this.Show(text); else @this.Hide(); }); } public static IDisposable AlertExecuting(this ReactiveCommand @this, IAlertDialogService dialogFactory, string text) { return @this.IsExecuting.Subscribe(x => { if (x) dialogFactory.Show(text); else dialogFactory.Hide(); }); } } } ================================================ FILE: CodeHub.Core/Services/IApplicationService.cs ================================================ using System; using System.Threading.Tasks; using CodeHub.Core.Data; namespace CodeHub.Core.Services { public interface IApplicationService { GitHubSharp.Client Client { get; } Octokit.GitHubClient GitHubClient { get; } Account Account { get; } Task UpdateActiveAccount(); void DeactivateUser(); void SetUserActivationAction(Action action); Action ActivationAction { get; set; } Task LoginAccount(Account account); } } ================================================ FILE: CodeHub.Core/Services/IFeaturesService.cs ================================================ using System.Threading.Tasks; namespace CodeHub.Core.Services { public interface IFeaturesService { bool IsProEnabled { get; } void ActivateProDirect(); Task ActivatePro(); Task RestorePro(); } } ================================================ FILE: CodeHub.Core/Services/IImgurService.cs ================================================ using System.Threading.Tasks; using CodeHub.Core.Data; namespace CodeHub.Core.Services { public interface IImgurService { Task SendImage(byte[] data); } } ================================================ FILE: CodeHub.Core/Services/IMarkdownService.cs ================================================ using System.Threading.Tasks; namespace CodeHub.Core.Services { public interface IMarkdownService { Task Convert(string s); } } ================================================ FILE: CodeHub.Core/Services/IMessageService.cs ================================================ using System; using System.Collections.Generic; namespace CodeHub.Core.Services { public interface IMessageService { void Send(T message); IDisposable Listen(Action action); } } ================================================ FILE: CodeHub.Core/Services/INetworkActivityService.cs ================================================ using System; using System.Reactive.Disposables; namespace CodeHub.Core.Services { public interface INetworkActivityService { void PushNetworkActive(); void PopNetworkActive(); } public static class NetworkActivityServiceExtensions { public static IDisposable ActivateNetwork(this INetworkActivityService @this) { @this.PushNetworkActive(); return Disposable.Create(@this.PopNetworkActive); } } } ================================================ FILE: CodeHub.Core/Services/IPushNotificationsService.cs ================================================ using System.Threading.Tasks; namespace CodeHub.Core.Services { public interface IPushNotificationsService { Task Register(); Task Deregister(); } } ================================================ FILE: CodeHub.Core/Services/IViewModelTxService.cs ================================================ namespace CodeHub.Core.Services { public interface IViewModelTxService { void Add(object obj); object Get(); } } ================================================ FILE: CodeHub.Core/Services/ImgurService.cs ================================================ using System; using System.Threading.Tasks; using System.Net.Http; using Newtonsoft.Json; using CodeHub.Core.Data; namespace CodeHub.Core.Services { public class ImgurService : IImgurService { private const string AuthorizationClientId = "4d2779fd2cc56cb"; private const string ImgurPostUrl = "https://api.imgur.com/3/image"; public async Task SendImage(byte[] data) { var client = new HttpClient(); client.Timeout = new TimeSpan(0, 0, 30); client.DefaultRequestHeaders.Add("Authorization", "Client-ID " + AuthorizationClientId); var body = JsonConvert.SerializeObject(new { image = Convert.ToBase64String(data) }); var content = new StringContent(body, System.Text.Encoding.UTF8, "application/json"); var response = await client.PostAsync(ImgurPostUrl, content).ConfigureAwait(false); if (!response.IsSuccessStatusCode) throw new InvalidOperationException("Unable to post to Imgur! " + response.ReasonPhrase); var responseBody = await response.Content.ReadAsStringAsync().ConfigureAwait(false); return JsonConvert.DeserializeObject(responseBody); } } } ================================================ FILE: CodeHub.Core/Services/LoginService.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; using CodeHub.Core.Data; using CodeHub.Core.Utilities; namespace CodeHub.Core.Services { public interface ILoginService { Task LoginWithToken(string clientId, string clientSecret, string code, string redirect, string requestDomain, string apiDomain); Task LoginWithToken(string apiDomain, string webDomain, string token, bool enterprise); Task LoginWithBasic(string domain, string user, string pass, string twoFactor = null); } public class LoginService : ILoginService { private static readonly string[] Scopes = { "user", "repo", "notifications", "gist" }; private readonly IApplicationService _applicationService; private readonly IAccountsService _accountsService; public LoginService( IAccountsService accountsService, IApplicationService applicationService) { _accountsService = accountsService; _applicationService = applicationService; } public async Task LoginWithToken(string clientId, string clientSecret, string code, string redirect, string requestDomain, string apiDomain) { var oauthRequest = new Octokit.OauthTokenRequest(clientId, clientSecret, code) { RedirectUri = new Uri(redirect) }; var client = new Octokit.GitHubClient(OctokitClientFactory.UserAgent); var token = await client.Oauth.CreateAccessToken(oauthRequest); var credentials = new Octokit.Credentials(token.AccessToken); client = OctokitClientFactory.Create(new Uri(apiDomain), credentials); var user = await client.User.Current(); var account = await _accountsService.Get(apiDomain, user.Login); account = account ?? new Account { Username = user.Login }; account.OAuth = token.AccessToken; account.AvatarUrl = user.AvatarUrl; account.Domain = apiDomain; account.WebDomain = requestDomain; await _accountsService.Save(account); await _applicationService.LoginAccount(account); } public async Task LoginWithToken(string apiDomain, string webDomain, string token, bool enterprise) { if (string.IsNullOrEmpty(token)) throw new ArgumentException("Token is invalid"); if (apiDomain != null && !Uri.IsWellFormedUriString(apiDomain, UriKind.Absolute)) throw new ArgumentException("API Domain is invalid"); if (webDomain != null && !Uri.IsWellFormedUriString(webDomain, UriKind.Absolute)) throw new ArgumentException("Web Domain is invalid"); var credentials = new Octokit.Credentials(token); var client = OctokitClientFactory.Create(new Uri(apiDomain), credentials); var userInfo = await client.User.Current(); var scopes = await GetScopes(apiDomain, userInfo.Login, token); CheckScopes(scopes); var account = (await _accountsService.Get(apiDomain, userInfo.Login)) ?? new Account(); account.Username = userInfo.Login; account.Domain = apiDomain; account.WebDomain = webDomain; account.IsEnterprise = enterprise; account.OAuth = token; account.AvatarUrl = userInfo.AvatarUrl; await _accountsService.Save(account); await _applicationService.LoginAccount(account); } public async Task LoginWithBasic(string domain, string user, string pass, string twoFactor = null) { if (string.IsNullOrEmpty(user)) throw new ArgumentException("Username is invalid"); if (string.IsNullOrEmpty(pass)) throw new ArgumentException("Password is invalid"); if (domain == null || !Uri.IsWellFormedUriString(domain, UriKind.Absolute)) throw new ArgumentException("Domain is invalid"); var newAuthorization = new Octokit.NewAuthorization( $"CodeHub: {user}", Scopes, Guid.NewGuid().ToString()); var credentials = new Octokit.Credentials(user, pass); var client = OctokitClientFactory.Create(new Uri(domain), credentials); var authorization = await (twoFactor == null ? client.Authorization.Create(newAuthorization) : client.Authorization.Create(newAuthorization, twoFactor)); var existingAccount = await _accountsService.Get(domain, user); var account = existingAccount ?? new Account { Username = user, IsEnterprise = true, WebDomain = domain, Domain = domain }; account.OAuth = authorization.Token; await _applicationService.LoginAccount(account); } private static async Task> GetScopes(string domain, string username, string token) { var client = new HttpClient(); var authToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format("{0}:{1}", username, token))); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authToken); domain = (domain.EndsWith("/", StringComparison.Ordinal) ? domain : domain + "/") + "user"; var response = await client.GetAsync(domain); if (!response.Headers.TryGetValues("X-OAuth-Scopes", out IEnumerable scopes)) return new List(); var values = scopes.FirstOrDefault() ?? string.Empty; return values.Split(',').Select(x => x.Trim()).ToList(); } private static void CheckScopes(IEnumerable scopes) { var missing = OctokitClientFactory.Scopes.Except(scopes).ToList(); if (missing.Any()) throw new InvalidOperationException("Missing scopes! You are missing access to the following " + "scopes that are necessary for CodeHub to operate correctly: " + string.Join(", ", missing)); } } } ================================================ FILE: CodeHub.Core/Services/MessageService.cs ================================================ using System; using System.Collections.Generic; namespace CodeHub.Core.Services { public class MessageService : IMessageService { private readonly IList> _subscriptions = new List>(); public IDisposable Listen(Action action) { var obj = Tuple.Create(typeof(T), new WeakReference(action)); lock (_subscriptions) _subscriptions.Add(obj); return new Reference(action, () => _subscriptions.Remove(obj)); } public void Send(T message) { lock (_subscriptions) { var shouldRemove = new LinkedList>(); foreach (var sub in _subscriptions) { if (!sub.Item2.IsAlive) shouldRemove.AddLast(sub); if (sub.Item1 == typeof(T)) { var handle = sub.Item2.Target; if (handle != null) { ((Action)handle).Invoke(message); } } } foreach (var r in shouldRemove) _subscriptions.Remove(r); } } private class Reference : IDisposable { private readonly Action _removal; private readonly object _handle; public Reference(object handle, Action removal) { _handle = handle; _removal = removal; } public void Dispose() => _removal.Invoke(); } } } ================================================ FILE: CodeHub.Core/Services/ViewModelTxService.cs ================================================ namespace CodeHub.Core.Services { public class ViewModelTxService : IViewModelTxService { private object _data; public void Add(object obj) { _data = obj; } public object Get() { return _data; } } } ================================================ FILE: CodeHub.Core/Settings.cs ================================================ using Plugin.Settings; using Plugin.Settings.Abstractions; namespace CodeHub.Core { public static class Settings { private const string DefaultAccountKey = "DEFAULT_ACCOUNT"; private const string ShouldStarKey = "SHOULD_STAR_CODEHUB"; private const string ShouldWatchKey = "SHOULD_WATCH_CODEHUB"; private const string HasSeenWelcomeKey = "HAS_SEEN_WELCOME_INTRO"; private const string ProEditionKey = "com.dillonbuchanan.codehub.pro"; private const string HasSeenOAuthKey = "HAS_SEEN_OAUTH_INFO"; private const string ImgurUploadWarn = "IMGUR_UPLOAD_WARN"; private static ISettings AppSettings => CrossSettings.Current; public static string DefaultAccount { get => AppSettings.GetValueOrDefault(DefaultAccountKey, null); set => AppSettings.AddOrUpdateValue(DefaultAccountKey, value); } public static bool ShouldStar { get => AppSettings.GetValueOrDefault(ShouldStarKey, false); set => AppSettings.AddOrUpdateValue(ShouldStarKey, value); } public static bool ShouldWatch { get => AppSettings.GetValueOrDefault(ShouldWatchKey, false); set => AppSettings.AddOrUpdateValue(ShouldWatchKey, value); } public static bool HasSeenWelcome { get => AppSettings.GetValueOrDefault(HasSeenWelcomeKey, false); set => AppSettings.AddOrUpdateValue(HasSeenWelcomeKey, value); } public static bool IsProEnabled { get => AppSettings.GetValueOrDefault(ProEditionKey, false); set => AppSettings.AddOrUpdateValue(ProEditionKey, value); } public static bool HasSeenOAuthWelcome { get => AppSettings.GetValueOrDefault(HasSeenOAuthKey, false); set => AppSettings.AddOrUpdateValue(HasSeenOAuthKey, value); } public static bool HasSeenImgurUploadWarn { get => AppSettings.GetValueOrDefault(ImgurUploadWarn, false); set => AppSettings.AddOrUpdateValue(ImgurUploadWarn, value); } } } ================================================ FILE: CodeHub.Core/Utils/CustomObservableCollection.cs ================================================ using System; using System.Collections.ObjectModel; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Collections.Specialized; namespace CodeHub.Core.Utils { public class CustomObservableCollection : ObservableCollection { public CustomObservableCollection() { } public CustomObservableCollection(IEnumerable collection) : base(collection) { } public CustomObservableCollection(List list) : base(list) { } public virtual void AddRange(IEnumerable collection) { if (collection == null) throw new ArgumentNullException("collection"); var added = false; var enumerable = collection as IList ?? collection.ToList(); foreach (var item in enumerable) { this.Items.Add(item); added = true; } if (added) { this.OnPropertyChanged(new PropertyChangedEventArgs("Count")); this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, enumerable.ToList())); // Cannot use NotifyCollectionChangedAction.Add, because Constructor supports only the 'Reset' action. } } public virtual void RemoveRange(IEnumerable collection) { if (collection == null) throw new ArgumentNullException("collection"); var removed = false; var enumerable = collection as T[] ?? collection.ToArray(); foreach (var item in enumerable.Where(item => this.Items.Remove(item))) removed = true; if (!removed) return; this.OnPropertyChanged(new PropertyChangedEventArgs("Count")); this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, enumerable.ToList())); // Cannot use NotifyCollectionChangedAction.Remove, because Constructor supports only the 'Reset' action. } public virtual void Reset(T item) { this.Reset(new List { item }); } public virtual void Reset(IEnumerable collection) { if (collection == null) return; int count = this.Count; // Step 1: Clear the old items this.Items.Clear(); // Step 2: Add new items foreach (T item in collection) this.Items.Add(item); // Step 3: Don't forget the event if (this.Count != count) this.OnPropertyChanged(new PropertyChangedEventArgs("Count")); this.OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } } ================================================ FILE: CodeHub.Core/Utils/DateTimeExtensions.cs ================================================ namespace System { public static class DateTimeExtensions { public static int TotalDaysAgo(this DateTimeOffset d) { return Convert.ToInt32(Math.Round(DateTimeOffset.Now.Subtract(d).TotalDays)); } } } ================================================ FILE: CodeHub.Core/Utils/Emojis.cs ================================================ using System; using System.Collections.Generic; using System.Text.RegularExpressions; namespace CodeHub.Core.Utilities { public static class Emojis { private static readonly IDictionary _emojis; static Emojis() { _emojis = new Dictionary(1000, StringComparer.OrdinalIgnoreCase) { {"smile", "😄"}, {"smiley", "😃"}, {"grinning", "😀"}, {"blush", "😊"}, {"relaxed", "☺️"}, {"wink", "😉"}, {"heart_eyes", "😍"}, {"kissing_heart", "😘"}, {"kissing_closed_eyes", "😚"}, {"kissing", "😗"}, {"kissing_smiling_eyes", "😙"}, {"stuck_out_tongue_winking_eye", "😜"}, {"stuck_out_tongue_closed_eyes", "😝"}, {"stuck_out_tongue", "😛"}, {"flushed", "😳"}, {"grin", "😁"}, {"pensive", "😔"}, {"relieved", "😌"}, {"unamused", "😒"}, {"disappointed", "😞"}, {"persevere", "😣"}, {"cry", "😢"}, {"joy", "😂"}, {"sob", "😭"}, {"sleepy", "😪"}, {"disappointed_relieved", "😥"}, {"cold_sweat", "😰"}, {"sweat_smile", "😅"}, {"sweat", "😓"}, {"weary", "😩"}, {"tired_face", "😫"}, {"fearful", "😨"}, {"scream", "😱"}, {"angry", "😠"}, {"rage", "😡"}, {"triumph", "😤"}, {"confounded", "😖"}, {"laughing", "😆"}, {"satisfied", "😆"}, {"yum", "😋"}, {"mask", "😷"}, {"sunglasses", "😎"}, {"sleeping", "😴"}, {"dizzy_face", "😵"}, {"astonished", "😲"}, {"worried", "😟"}, {"frowning", "😦"}, {"anguished", "😧"}, {"smiling_imp", "😈"}, {"imp", "👿"}, {"open_mouth", "😮"}, {"grimacing", "😬"}, {"neutral_face", "😐"}, {"confused", "😕"}, {"hushed", "😯"}, {"no_mouth", "😶"}, {"innocent", "😇"}, {"smirk", "😏"}, {"expressionless", "😑"}, {"man_with_gua_pi_mao", "👲"}, {"man_with_turban", "👳"}, {"cop", "👮"}, {"construction_worker", "👷"}, {"guardsman", "💂"}, {"baby", "👶"}, {"boy", "👦"}, {"girl", "👧"}, {"man", "👨"}, {"woman", "👩"}, {"older_man", "👴"}, {"older_woman", "👵"}, {"person_with_blond_hair", "👱"}, {"angel", "👼"}, {"princess", "👸"}, {"smiley_cat", "😺"}, {"smile_cat", "😸"}, {"heart_eyes_cat", "😻"}, {"kissing_cat", "😽"}, {"smirk_cat", "😼"}, {"scream_cat", "🙀"}, {"crying_cat_face", "😿"}, {"joy_cat", "😹"}, {"pouting_cat", "😾"}, {"japanese_ogre", "👹"}, {"japanese_goblin", "👺"}, {"see_no_evil", "🙈"}, {"hear_no_evil", "🙉"}, {"speak_no_evil", "🙊"}, {"skull", "💀"}, {"alien", "👽"}, {"hankey", "💩"}, {"poop", "💩"}, {"shit", "💩"}, {"fire", "🔥"}, {"sparkles", "✨"}, {"star2", "🌟"}, {"dizzy", "💫"}, {"boom", "💥"}, {"collision", "💥"}, {"anger", "💢"}, {"sweat_drops", "💦"}, {"droplet", "💧"}, {"zzz", "💤"}, {"dash", "💨"}, {"ear", "👂"}, {"eyes", "👀"}, {"nose", "👃"}, {"tongue", "👅"}, {"lips", "👄"}, {"+1", "👍"}, {"thumbsup", "👍"}, {"-1", "👎"}, {"thumbsdown", "👎"}, {"ok_hand", "👌"}, {"facepunch", "👊"}, {"punch", "👊"}, {"fist", "✊"}, {"v", "✌️"}, {"wave", "👋"}, {"hand", "✋"}, {"raised_hand", "✋"}, {"open_hands", "👐"}, {"point_up_2", "👆"}, {"point_down", "👇"}, {"point_right", "👉"}, {"point_left", "👈"}, {"raised_hands", "🙌"}, {"pray", "🙏"}, {"point_up", "☝️"}, {"clap", "👏"}, {"muscle", "💪"}, {"walking", "🚶"}, {"runner", "🏃"}, {"running", "🏃"}, {"dancer", "💃"}, {"couple", "👫"}, {"family", "👪"}, {"two_men_holding_hands", "👬"}, {"two_women_holding_hands", "👭"}, {"couplekiss", "💏"}, {"couple_with_heart", "💑"}, {"dancers", "👯"}, {"ok_woman", "🙆"}, {"no_good", "🙅"}, {"information_desk_person", "💁"}, {"raising_hand", "🙋"}, {"massage", "💆"}, {"haircut", "💇"}, {"nail_care", "💅"}, {"bride_with_veil", "👰"}, {"person_with_pouting_face", "🙎"}, {"person_frowning", "🙍"}, {"bow", "🙇"}, {"tophat", "🎩"}, {"crown", "👑"}, {"womans_hat", "👒"}, {"athletic_shoe", "👟"}, {"mans_shoe", "👞"}, {"shoe", "👞"}, {"sandal", "👡"}, {"high_heel", "👠"}, {"boot", "👢"}, {"shirt", "👕"}, {"tshirt", "👕"}, {"necktie", "👔"}, {"womans_clothes", "👚"}, {"dress", "👗"}, {"running_shirt_with_sash", "🎽"}, {"jeans", "👖"}, {"kimono", "👘"}, {"bikini", "👙"}, {"briefcase", "💼"}, {"handbag", "👜"}, {"pouch", "👝"}, {"purse", "👛"}, {"eyeglasses", "👓"}, {"ribbon", "🎀"}, {"closed_umbrella", "🌂"}, {"lipstick", "💄"}, {"yellow_heart", "💛"}, {"blue_heart", "💙"}, {"purple_heart", "💜"}, {"green_heart", "💚"}, {"heart", "❤️"}, {"broken_heart", "💔"}, {"heartpulse", "💗"}, {"heartbeat", "💓"}, {"two_hearts", "💕"}, {"sparkling_heart", "💖"}, {"revolving_hearts", "💞"}, {"cupid", "💘"}, {"love_letter", "💌"}, {"kiss", "💋"}, {"ring", "💍"}, {"gem", "💎"}, {"bust_in_silhouette", "👤"}, {"busts_in_silhouette", "👥"}, {"speech_balloon", "💬"}, {"footprints", "👣"}, {"thought_balloon", "💭"}, {"dog", "🐶"}, {"wolf", "🐺"}, {"cat", "🐱"}, {"mouse", "🐭"}, {"hamster", "🐹"}, {"rabbit", "🐰"}, {"frog", "🐸"}, {"tiger", "🐯"}, {"koala", "🐨"}, {"bear", "🐻"}, {"pig", "🐷"}, {"pig_nose", "🐽"}, {"cow", "🐮"}, {"boar", "🐗"}, {"monkey_face", "🐵"}, {"monkey", "🐒"}, {"horse", "🐴"}, {"sheep", "🐑"}, {"elephant", "🐘"}, {"panda_face", "🐼"}, {"penguin", "🐧"}, {"bird", "🐦"}, {"baby_chick", "🐤"}, {"hatched_chick", "🐥"}, {"hatching_chick", "🐣"}, {"chicken", "🐔"}, {"snake", "🐍"}, {"turtle", "🐢"}, {"bug", "🐛"}, {"bee", "🐝"}, {"honeybee", "🐝"}, {"ant", "🐜"}, {"beetle", "🐞"}, {"snail", "🐌"}, {"octopus", "🐙"}, {"shell", "🐚"}, {"tropical_fish", "🐠"}, {"fish", "🐟"}, {"dolphin", "🐬"}, {"flipper", "🐬"}, {"whale", "🐳"}, {"whale2", "🐋"}, {"cow2", "🐄"}, {"ram", "🐏"}, {"rat", "🐀"}, {"water_buffalo", "🐃"}, {"tiger2", "🐅"}, {"rabbit2", "🐇"}, {"dragon", "🐉"}, {"racehorse", "🐎"}, {"goat", "🐐"}, {"rooster", "🐓"}, {"dog2", "🐕"}, {"pig2", "🐖"}, {"mouse2", "🐁"}, {"ox", "🐂"}, {"dragon_face", "🐲"}, {"blowfish", "🐡"}, {"crocodile", "🐊"}, {"camel", "🐫"}, {"dromedary_camel", "🐪"}, {"leopard", "🐆"}, {"cat2", "🐈"}, {"poodle", "🐩"}, {"feet", "🐾"}, {"paw_prints", "🐾"}, {"bouquet", "💐"}, {"cherry_blossom", "🌸"}, {"tulip", "🌷"}, {"four_leaf_clover", "🍀"}, {"rose", "🌹"}, {"sunflower", "🌻"}, {"hibiscus", "🌺"}, {"maple_leaf", "🍁"}, {"leaves", "🍃"}, {"fallen_leaf", "🍂"}, {"herb", "🌿"}, {"ear_of_rice", "🌾"}, {"mushroom", "🍄"}, {"cactus", "🌵"}, {"palm_tree", "🌴"}, {"evergreen_tree", "🌲"}, {"deciduous_tree", "🌳"}, {"chestnut", "🌰"}, {"seedling", "🌱"}, {"blossom", "🌼"}, {"globe_with_meridians", "🌐"}, {"sun_with_face", "🌞"}, {"full_moon_with_face", "🌝"}, {"new_moon_with_face", "🌚"}, {"new_moon", "🌑"}, {"waxing_crescent_moon", "🌒"}, {"first_quarter_moon", "🌓"}, {"moon", "🌔"}, {"waxing_gibbous_moon", "🌔"}, {"full_moon", "🌕"}, {"waning_gibbous_moon", "🌖"}, {"last_quarter_moon", "🌗"}, {"waning_crescent_moon", "🌘"}, {"last_quarter_moon_with_face", "🌜"}, {"first_quarter_moon_with_face", "🌛"}, {"crescent_moon", "🌙"}, {"earth_africa", "🌍"}, {"earth_americas", "🌎"}, {"earth_asia", "🌏"}, {"volcano", "🌋"}, {"milky_way", "🌌"}, {"stars", "🌠"}, {"star", "⭐"}, {"sunny", "☀️"}, {"partly_sunny", "⛅"}, {"cloud", "☁️"}, {"zap", "⚡"}, {"umbrella", "☔"}, {"snowflake", "❄️"}, {"snowman", "⛄"}, {"cyclone", "🌀"}, {"foggy", "🌁"}, {"rainbow", "🌈"}, {"ocean", "🌊"}, {"bamboo", "🎍"}, {"gift_heart", "💝"}, {"dolls", "🎎"}, {"school_satchel", "🎒"}, {"mortar_board", "🎓"}, {"flags", "🎏"}, {"fireworks", "🎆"}, {"sparkler", "🎇"}, {"wind_chime", "🎐"}, {"rice_scene", "🎑"}, {"jack_o_lantern", "🎃"}, {"ghost", "👻"}, {"santa", "🎅"}, {"christmas_tree", "🎄"}, {"gift", "🎁"}, {"tanabata_tree", "🎋"}, {"tada", "🎉"}, {"confetti_ball", "🎊"}, {"balloon", "🎈"}, {"crossed_flags", "🎌"}, {"crystal_ball", "🔮"}, {"movie_camera", "🎥"}, {"camera", "📷"}, {"video_camera", "📹"}, {"vhs", "📼"}, {"cd", "💿"}, {"dvd", "📀"}, {"minidisc", "💽"}, {"floppy_disk", "💾"}, {"computer", "💻"}, {"iphone", "📱"}, {"phone", "☎️"}, {"telephone", "☎️"}, {"telephone_receiver", "📞"}, {"pager", "📟"}, {"fax", "📠"}, {"satellite", "📡"}, {"tv", "📺"}, {"radio", "📻"}, {"loud_sound", "🔊"}, {"sound", "🔉"}, {"speaker", "🔈"}, {"mute", "🔇"}, {"bell", "🔔"}, {"no_bell", "🔕"}, {"loudspeaker", "📢"}, {"mega", "📣"}, {"hourglass_flowing_sand", "⏳"}, {"hourglass", "⌛"}, {"alarm_clock", "⏰"}, {"watch", "⌚"}, {"unlock", "🔓"}, {"lock", "🔒"}, {"lock_with_ink_pen", "🔏"}, {"closed_lock_with_key", "🔐"}, {"key", "🔑"}, {"mag_right", "🔎"}, {"bulb", "💡"}, {"flashlight", "🔦"}, {"high_brightness", "🔆"}, {"low_brightness", "🔅"}, {"electric_plug", "🔌"}, {"battery", "🔋"}, {"mag", "🔍"}, {"bathtub", "🛁"}, {"bath", "🛀"}, {"shower", "🚿"}, {"toilet", "🚽"}, {"wrench", "🔧"}, {"nut_and_bolt", "🔩"}, {"hammer", "🔨"}, {"door", "🚪"}, {"smoking", "🚬"}, {"bomb", "💣"}, {"gun", "🔫"}, {"hocho", "🔪"}, {"knife", "🔪"}, {"pill", "💊"}, {"syringe", "💉"}, {"moneybag", "💰"}, {"yen", "💴"}, {"dollar", "💵"}, {"pound", "💷"}, {"euro", "💶"}, {"credit_card", "💳"}, {"money_with_wings", "💸"}, {"calling", "📲"}, {"e-mail", "📧"}, {"inbox_tray", "📥"}, {"outbox_tray", "📤"}, {"email", "✉️"}, {"envelope", "✉️"}, {"envelope_with_arrow", "📩"}, {"incoming_envelope", "📨"}, {"postal_horn", "📯"}, {"mailbox", "📫"}, {"mailbox_closed", "📪"}, {"mailbox_with_mail", "📬"}, {"mailbox_with_no_mail", "📭"}, {"postbox", "📮"}, {"package", "📦"}, {"memo", "📝"}, {"pencil", "📝"}, {"page_facing_up", "📄"}, {"page_with_curl", "📃"}, {"bookmark_tabs", "📑"}, {"bar_chart", "📊"}, {"chart_with_upwards_trend", "📈"}, {"chart_with_downwards_trend", "📉"}, {"scroll", "📜"}, {"clipboard", "📋"}, {"date", "📅"}, {"calendar", "📆"}, {"card_index", "📇"}, {"file_folder", "📁"}, {"open_file_folder", "📂"}, {"scissors", "✂️"}, {"pushpin", "📌"}, {"paperclip", "📎"}, {"black_nib", "✒️"}, {"pencil2", "✏️"}, {"straight_ruler", "📏"}, {"triangular_ruler", "📐"}, {"closed_book", "📕"}, {"green_book", "📗"}, {"blue_book", "📘"}, {"orange_book", "📙"}, {"notebook", "📓"}, {"notebook_with_decorative_cover", "📔"}, {"ledger", "📒"}, {"books", "📚"}, {"book", "📖"}, {"open_book", "📖"}, {"bookmark", "🔖"}, {"name_badge", "📛"}, {"microscope", "🔬"}, {"telescope", "🔭"}, {"newspaper", "📰"}, {"art", "🎨"}, {"clapper", "🎬"}, {"microphone", "🎤"}, {"headphones", "🎧"}, {"musical_score", "🎼"}, {"musical_note", "🎵"}, {"notes", "🎶"}, {"musical_keyboard", "🎹"}, {"violin", "🎻"}, {"trumpet", "🎺"}, {"saxophone", "🎷"}, {"guitar", "🎸"}, {"space_invader", "👾"}, {"video_game", "🎮"}, {"black_joker", "🃏"}, {"flower_playing_cards", "🎴"}, {"mahjong", "🀄"}, {"game_die", "🎲"}, {"dart", "🎯"}, {"football", "🏈"}, {"basketball", "🏀"}, {"soccer", "⚽"}, {"baseball", "⚾️"}, {"tennis", "🎾"}, {"8ball", "🎱"}, {"rugby_football", "🏉"}, {"bowling", "🎳"}, {"golf", "⛳"}, {"mountain_bicyclist", "🚵"}, {"bicyclist", "🚴"}, {"checkered_flag", "🏁"}, {"horse_racing", "🏇"}, {"trophy", "🏆"}, {"ski", "🎿"}, {"snowboarder", "🏂"}, {"swimmer", "🏊"}, {"surfer", "🏄"}, {"fishing_pole_and_fish", "🎣"}, {"coffee", "☕"}, {"tea", "🍵"}, {"sake", "🍶"}, {"baby_bottle", "🍼"}, {"beer", "🍺"}, {"beers", "🍻"}, {"cocktail", "🍸"}, {"tropical_drink", "🍹"}, {"wine_glass", "🍷"}, {"fork_and_knife", "🍴"}, {"pizza", "🍕"}, {"hamburger", "🍔"}, {"fries", "🍟"}, {"poultry_leg", "🍗"}, {"meat_on_bone", "🍖"}, {"spaghetti", "🍝"}, {"curry", "🍛"}, {"fried_shrimp", "🍤"}, {"bento", "🍱"}, {"sushi", "🍣"}, {"fish_cake", "🍥"}, {"rice_ball", "🍙"}, {"rice_cracker", "🍘"}, {"rice", "🍚"}, {"ramen", "🍜"}, {"stew", "🍲"}, {"oden", "🍢"}, {"dango", "🍡"}, {"egg", "🍳"}, {"bread", "🍞"}, {"doughnut", "🍩"}, {"custard", "🍮"}, {"icecream", "🍦"}, {"ice_cream", "🍨"}, {"shaved_ice", "🍧"}, {"birthday", "🎂"}, {"cake", "🍰"}, {"cookie", "🍪"}, {"chocolate_bar", "🍫"}, {"candy", "🍬"}, {"lollipop", "🍭"}, {"honey_pot", "🍯"}, {"apple", "🍎"}, {"green_apple", "🍏"}, {"tangerine", "🍊"}, {"lemon", "🍋"}, {"cherries", "🍒"}, {"grapes", "🍇"}, {"watermelon", "🍉"}, {"strawberry", "🍓"}, {"peach", "🍑"}, {"melon", "🍈"}, {"banana", "🍌"}, {"pear", "🍐"}, {"pineapple", "🍍"}, {"sweet_potato", "🍠"}, {"eggplant", "🍆"}, {"tomato", "🍅"}, {"corn", "🌽"}, {"house", "🏠"}, {"house_with_garden", "🏡"}, {"school", "🏫"}, {"office", "🏢"}, {"post_office", "🏣"}, {"hospital", "🏥"}, {"bank", "🏦"}, {"convenience_store", "🏪"}, {"love_hotel", "🏩"}, {"hotel", "🏨"}, {"wedding", "💒"}, {"church", "⛪"}, {"department_store", "🏬"}, {"european_post_office", "🏤"}, {"city_sunrise", "🌇"}, {"city_sunset", "🌆"}, {"japanese_castle", "🏯"}, {"european_castle", "🏰"}, {"tent", "⛺"}, {"factory", "🏭"}, {"tokyo_tower", "🗼"}, {"japan", "🗾"}, {"mount_fuji", "🗻"}, {"sunrise_over_mountains", "🌄"}, {"sunrise", "🌅"}, {"night_with_stars", "🌃"}, {"statue_of_liberty", "🗽"}, {"bridge_at_night", "🌉"}, {"carousel_horse", "🎠"}, {"ferris_wheel", "🎡"}, {"fountain", "⛲"}, {"roller_coaster", "🎢"}, {"ship", "🚢"}, {"boat", "⛵"}, {"sailboat", "⛵"}, {"speedboat", "🚤"}, {"rowboat", "🚣"}, {"anchor", "⚓"}, {"rocket", "🚀"}, {"airplane", "✈️"}, {"seat", "💺"}, {"helicopter", "🚁"}, {"steam_locomotive", "🚂"}, {"tram", "🚊"}, {"station", "🚉"}, {"mountain_railway", "🚞"}, {"train2", "🚆"}, {"bullettrain_side", "🚄"}, {"bullettrain_front", "🚅"}, {"light_rail", "🚈"}, {"metro", "🚇"}, {"monorail", "🚝"}, {"train", "🚋"}, {"railway_car", "🚃"}, {"trolleybus", "🚎"}, {"bus", "🚌"}, {"oncoming_bus", "🚍"}, {"blue_car", "🚙"}, {"oncoming_automobile", "🚘"}, {"car", "🚗"}, {"red_car", "🚗"}, {"taxi", "🚕"}, {"oncoming_taxi", "🚖"}, {"articulated_lorry", "🚛"}, {"truck", "🚚"}, {"rotating_light", "🚨"}, {"police_car", "🚓"}, {"oncoming_police_car", "🚔"}, {"fire_engine", "🚒"}, {"ambulance", "🚑"}, {"minibus", "🚐"}, {"bike", "🚲"}, {"aerial_tramway", "🚡"}, {"suspension_railway", "🚟"}, {"mountain_cableway", "🚠"}, {"tractor", "🚜"}, {"barber", "💈"}, {"busstop", "🚏"}, {"ticket", "🎫"}, {"vertical_traffic_light", "🚦"}, {"traffic_light", "🚥"}, {"warning", "⚠️"}, {"construction", "🚧"}, {"beginner", "🔰"}, {"fuelpump", "⛽"}, {"izakaya_lantern", "🏮"}, {"lantern", "🏮"}, {"slot_machine", "🎰"}, {"hotsprings", "♨️"}, {"moyai", "🗿"}, {"circus_tent", "🎪"}, {"performing_arts", "🎭"}, {"round_pushpin", "📍"}, {"triangular_flag_on_post", "🚩"}, {"jp", "🇯🇵"}, {"kr", "🇰🇷"}, {"de", "🇩🇪"}, {"cn", "🇨🇳"}, {"us", "🇺🇸"}, {"fr", "🇫🇷"}, {"es", "🇪🇸"}, {"it", "🇮🇹"}, {"ru", "🇷🇺"}, {"gb", "🇬🇧"}, {"uk", "🇬🇧"}, {"one", "1️⃣"}, {"two", "2️⃣"}, {"three", "3️⃣"}, {"four", "4️⃣"}, {"five", "5️⃣"}, {"six", "6️⃣"}, {"seven", "7️⃣"}, {"eight", "8️⃣"}, {"nine", "9️⃣"}, {"zero", "0️⃣"}, {"keycap_ten", "🔟"}, {"1234", "🔢"}, {"hash", "#️⃣"}, {"symbols", "🔣"}, {"arrow_up", "⬆️"}, {"arrow_down", "⬇️"}, {"arrow_left", "⬅️"}, {"arrow_right", "➡️"}, {"capital_abcd", "🔠"}, {"abcd", "🔡"}, {"abc", "🔤"}, {"arrow_upper_right", "↗️"}, {"arrow_upper_left", "↖️"}, {"arrow_lower_right", "↘️"}, {"arrow_lower_left", "↙️"}, {"left_right_arrow", "↔️"}, {"arrow_up_down", "↕️"}, {"arrows_counterclockwise", "🔄"}, {"arrow_backward", "◀️"}, {"arrow_forward", "▶️"}, {"arrow_up_small", "🔼"}, {"arrow_down_small", "🔽"}, {"leftwards_arrow_with_hook", "↩️"}, {"arrow_right_hook", "↪️"}, {"information_source", "ℹ️"}, {"rewind", "⏪"}, {"fast_forward", "⏩"}, {"arrow_double_up", "⏫"}, {"arrow_double_down", "⏬"}, {"arrow_heading_down", "⤵️"}, {"arrow_heading_up", "⤴️"}, {"ok", "🆗"}, {"twisted_rightwards_arrows", "🔀"}, {"repeat", "🔁"}, {"repeat_one", "🔂"}, {"new", "🆕"}, {"up", "🆙"}, {"cool", "🆒"}, {"free", "🆓"}, {"ng", "🆖"}, {"signal_strength", "📶"}, {"cinema", "🎦"}, {"koko", "🈁"}, {"u6307", "🈯"}, {"u7a7a", "🈳"}, {"u6e80", "🈵"}, {"u5408", "🈴"}, {"u7981", "🈲"}, {"ideograph_advantage", "🉐"}, {"u5272", "🈹"}, {"u55b6", "🈺"}, {"u6709", "🈶"}, {"u7121", "🈚"}, {"restroom", "🚻"}, {"mens", "🚹"}, {"womens", "🚺"}, {"baby_symbol", "🚼"}, {"wc", "🚾"}, {"potable_water", "🚰"}, {"put_litter_in_its_place", "🚮"}, {"parking", "🅿️"}, {"wheelchair", "♿"}, {"no_smoking", "🚭"}, {"u6708", "🈷️"}, {"u7533", "🈸"}, {"sa", "🈂️"}, {"m", "Ⓜ️"}, {"passport_control", "🛂"}, {"baggage_claim", "🛄"}, {"left_luggage", "🛅"}, {"customs", "🛃"}, {"accept", "🉑"}, {"secret", "㊙️"}, {"congratulations", "㊗️"}, {"cl", "🆑"}, {"sos", "🆘"}, {"id", "🆔"}, {"no_entry_sign", "🚫"}, {"underage", "🔞"}, {"no_mobile_phones", "📵"}, {"do_not_litter", "🚯"}, {"non-potable_water", "🚱"}, {"no_bicycles", "🚳"}, {"no_pedestrians", "🚷"}, {"children_crossing", "🚸"}, {"no_entry", "⛔"}, {"eight_spoked_asterisk", "✳️"}, {"sparkle", "❇️"}, {"negative_squared_cross_mark", "❎"}, {"white_check_mark", "✅"}, {"eight_pointed_black_star", "✴️"}, {"heart_decoration", "💟"}, {"vs", "🆚"}, {"vibration_mode", "📳"}, {"mobile_phone_off", "📴"}, {"a", "🅰️"}, {"b", "🅱️"}, {"ab", "🆎"}, {"o2", "🅾️"}, {"diamond_shape_with_a_dot_inside", "💠"}, {"loop", "➿"}, {"recycle", "♻️"}, {"aries", "♈"}, {"taurus", "♉"}, {"gemini", "♊"}, {"cancer", "♋"}, {"leo", "♌"}, {"virgo", "♍"}, {"libra", "♎"}, {"scorpius", "♏"}, {"sagittarius", "♐"}, {"capricorn", "♑"}, {"aquarius", "♒"}, {"pisces", "♓"}, {"ophiuchus", "⛎"}, {"six_pointed_star", "🔯"}, {"atm", "🏧"}, {"chart", "💹"}, {"heavy_dollar_sign", "💲"}, {"currency_exchange", "💱"}, {"copyright", "©️"}, {"registered", "®️"}, {"tm", "™️"}, {"x", "❌"}, {"bangbang", "‼️"}, {"interrobang", "⁉️"}, {"exclamation", "❗"}, {"heavy_exclamation_mark", "❗"}, {"question", "❓"}, {"grey_exclamation", "❕"}, {"grey_question", "❔"}, {"o", "⭕"}, {"top", "🔝"}, {"end", "🔚"}, {"back", "🔙"}, {"on", "🔛"}, {"soon", "🔜"}, {"arrows_clockwise", "🔃"}, {"clock12", "🕛"}, {"clock1230", "🕧"}, {"clock1", "🕐"}, {"clock130", "🕜"}, {"clock2", "🕑"}, {"clock230", "🕝"}, {"clock3", "🕒"}, {"clock330", "🕞"}, {"clock4", "🕓"}, {"clock430", "🕟"}, {"clock5", "🕔"}, {"clock530", "🕠"}, {"clock6", "🕕"}, {"clock7", "🕖"}, {"clock8", "🕗"}, {"clock9", "🕘"}, {"clock10", "🕙"}, {"clock11", "🕚"}, {"clock630", "🕡"}, {"clock730", "🕢"}, {"clock830", "🕣"}, {"clock930", "🕤"}, {"clock1030", "🕥"}, {"clock1130", "🕦"}, {"heavy_multiplication_x", "✖️"}, {"heavy_plus_sign", "➕"}, {"heavy_minus_sign", "➖"}, {"heavy_division_sign", "➗"}, {"spades", "♠️"}, {"hearts", "♥️"}, {"clubs", "♣️"}, {"diamonds", "♦️"}, {"white_flower", "💮"}, {"100", "💯"}, {"heavy_check_mark", "✔️"}, {"ballot_box_with_check", "☑️"}, {"radio_button", "🔘"}, {"link", "🔗"}, {"curly_loop", "➰"}, {"wavy_dash", "〰️"}, {"part_alternation_mark", "〽️"}, {"trident", "🔱"}, {"black_medium_square", "◼️"}, {"white_medium_square", "◻️"}, {"black_medium_small_square", "◾"}, {"white_medium_small_square", "◽"}, {"black_small_square", "▪️"}, {"white_small_square", "▫️"}, {"small_red_triangle", "🔺"}, {"black_square_button", "🔲"}, {"white_square_button", "🔳"}, {"black_circle", "⚫"}, {"white_circle", "⚪"}, {"red_circle", "🔴"}, {"large_blue_circle", "🔵"}, {"small_red_triangle_down", "🔻"}, {"white_large_square", "⬜"}, {"black_large_square", "⬛"}, {"large_orange_diamond", "🔶"}, {"large_blue_diamond", "🔷"}, {"small_orange_diamond", "🔸"}, {"small_blue_diamond", "🔹"} }; } public static string FindAndReplace(string text) { if (string.IsNullOrEmpty(text)) return text; var match = Regex.Match(text, @":(\w+):"); if (match.Success) { for (var i = 1; i < match.Groups.Count; i++) { string emoji; if (_emojis.TryGetValue(match.Groups[i].Value, out emoji)) text = text.Replace(":" + match.Groups[i].Value + ":", emoji); } } return text; } } } ================================================ FILE: CodeHub.Core/Utils/FilterGroup.cs ================================================ using System.Collections.Generic; using System.Linq; using CodeHub.Core.ViewModels; namespace CodeHub.Core.Utils { public static class FilterGroup { public static int[] IntegerCeilings = { 6, 11, 21, 31, 41, 51, 61, 71, 81, 91, 101, 251, 501, 1001, 2001, 4001, 8001, 16001, int.MaxValue }; private static string CreateRangeString(int key) { return IntegerCeilings.LastOrDefault(x => x < key) + " to " + (key - 1); } public static List> CreateNumberedGroup(IEnumerable> results, string title, string prefix = null) { return results.Select(x => { var text = (prefix != null ? prefix + " " : "") + CreateRangeString(x.Key) + " " + title; return (IGrouping)new FilterGroup(text, x.ToList()); }).ToList(); } } } ================================================ FILE: CodeHub.Core/Utils/GitHubAvatar.cs ================================================ using System; using CodeHub.Core.Utilities; namespace CodeHub.Core.Utilities { public class GitHubAvatar { public string AvatarUrl { get; } public static GitHubAvatar Empty { get { return new GitHubAvatar((string)null); } } public GitHubAvatar(string avatarUrl) { AvatarUrl = avatarUrl; } public GitHubAvatar(Uri avatarUri) { AvatarUrl = avatarUri?.AbsoluteUri; } } } public static class GitHubAvatarExtensions { public static Uri ToUri(this GitHubAvatar @this, int? size = null) { if (@this == null || @this.AvatarUrl == null) return null; try { var baseUri = new UriBuilder(@this.AvatarUrl); if (size == null) return baseUri.Uri; var queryToAppend = "size=" + size.Value; if (baseUri.Query != null && baseUri.Query.Length > 1) baseUri.Query = baseUri.Query.Substring(1) + "&" + queryToAppend; else baseUri.Query = queryToAppend; return baseUri.Uri; } catch { return null; } } } ================================================ FILE: CodeHub.Core/Utils/GitHubExtensions.cs ================================================ using System; using GitHubSharp.Models; using System.Text; using System.Security.Cryptography; public static class GitHubExtensions { private const string GitHubDefaultGravitar = "https%3A%2F%2Fassets-cdn.github.com%2Fimages%2Fgravatars%2Fgravatar-user-420.png&r=x&s=140"; public static string GenerateCommiterName(this CommitModel x) { if (x.Commit.Author != null && !string.IsNullOrEmpty(x.Commit.Author.Name)) return x.Commit.Author.Name; if (x.Commit.Committer != null && !string.IsNullOrEmpty(x.Commit.Committer.Name)) return x.Commit.Committer.Name; if (x.Author != null) return x.Author.Login; return x.Committer != null ? x.Committer.Login : "Unknown"; } public static Uri GenerateGravatarUrl(this CommitModel x) { if (x == null) return null; try { if (x.Author != null && !string.IsNullOrEmpty(x.Author.AvatarUrl)) return new Uri(x.Author.AvatarUrl); var inputBytes = Encoding.UTF8.GetBytes(x.Commit.Author.Email.Trim().ToLower()); var hash = MD5.Create().ComputeHash(inputBytes); var sb = new StringBuilder(); for (int i = 0; i < hash.Length; i++) sb.Append(hash[i].ToString("x2")); return new Uri(string.Format("http://www.gravatar.com/avatar/{0}?d={1}", sb, GitHubDefaultGravitar)); } catch { return null; } } } ================================================ FILE: CodeHub.Core/Utils/GitHubList.cs ================================================ using System; using System.Collections.Generic; using System.Threading.Tasks; using Octokit; namespace CodeHub.Core.Utils { public class GitHubList { private readonly GitHubClient _client; private Uri _uri; private readonly IDictionary _parameters; public GitHubList( GitHubClient client, Uri uri, IDictionary parameters = null) { _client = client; _uri = uri; _parameters = parameters; } public async Task> Next() { if (_uri == null) return null; var ret = await _client.Connection.Get>( _uri, _parameters, "application/json"); _uri = ret.HttpResponse.ApiInfo.Links.ContainsKey("next") ? ret.HttpResponse.ApiInfo.Links["next"] : null; return ret.Body; } } } ================================================ FILE: CodeHub.Core/Utils/OctokitClientFactory.cs ================================================ using System; using Octokit.Internal; using Splat; using Octokit; using System.Net.Http; using CodeHub.Core.Services; namespace CodeHub.Core.Utilities { public static class OctokitClientFactory { public static Func CreateMessageHandler = () => new HttpClientHandler(); public static readonly string[] Scopes = { "user", "repo", "gist", "notifications" }; public static readonly ProductHeaderValue UserAgent = new ProductHeaderValue("CodeHub"); public static GitHubClient Create( Uri domain, Credentials credentials, TimeSpan? requestTimeout = null) { var networkActivityService = Locator.Current.GetService(); var client = new HttpClientAdapter(CreateMessageHandler); var httpClient = new OctokitNetworkClient(client, networkActivityService); var connection = new Connection( UserAgent, domain, new InMemoryCredentialStore(credentials), httpClient, new SimpleJsonSerializer()); var gitHubClient = new GitHubClient(connection); gitHubClient.SetRequestTimeout(requestTimeout ?? TimeSpan.FromSeconds(20)); return gitHubClient; } } } ================================================ FILE: CodeHub.Core/Utils/OctokitNetworkClient.cs ================================================ using Octokit.Internal; using Octokit; using System.Threading.Tasks; using CodeHub.Core.Services; using System; namespace CodeHub.Core.Utilities { /// /// A decorator class for the object which will /// trigger the network activity spinner /// class OctokitNetworkClient : IHttpClient { private readonly IHttpClient _httpClient; private readonly INetworkActivityService _networkActivity; public OctokitNetworkClient(IHttpClient httpClient, INetworkActivityService networkActivity) { _httpClient = httpClient; _networkActivity = networkActivity; } public async Task Send(IRequest request, System.Threading.CancellationToken cancellationToken) { using (_networkActivity.ActivateNetwork()) return await _httpClient.Send(request, cancellationToken); } public void Dispose() => _httpClient.Dispose(); public void SetRequestTimeout(TimeSpan timeout) => _httpClient.SetRequestTimeout(timeout); } } ================================================ FILE: CodeHub.Core/Utils/RepositoryIdentifier.cs ================================================ using System.Linq; namespace CodeHub.Core.Utils { public class RepositoryIdentifier { public string Owner { get; } public string Name { get; } public RepositoryIdentifier(string owner, string name) { Owner = owner; Name = name; } public static RepositoryIdentifier FromFullName(string id) { var split = id.Split(new [] { '/' }, 2); if (split.Length != 2 || split.Any(string.IsNullOrEmpty)) return null; return new RepositoryIdentifier(split[0], split[1]); } } } ================================================ FILE: CodeHub.Core/Utils/ViewModelExtensions.cs ================================================ using CodeHub.Core.Services; using CodeHub.Core.ViewModels; using MvvmCross.Platform; public static class ViewModelExtensions { public static IApplicationService GetApplication(this BaseViewModel vm) { return Mvx.Resolve(); } } ================================================ FILE: CodeHub.Core/Utils/WeakReferenceExtensions.cs ================================================ using System; public static class WeakReferenceExtensions { public static T Get(this WeakReference @this) where T : class { T t; @this.TryGetTarget(out t); return t; } } ================================================ FILE: CodeHub.Core/ViewModels/Accounts/AddAccountViewModel.cs ================================================ using System; using System.Threading.Tasks; using CodeHub.Core.Services; using CodeHub.Core.Messages; using System.Reactive; using ReactiveUI; using Splat; using System.Reactive.Linq; using GitHubSharp; using System.Reactive.Threading.Tasks; namespace CodeHub.Core.ViewModels.Accounts { public class AddAccountViewModel : ReactiveObject { private readonly ILoginService _loginService; private readonly IAlertDialogService _alertDialogService; private string _username; public string Username { get { return _username; } set { this.RaiseAndSetIfChanged(ref _username, value); } } private string _password; public string Password { get { return _password; } set { this.RaiseAndSetIfChanged(ref _password, value); } } private string _domain; public string Domain { get { return _domain; } set { this.RaiseAndSetIfChanged(ref _domain, value); } } private string _token; public string Token { get { return _token; } set { this.RaiseAndSetIfChanged(ref _token, value); } } private bool _tokenAuthentication; public bool TokenAuthentication { get { return _tokenAuthentication; } set { this.RaiseAndSetIfChanged(ref _tokenAuthentication, value); } } public string TwoFactor { get; set; } public ReactiveCommand LoginCommand { get; } public AddAccountViewModel( ILoginService loginService = null, IAlertDialogService alertDialogService = null) { _loginService = loginService ?? Locator.Current.GetService(); _alertDialogService = alertDialogService ?? Locator.Current.GetService(); LoginCommand = ReactiveCommand.CreateFromTask(Login); LoginCommand .Subscribe(x => MessageBus.Current.SendMessage(new LogoutMessage())); LoginCommand .ThrownExceptions .SelectMany(HandleLoginException) .SelectMany(Interactions.Errors.Handle) .Subscribe(); } private IObservable HandleLoginException(Exception e) { TwoFactor = null; if (e is Octokit.TwoFactorRequiredException) { _alertDialogService .PromptTextBox("Authentication Error", "Please provide the two-factor authentication code for this account.", string.Empty, "Login") .ToObservable() .Do(x => TwoFactor = x) .Select(_ => Unit.Default) .InvokeReactiveCommand(LoginCommand); return Observable.Empty(); } if (e is Octokit.NotFoundException err) { return Observable.Return( new UserError($"The provided domain was incorrect. The host could not be found.")); } if (e is Octokit.ForbiddenException && TokenAuthentication) { return Observable.Return( new UserError("The provided token is invalid! Please try again or " + "create a new token as this one might have been revoked.")); } return Observable.Return(new UserError("Unable to login!", e)); } private async Task Login() { if (string.IsNullOrEmpty(Domain)) throw new ArgumentException("Must have a valid GitHub domain"); if (!Uri.TryCreate(Domain, UriKind.Absolute, out Uri domainUri)) throw new Exception("The provided domain is not a valid URL."); var apiUrl = Domain; if (apiUrl != null) { if (!apiUrl.EndsWith("/", StringComparison.Ordinal)) apiUrl += "/"; if (!apiUrl.Contains("/api/")) apiUrl += "api/v3/"; } if (TokenAuthentication) { var trimmedToken = Token?.Trim() ?? string.Empty; if (string.IsNullOrEmpty(trimmedToken)) throw new ArgumentException("Must have a valid token"); await _loginService.LoginWithToken(apiUrl, Domain, trimmedToken, true); } else { if (string.IsNullOrEmpty(Username)) throw new ArgumentException("Must have a valid username"); if (string.IsNullOrEmpty(Password)) throw new ArgumentException("Must have a valid password"); await _loginService.LoginWithBasic(apiUrl, Username, Password, TwoFactor); } } } } ================================================ FILE: CodeHub.Core/ViewModels/Accounts/OAuthLoginViewModel.cs ================================================ using System; using ReactiveUI; using CodeHub.Core.Messages; using CodeHub.Core.Services; using Splat; using System.Reactive; namespace CodeHub.Core.ViewModels.Accounts { public class OAuthLoginViewModel : ReactiveObject { public static readonly string RedirectUri = "http://dillonbuchanan.com/"; private readonly ILoginService _loginService; private readonly IAlertDialogService _alertDialogService; public string LoginUrl { get { var web = WebDomain.TrimEnd('/'); return string.Format( web + "/login/oauth/authorize?client_id={0}&redirect_uri={1}&scope={2}", Secrets.GithubOAuthId, Uri.EscapeDataString(OAuthLoginViewModel.RedirectUri), Uri.EscapeDataString("user:follow,repo,notifications,gist,read:org")); } } public string WebDomain { get; set; } = GitHubSharp.Client.AccessTokenUri; public ReactiveCommand LoginCommand { get; } public OAuthLoginViewModel( ILoginService loginService = null, IAlertDialogService alertDialogService = null) { _loginService = loginService ?? Locator.Current.GetService(); _alertDialogService = alertDialogService ?? Locator.Current.GetService(); LoginCommand = ReactiveCommand.CreateFromTask(async code => { await _loginService.LoginWithToken( Secrets.GithubOAuthId, Secrets.GithubOAuthSecret, code, RedirectUri, WebDomain, GitHubSharp.Client.DefaultApi); MessageBus.Current.SendMessage(new LogoutMessage()); }); LoginCommand .ThrownExceptions .Subscribe(err => _alertDialogService.Alert("Error!", err.Message).ToBackground()); } } } ================================================ FILE: CodeHub.Core/ViewModels/App/FeedbackComposerViewModel.cs ================================================ using ReactiveUI; using System.Reactive; using CodeHub.Core.Services; using System; using System.Linq; using System.Reactive.Linq; using Humanizer; using Splat; namespace CodeHub.Core.ViewModels.App { public class FeedbackComposerViewModel : ReactiveObject { private const string CodeHubOwner = "codehubapp"; private const string CodeHubName = "codehub"; private string _subject; public string Subject { get { return _subject; } set { this.RaiseAndSetIfChanged(ref _subject, value); } } private string _description; public string Description { get { return _description; } set { this.RaiseAndSetIfChanged(ref _description, value); } } public string Title => "Open Issue"; public ReactiveCommand SubmitCommand { get; private set; } public ReactiveCommand DismissCommand { get; private set; } public FeedbackComposerViewModel( IApplicationService applicationService = null, IAlertDialogService alertDialogService = null) { applicationService = applicationService ?? Locator.Current.GetService(); alertDialogService = alertDialogService ?? Locator.Current.GetService(); SubmitCommand = ReactiveCommand.CreateFromTask(async _ => { if (string.IsNullOrEmpty(Subject)) throw new ArgumentException("You must provide a title for this issue!"); var createIssueRequest = new Octokit.NewIssue(Subject) { Body = Description }; await applicationService.GitHubClient.Issue.Create(CodeHubOwner, CodeHubName, createIssueRequest); }, this.WhenAnyValue(x => x.Subject).Select(x => !string.IsNullOrEmpty(x))); SubmitCommand .ThrownExceptions .Select(ex => new UserError("There was a problem trying to post your feedback.", ex)) .SelectMany(Interactions.Errors.Handle) .Subscribe(); DismissCommand = ReactiveCommand.CreateFromTask(async t => { if (string.IsNullOrEmpty(Description) && string.IsNullOrEmpty(Subject)) return true; return await alertDialogService.PromptYesNo( "Discard Issue?", "Are you sure you want to discard this issue?"); }); } } } ================================================ FILE: CodeHub.Core/ViewModels/App/FeedbackItemViewModel.cs ================================================ using System; using ReactiveUI; using Humanizer; using System.Reactive; namespace CodeHub.Core.ViewModels.App { public class FeedbackItemViewModel : ReactiveObject, ICanGoToViewModel { public string Title { get; } public string ImageUrl { get; } public ReactiveCommand GoToCommand { get; } = ReactiveCommand.Create(() => { }); public DateTimeOffset Created { get; } public string CreatedString { get; } public string RepositoryName { get; } public string RepositoryOwner { get; } public int IssueId { get; } internal FeedbackItemViewModel(string repositoryOwner, string repositoryName, Octokit.Issue issue) { RepositoryOwner = repositoryOwner; RepositoryName = repositoryName; IssueId = issue.Number; Title = issue.Title; ImageUrl = issue.User.AvatarUrl; Created = issue.CreatedAt; CreatedString = Created.Humanize(); } } } ================================================ FILE: CodeHub.Core/ViewModels/App/FeedbackViewModel.cs ================================================ using System; using ReactiveUI; using CodeHub.Core.Services; using System.Reactive; using Splat; using Octokit; namespace CodeHub.Core.ViewModels.App { public class FeedbackViewModel : ReactiveObject, IProvidesSearchKeyword { private const string CodeHubOwner = "codehubapp"; private const string CodeHubName = "codehub"; public IReadOnlyReactiveList Items { get; } private string _searchKeyword; public string SearchKeyword { get { return _searchKeyword; } set { this.RaiseAndSetIfChanged(ref _searchKeyword, value); } } private bool? _isEmpty; public bool? IsEmpty { get { return _isEmpty; } set { this.RaiseAndSetIfChanged(ref _isEmpty, value); } } public string Title => "Feedback"; public ReactiveCommand LoadCommand { get; private set; } public FeedbackViewModel(IApplicationService applicationService = null) { applicationService = applicationService ?? Locator.Current.GetService(); var items = new ReactiveList(resetChangeThreshold: 1); var feedbackItems = items.CreateDerivedCollection( x => new FeedbackItemViewModel(CodeHubOwner, CodeHubName, x)); Items = feedbackItems.CreateDerivedCollection( x => x, x => x.Title.ContainsKeyword(SearchKeyword), signalReset: this.WhenAnyValue(x => x.SearchKeyword)); LoadCommand = ReactiveCommand.CreateFromTask(async _ => { items.Clear(); var list = applicationService.GitHubClient.RetrieveList( ApiUrls.Issues(CodeHubOwner, CodeHubName)); var issues = await list.Next(); IsEmpty = issues?.Count > 0; while (issues != null) { items.AddRange(issues); issues = await list.Next(); } }); } } } ================================================ FILE: CodeHub.Core/ViewModels/App/MenuViewModel.cs ================================================ using System.Collections.Generic; using System.Windows.Input; using CodeHub.Core.Data; using CodeHub.Core.Services; using CodeHub.Core.ViewModels.Events; using CodeHub.Core.ViewModels.Issues; using CodeHub.Core.ViewModels.User; using System.Linq; using CodeHub.Core.Messages; using CodeHub.Core.ViewModels.Notifications; using GitHubSharp.Models; using MvvmCross.Core.ViewModels; using System; namespace CodeHub.Core.ViewModels.App { public class MenuViewModel : BaseViewModel { private readonly IApplicationService _applicationService; private readonly IFeaturesService _featuresService; private int _notifications; private List _organizations; private readonly IDisposable _notificationCountToken; public int Notifications { get { return _notifications; } set { _notifications = value; RaisePropertyChanged(); } } public List Organizations { get { return _organizations; } set { _organizations = value; RaisePropertyChanged(); } } public Account Account { get { return _applicationService.Account; } } public bool ShouldShowUpgrades { get { return !_featuresService.IsProEnabled; } } public MenuViewModel(IApplicationService application = null, IFeaturesService featuresService = null, IMessageService messageService = null) { _applicationService = application ?? GetService(); _featuresService = featuresService ?? GetService(); messageService = messageService ?? GetService(); _notificationCountToken = messageService.Listen(OnNotificationCountMessage); } private void OnNotificationCountMessage(NotificationCountMessage msg) { Notifications = msg.Count; } public ICommand GoToProfileCommand { get { return new MvxCommand(() => ShowMenuViewModel(new UserViewModel.NavObject { Username = _applicationService.Account.Username })); } } public ICommand GoToNotificationsCommand { get { return new MvxCommand(() => ShowMenuViewModel(null)); } } public ICommand GoToMyIssuesCommand { get { return new MvxCommand(() => ShowMenuViewModel(null)); } } public ICommand GoToMyEvents { get { return new MvxCommand(() => ShowMenuViewModel(new UserEventsViewModel.NavObject { Username = Account.Username })); } } public ICommand GoToOrganizationEventsCommand { get { return new MvxCommand(x => ShowMenuViewModel(new Events.UserEventsViewModel.NavObject { Username = x }));} } public ICommand GoToOrganizationCommand { get { return new MvxCommand(x => ShowMenuViewModel(new Organizations.OrganizationViewModel.NavObject { Name = x }));} } public ICommand GoToOrganizationsCommand { get { return new MvxCommand(() => ShowMenuViewModel(new Organizations.OrganizationsViewModel.NavObject { Username = Account.Username }));} } public ICommand GoToNewsCommand { get { return new MvxCommand(() => ShowMenuViewModel(null));} } public ICommand LoadCommand { get { return new MvxCommand(Load);} } private void Load() { var notificationRequest = this.GetApplication().Client.Notifications.GetAll(); this.GetApplication().Client.ExecuteAsync(notificationRequest) .ToBackground(x => Notifications = x.Data.Count); var organizationsRequest = this.GetApplication().Client.AuthenticatedUser.GetOrganizations(); this.GetApplication().Client.ExecuteAsync(organizationsRequest) .ToBackground(x => Organizations = x.Data.ToList()); } // // private async Task PromptForPushNotifications() // { // // Push notifications are not enabled for enterprise // if (Account.IsEnterprise) // return; // // try // { // var features = Mvx.Resolve(); // var alertDialog = Mvx.Resolve(); // var push = Mvx.Resolve(); // var // // Check for push notifications // if (Account.IsPushNotificationsEnabled == null && features.IsPushNotificationsActivated) // { // var result = await alertDialog.PromptYesNo("Push Notifications", "Would you like to enable push notifications for this account?"); // if (result) // Task.Run(() => push.Register()).FireAndForget(); // Account.IsPushNotificationsEnabled = result; // Accounts.Update(Account); // } // else if (Account.IsPushNotificationsEnabled.HasValue && Account.IsPushNotificationsEnabled.Value) // { // Task.Run(() => push.Register()).FireAndForget(); // } // } // catch (Exception e) // { // _alertDialogService.Alert("Error", e.Message); // } // } private static readonly IDictionary Presentation = new Dictionary { { PresentationValues.SlideoutRootPresentation, string.Empty } }; public ICommand DeletePinnedRepositoryCommand { get { return new MvxCommand(x => { Account.PinnedRepositories.Remove(x); _applicationService.UpdateActiveAccount().ToBackground(); }, x => x != null); } } protected bool ShowMenuViewModel(object data) where T : IMvxViewModel { return this.ShowViewModel(data, new MvxBundle(Presentation)); } public IEnumerable PinnedRepositories { get { return Account.PinnedRepositories; } } } } ================================================ FILE: CodeHub.Core/ViewModels/App/StartupViewModel.cs ================================================ using System; using CodeHub.Core.Services; using System.Linq; using System.Windows.Input; using Dumb = MvvmCross.Core.ViewModels; using System.Threading.Tasks; using ReactiveUI; using System.Reactive.Threading.Tasks; using System.Reactive; namespace CodeHub.Core.ViewModels.App { public class StartupViewModel : BaseViewModel { private bool _isLoggingIn; private string _status; private Uri _imageUrl; private readonly IApplicationService _applicationService; private readonly IAccountsService _accountsService; public bool IsLoggingIn { get { return _isLoggingIn; } private set { this.RaiseAndSetIfChanged(ref _isLoggingIn, value); } } public string Status { get { return _status; } private set { this.RaiseAndSetIfChanged(ref _status, value); } } public Uri ImageUrl { get { return _imageUrl; } private set { this.RaiseAndSetIfChanged(ref _imageUrl, value); } } public ICommand StartupCommand { get { return new Dumb.MvxAsyncCommand(Startup); } } public Data.Account Account => _applicationService.Account; public ReactiveCommand GoToMenu { get; } = ReactiveCommand.Create(() => { }); public ReactiveCommand GoToAccounts { get; } = ReactiveCommand.Create(() => { }); public ReactiveCommand GoToNewAccount { get; } = ReactiveCommand.Create(() => { }); public StartupViewModel( IApplicationService applicationService = null, IAccountsService accountsService = null) { _applicationService = applicationService ?? GetService(); _accountsService = accountsService ?? GetService(); } protected async Task Startup() { var accounts = (await _accountsService.GetAccounts()).ToList(); if (!accounts.Any()) { GoToNewAccount.ExecuteNow(); return; } var account = await _accountsService.GetActiveAccount(); if (account == null) { GoToAccounts.ExecuteNow(); return; } var isEnterprise = account.IsEnterprise || !string.IsNullOrEmpty(account.Password); //Lets login! try { ImageUrl = null; Status = null; IsLoggingIn = true; Uri accountAvatarUri = null; Uri.TryCreate(account.AvatarUrl, UriKind.Absolute, out accountAvatarUri); ImageUrl = accountAvatarUri; Status = "Logging in as " + account.Username; await _applicationService.LoginAccount(account); if (!isEnterprise) StarOrWatch(); GoToMenu.ExecuteNow(); } catch (Octokit.AuthorizationException e) { DisplayAlertAsync("The credentials for the selected account are not valid. " + e.Message) .ToObservable() .BindCommand(GoToAccounts); } catch (Exception e) { DisplayAlert(e.Message); GoToAccounts.ExecuteNow(); } finally { IsLoggingIn = false; } } private void StarOrWatch() { if (Settings.ShouldStar) { Settings.ShouldStar = false; _applicationService .GitHubClient.Activity.Starring .StarRepo("codehubapp", "codehub") .ToBackground(); } if (Settings.ShouldWatch) { Settings.ShouldWatch = false; var subscription = new Octokit.NewSubscription { Subscribed = true }; _applicationService .GitHubClient.Activity.Watching .WatchRepo("codehubapp", "codehub", subscription) .ToBackground(); } } } } ================================================ FILE: CodeHub.Core/ViewModels/App/SupportViewModel.cs ================================================ using System; using ReactiveUI; using CodeHub.Core.Services; using System.Reactive.Linq; using System.Reactive; using Splat; namespace CodeHub.Core.ViewModels.App { public class SupportViewModel : ReactiveObject, ILoadableViewModel { public readonly static string CodeHubOwner = "codehubapp"; public readonly static string CodeHubName = "codehub"; private int? _contributors; public int? Contributors { get { return _contributors; } private set { this.RaiseAndSetIfChanged(ref _contributors, value); } } public string Title => "Feedback & Support"; private readonly ObservableAsPropertyHelper _lastCommit; public DateTimeOffset? LastCommit => _lastCommit.Value; private Octokit.Repository _repository; public Octokit.Repository Repository { get { return _repository; } private set { this.RaiseAndSetIfChanged(ref _repository, value); } } public ReactiveCommand LoadCommand { get; } public SupportViewModel(IApplicationService applicationService = null) { applicationService = applicationService ?? Locator.Current.GetService(); _lastCommit = this .WhenAnyValue(x => x.Repository).Where(x => x != null) .Select(x => x.PushedAt).ToProperty(this, x => x.LastCommit); LoadCommand = ReactiveCommand.CreateFromTask(async _ => { applicationService.GitHubClient.Repository.GetAllContributors(CodeHubOwner, CodeHubName) .ToBackground(x => Contributors = x.Count); Repository = await applicationService.GitHubClient.Repository.Get(CodeHubOwner, CodeHubName); }); } } } ================================================ FILE: CodeHub.Core/ViewModels/BaseViewModel.cs ================================================ using CodeHub.Core.Services; using System.Windows.Input; using MvvmCross.Core.ViewModels; using MvvmCross.Platform; using ReactiveUI; using System; using System.Reactive; using System.Reactive.Subjects; using System.Threading.Tasks; namespace CodeHub.Core.ViewModels { public interface IProvidesTitle { string Title { get; } } public interface IRoutingViewModel { IObservable RequestNavigation { get; } IObservable RequestDismiss { get; } } public interface IBaseViewModel : ISupportsActivation, IProvidesTitle, IRoutingViewModel { } /// /// Defines the BaseViewModel type. /// public abstract class BaseViewModel : MvxViewModel, IBaseViewModel, IReactiveObject { private readonly ViewModelActivator _viewModelActivator = new ViewModelActivator(); private readonly ISubject _requestNavigationSubject = new Subject(); private readonly ISubject _requestDismissSubject = new Subject(); public event PropertyChangingEventHandler PropertyChanging; public void RaisePropertyChanging(PropertyChangingEventArgs args) { this.RaisePropertyChanged(args.PropertyName); } ViewModelActivator ISupportsActivation.Activator { get { return _viewModelActivator; } } private string _title; public string Title { get { return _title; } protected set { if (value != _title) { _title = value; this.RaisePropertyChanged(); } } } protected void NavigateTo(IBaseViewModel viewModel) { _requestNavigationSubject.OnNext(viewModel); } protected void Dismiss() { _requestDismissSubject.OnNext(Unit.Default); } IObservable IRoutingViewModel.RequestNavigation { get { return _requestNavigationSubject; } } IObservable IRoutingViewModel.RequestDismiss { get { return _requestDismissSubject; } } /// /// Gets the go to URL command. /// /// The go to URL command. public ICommand GoToUrlCommand { get { return new MvxCommand(x => ShowViewModel(new WebBrowserViewModel.NavObject { Url = x })); } } /// /// Gets the ViewModelTxService /// /// The tx sevice. protected IViewModelTxService TxSevice { get { return GetService(); } } /// /// Gets the alert service /// /// The alert service. protected IAlertDialogService AlertService { get { return GetService(); } } /// /// Gets the service. /// /// The type of the service. /// An instance of the service. protected TService GetService() where TService : class { return Mvx.Resolve(); } /// /// Display an error message to the user /// /// Message. protected void DisplayAlert(string message) { AlertService.Alert("Error!", message).ToBackground(); } /// /// Display an error message to the user /// /// Message. protected Task DisplayAlertAsync(string message) { return AlertService.Alert("Error!", message); } } } ================================================ FILE: CodeHub.Core/ViewModels/Changesets/ChangesetViewModel.cs ================================================ using System.Windows.Input; using MvvmCross.Core.ViewModels; using CodeHub.Core.Services; using CodeHub.Core.ViewModels.Repositories; using GitHubSharp.Models; using System.Threading.Tasks; using CodeHub.Core.ViewModels.Source; using MvvmCross.Platform; using System.Reactive.Linq; using CodeHub.Core.ViewModels.User; using System.Reactive; namespace CodeHub.Core.ViewModels.Changesets { public class ChangesetViewModel : LoadableViewModel { private readonly CollectionViewModel _comments = new CollectionViewModel(); private readonly IApplicationService _applicationService; private readonly IFeaturesService _featuresService; private CommitModel _commitModel; public string Node { get; private set; } public string User { get; private set; } public string Repository { get; private set; } public bool ShowRepository { get; private set; } public CommitModel Changeset { get { return _commitModel; } private set { this.RaiseAndSetIfChanged(ref _commitModel, value); } } private bool _shouldShowPro; public bool ShouldShowPro { get { return _shouldShowPro; } protected set { this.RaiseAndSetIfChanged(ref _shouldShowPro, value); } } public ICommand GoToRepositoryCommand { get { return new MvxCommand(() => ShowViewModel(new RepositoryViewModel.NavObject { Username = User, Repository = Repository })); } } public ICommand GoToHtmlUrlCommand { get { return new MvxCommand(() => ShowViewModel(new WebBrowserViewModel.NavObject { Url = _commitModel.Url }), () => _commitModel != null); } } public CollectionViewModel Comments { get { return _comments; } } public ReactiveUI.ReactiveCommand GoToOwner { get; } public ChangesetViewModel(IApplicationService application, IFeaturesService featuresService) { _applicationService = application; _featuresService = featuresService; GoToOwner = ReactiveUI.ReactiveCommand.Create( () => ShowViewModel(new UserViewModel.NavObject { Username = Changeset?.Author?.Login }), this.Bind(x => x.Changeset, true).Select(x => x?.Author?.Login != null)); } public void Init(NavObject navObject) { User = navObject.Username; Repository = navObject.Repository; Node = navObject.Node; ShowRepository = navObject.ShowRepository; Title = "Commit " + (Node.Length > 6 ? Node.Substring(0, 6) : Node); } protected override Task Load() { if (_featuresService.IsProEnabled) ShouldShowPro = false; else { var request = _applicationService.Client.Users[User].Repositories[Repository].Get(); _applicationService.Client.ExecuteAsync(request) .ToBackground(x => ShouldShowPro = x.Data.Private && !_featuresService.IsProEnabled); } var t1 = this.RequestModel(_applicationService.Client.Users[User].Repositories[Repository].Commits[Node].Get(), response => Changeset = response.Data); Comments.SimpleCollectionLoad(_applicationService.Client.Users[User].Repositories[Repository].Commits[Node].Comments.GetAll()).ToBackground(); return t1; } public async Task AddComment(string text) { var c = await _applicationService.Client.ExecuteAsync(_applicationService.Client.Users[User].Repositories[Repository].Commits[Node].Comments.Create(text)); Comments.Items.Add(c.Data); } public class NavObject { public string Username { get; set; } public string Repository { get; set; } public string Node { get; set; } public bool ShowRepository { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Changesets/ChangesetsViewModel.cs ================================================ using GitHubSharp; using System.Collections.Generic; using GitHubSharp.Models; using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Changesets { public class ChangesetsViewModel : CommitsViewModel { public string Branch { get; private set; } public ChangesetsViewModel( IApplicationService applicationService = null, IFeaturesService featuresService = null) : base(applicationService, featuresService) { } public void Init(NavObject navObject) { base.Init(navObject); Branch = navObject.Branch; } protected override GitHubRequest> GetRequest() { return this.GetApplication().Client.Users[Username].Repositories[Repository].Commits.GetAll(Branch); } public new class NavObject : CommitsViewModel.NavObject { public string Branch { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Changesets/CommitsViewModel.cs ================================================ using GitHubSharp.Models; using System.Windows.Input; using System.Threading.Tasks; using GitHubSharp; using System.Collections.Generic; using MvvmCross.Core.ViewModels; using CodeHub.Core.Services; using Splat; namespace CodeHub.Core.ViewModels.Changesets { public abstract class CommitsViewModel : LoadableViewModel { private readonly CollectionViewModel _commits = new CollectionViewModel(); private readonly IFeaturesService _featuresService; private readonly IApplicationService _applicationService; public string Username { get; private set; } public string Repository { get; private set; } private bool _shouldShowPro; public bool ShouldShowPro { get { return _shouldShowPro; } protected set { this.RaiseAndSetIfChanged(ref _shouldShowPro, value); } } public ICommand GoToChangesetCommand { get { return new MvxCommand(x => ShowViewModel(new ChangesetViewModel.NavObject { Username = Username, Repository = Repository, Node = x.Sha })); } } public CollectionViewModel Commits { get { return _commits; } } protected CommitsViewModel( IApplicationService applicationService = null, IFeaturesService featuresService = null) { _applicationService = applicationService ?? Locator.Current.GetService(); _featuresService = featuresService ?? Locator.Current.GetService(); } public void Init(NavObject navObject) { Username = navObject.Username; Repository = navObject.Repository; } protected override Task Load() { if (_featuresService.IsProEnabled) ShouldShowPro = false; else { var repoRequest = _applicationService.Client.Users[Username].Repositories[Repository].Get(); _applicationService.Client.ExecuteAsync(repoRequest) .ToBackground(x => ShouldShowPro = x.Data.Private && !_featuresService.IsProEnabled); } return Commits.SimpleCollectionLoad(GetRequest()); } protected abstract GitHubRequest> GetRequest(); public class NavObject { public string Username { get; set; } public string Repository { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/CollectionViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using CodeHub.Core.Utils; using MvvmCross.Core.ViewModels; namespace CodeHub.Core.ViewModels { public class CollectionViewModel : MvxViewModel, IEnumerable, INotifyCollectionChanged { private readonly CustomObservableCollection _source = new CustomObservableCollection(); private Func, IEnumerable>> _groupingFunction; private Func, IEnumerable> _sortingFunction; private Func, IEnumerable> _filteringFunction; private Action _moreItems; private int _deferLevel; public event NotifyCollectionChangedEventHandler CollectionChanged; public CustomObservableCollection Items { get { return _source; } } public bool IsDefering { get { return _deferLevel > 0; } } public Action MoreItems { get { return _moreItems; } set { _moreItems = value; if (!IsDefering) RaisePropertyChanged(() => MoreItems); } } public Func, IEnumerable> SortingFunction { get { return _sortingFunction; } set { _sortingFunction = value; if (!IsDefering) RaisePropertyChanged(() => SortingFunction); } } public Func, IEnumerable> FilteringFunction { get { return _filteringFunction; } set { _filteringFunction = value; if (!IsDefering) RaisePropertyChanged(() => FilteringFunction); } } public Func, IEnumerable>> GroupingFunction { get { return _groupingFunction; } set { _groupingFunction = value; if (!IsDefering) RaisePropertyChanged(() => GroupingFunction); } } public CollectionViewModel() : this (new CustomObservableCollection()) { } public CollectionViewModel(CustomObservableCollection source) { //Forward events _source = source; _source.CollectionChanged += (sender, e) => { if (IsDefering) return; var eventHandler = CollectionChanged; if (eventHandler != null) eventHandler(sender, e); }; } public void Refresh() { if (IsDefering) return; var eventHandler = CollectionChanged; if (eventHandler != null) eventHandler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } public IDisposable DeferRefresh() { ++_deferLevel; return new DeferHelper(this); } private void EndDefer() { --_deferLevel; if (_deferLevel == 0) Refresh(); } public IEnumerator GetEnumerator() { if (SortingFunction != null) return SortingFunction(Items).GetEnumerator(); return Items.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } private class DeferHelper : IDisposable { private readonly CollectionViewModel _parent; public DeferHelper(CollectionViewModel parent) { _parent = parent; } public void Dispose() { if (_parent != null) _parent.EndDefer(); } } } } ================================================ FILE: CodeHub.Core/ViewModels/Events/BaseEventsViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using MvvmCross.Core.ViewModels; using CodeHub.Core.ViewModels.Gists; using CodeHub.Core.ViewModels.Issues; using CodeHub.Core.ViewModels.PullRequests; using CodeHub.Core.ViewModels.Repositories; using CodeHub.Core.ViewModels.User; using GitHubSharp; using GitHubSharp.Models; using CodeHub.Core.Utils; using CodeHub.Core.ViewModels.Changesets; using System.Reactive; using System.Reactive.Subjects; namespace CodeHub.Core.ViewModels.Events { public abstract class BaseEventsViewModel : LoadableViewModel { private readonly CollectionViewModel> _events = new CollectionViewModel>(); public readonly ISubject> GoToTagCommand = new Subject>(); public readonly ISubject> GoToBranchCommand = new Subject>(); public CollectionViewModel> Events => _events; public bool ReportRepository { get; private set; } protected BaseEventsViewModel() { ReportRepository = true; } protected override Task Load() { return this.RequestModel(CreateRequest(0, 100), response => { this.CreateMore(response, m => Events.MoreItems = m, d => Events.Items.AddRange(CreateDataFromLoad(d))); Events.Items.Reset(CreateDataFromLoad(response.Data)); }); } private IEnumerable> CreateDataFromLoad(List events) { var transformedEvents = new List>(events.Count); foreach (var e in events) { try { transformedEvents.Add(new Tuple(e, CreateEventTextBlocks(e))); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex.ToString()); } } return transformedEvents; } protected abstract GitHubRequest> CreateRequest(int page, int perPage); private void GoToCommits(EventModel.RepoModel repoModel, string branch) { var repoId = RepositoryIdentifier.FromFullName(repoModel.Name); if (repoId == null) return; ShowViewModel(new ChangesetsViewModel.NavObject { Username = repoId?.Owner, Repository = repoId?.Name, Branch = branch }); } public ICommand GoToRepositoryCommand { get { return new MvxCommand(GoToRepository, x => x != null); } } private void GoToRepository(EventModel.RepoModel eventModel) { var repoId = RepositoryIdentifier.FromFullName(eventModel.Name); if (repoId == null) return; ShowViewModel(new RepositoryViewModel.NavObject { Username = repoId?.Owner, Repository = repoId?.Name }); } private void GoToUser(string username) { if (string.IsNullOrEmpty(username)) return; ShowViewModel(new UserViewModel.NavObject {Username = username}); } private void GoToBranch(RepositoryIdentifier repoId, string branchName) { if (repoId == null) return; GoToBranchCommand.OnNext(Tuple.Create(repoId, branchName)); } private void GoToTag(EventModel.RepoModel eventModel, string tagName) { var repoId = RepositoryIdentifier.FromFullName(eventModel.Name); if (repoId == null) return; GoToTagCommand.OnNext(Tuple.Create(repoId, tagName)); } public ICommand GoToGistCommand { get { return new MvxCommand(x => ShowViewModel(new GistViewModel.NavObject { Id = x.Gist.Id }), x => x != null && x.Gist != null); } } private void GoToIssue(RepositoryIdentifier repo, long id) { if (repo == null || string.IsNullOrEmpty(repo.Name) || string.IsNullOrEmpty(repo.Owner)) return; ShowViewModel(new IssueViewModel.NavObject { Username = repo.Owner, Repository = repo.Name, Id = id }); } private void GoToPullRequest(RepositoryIdentifier repo, long id) { if (repo == null || string.IsNullOrEmpty(repo.Name) || string.IsNullOrEmpty(repo.Owner)) return; ShowViewModel(new PullRequestViewModel.NavObject { Username = repo.Owner, Repository = repo.Name, Id = id }); } private void GoToPullRequests(RepositoryIdentifier repo) { if (repo == null || string.IsNullOrEmpty(repo.Name) || string.IsNullOrEmpty(repo.Owner)) return; ShowViewModel(new PullRequestsViewModel.NavObject { Username = repo.Owner, Repository = repo.Name }); } private void GoToChangeset(RepositoryIdentifier repo, string sha) { if (repo == null || string.IsNullOrEmpty(repo.Name) || string.IsNullOrEmpty(repo.Owner)) return; ShowViewModel(new ChangesetViewModel.NavObject { Username = repo.Owner, Repository = repo.Name, Node = sha }); } private EventBlock CreateEventTextBlocks(EventModel eventModel) { var eventBlock = new EventBlock(); var repoId = eventModel.Repo != null ? RepositoryIdentifier.FromFullName(eventModel.Repo.Name) : new RepositoryIdentifier(string.Empty, string.Empty); var username = eventModel.Actor != null ? eventModel.Actor.Login : null; // Insert the actor eventBlock.Header.Add(new AnchorBlock(username, () => GoToUser(username))); var commitCommentEvent = eventModel.PayloadObject as EventModel.CommitCommentEvent; if (commitCommentEvent != null) { var node = commitCommentEvent.Comment.CommitId.Substring(0, commitCommentEvent.Comment.CommitId.Length > 6 ? 6 : commitCommentEvent.Comment.CommitId.Length); eventBlock.Tapped = () => GoToChangeset(repoId, commitCommentEvent.Comment.CommitId); eventBlock.Header.Add(new TextBlock(" commented on commit ")); eventBlock.Header.Add(new AnchorBlock(node, eventBlock.Tapped)); if (ReportRepository) { eventBlock.Header.Add(new TextBlock(" in ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); } eventBlock.Body.Add(new TextBlock(commitCommentEvent.Comment.Body)); return eventBlock; } var createEvent = eventModel.PayloadObject as EventModel.CreateEvent; if (createEvent != null) { if (createEvent.RefType.Equals("repository")) { if (ReportRepository) { eventBlock.Tapped = () => GoToRepositoryCommand.Execute(eventModel.Repo); eventBlock.Header.Add(new TextBlock(" created repository ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); } else eventBlock.Header.Add(new TextBlock(" created this repository")); } else if (createEvent.RefType.Equals("branch")) { eventBlock.Tapped = () => GoToBranch(repoId, createEvent.Ref); eventBlock.Header.Add(new TextBlock(" created branch ")); eventBlock.Header.Add(new AnchorBlock(createEvent.Ref, eventBlock.Tapped)); if (ReportRepository) { eventBlock.Header.Add(new TextBlock(" in ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); } } else if (createEvent.RefType.Equals("tag")) { eventBlock.Tapped = () => GoToTag(eventModel.Repo, createEvent.Ref); eventBlock.Header.Add(new TextBlock(" created tag ")); eventBlock.Header.Add(new AnchorBlock(createEvent.Ref, eventBlock.Tapped)); if (ReportRepository) { eventBlock.Header.Add(new TextBlock(" in ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); } } } var deleteEvent = eventModel.PayloadObject as EventModel.DeleteEvent; if (deleteEvent != null) { if (deleteEvent.RefType.Equals("branch")) { eventBlock.Tapped = () => GoToRepository(eventModel.Repo); eventBlock.Header.Add(new TextBlock(" deleted branch ")); } else if (deleteEvent.RefType.Equals("tag")) { eventBlock.Tapped = () => GoToRepository(eventModel.Repo); eventBlock.Header.Add(new TextBlock(" deleted tag ")); } else return null; eventBlock.Header.Add(new AnchorBlock(deleteEvent.Ref, eventBlock.Tapped)); if (!ReportRepository) return eventBlock; eventBlock.Header.Add(new TextBlock(" in ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); return eventBlock; } if (eventModel.PayloadObject is EventModel.DownloadEvent) { // Don't show the download event for now... return null; } var followEvent = eventModel.PayloadObject as EventModel.FollowEvent; if (followEvent != null) { eventBlock.Tapped = () => GoToUser(followEvent.Target.Login); eventBlock.Header.Add(new TextBlock(" started following ")); eventBlock.Header.Add(new AnchorBlock(followEvent.Target.Login, eventBlock.Tapped)); return eventBlock; } /* * FORK EVENT */ else if (eventModel.PayloadObject is EventModel.ForkEvent) { var forkEvent = (EventModel.ForkEvent)eventModel.PayloadObject; var forkedRepo = new EventModel.RepoModel {Id = forkEvent.Forkee.Id, Name = forkEvent.Forkee.FullName, Url = forkEvent.Forkee.Url}; eventBlock.Tapped = () => GoToRepositoryCommand.Execute(forkedRepo); eventBlock.Header.Add(new TextBlock(" forked ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); eventBlock.Header.Add(new TextBlock(" to ")); eventBlock.Header.Add(CreateRepositoryTextBlock(forkedRepo)); } /* * FORK APPLY EVENT */ else if (eventModel.PayloadObject is EventModel.ForkApplyEvent) { var forkEvent = (EventModel.ForkApplyEvent)eventModel.PayloadObject; eventBlock.Tapped = () => GoToRepositoryCommand.Execute(eventModel.Repo); eventBlock.Header.Add(new TextBlock(" applied fork to ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); eventBlock.Header.Add(new TextBlock(" on branch ")); eventBlock.Header.Add(new AnchorBlock(forkEvent.Head, () => GoToBranch(repoId, forkEvent.Head))); } /* * GIST EVENT */ else if (eventModel.PayloadObject is EventModel.GistEvent) { var gistEvent = (EventModel.GistEvent)eventModel.PayloadObject; eventBlock.Tapped = () => GoToGistCommand.Execute(gistEvent); if (string.Equals(gistEvent.Action, "create", StringComparison.OrdinalIgnoreCase)) eventBlock.Header.Add(new TextBlock(" created Gist #")); else if (string.Equals(gistEvent.Action, "update", StringComparison.OrdinalIgnoreCase)) eventBlock.Header.Add(new TextBlock(" updated Gist #")); else if (string.Equals(gistEvent.Action, "fork", StringComparison.OrdinalIgnoreCase)) eventBlock.Header.Add(new TextBlock(" forked Gist #")); eventBlock.Header.Add(new AnchorBlock(gistEvent.Gist.Id, eventBlock.Tapped)); eventBlock.Body.Add(new TextBlock(gistEvent.Gist.Description.Replace('\n', ' ').Replace("\r", "").Trim())); } /* * GOLLUM EVENT (WIKI) */ else if (eventModel.PayloadObject is EventModel.GollumEvent) { var gollumEvent = eventModel.PayloadObject as EventModel.GollumEvent; eventBlock.Header.Add(new TextBlock(" modified the wiki in ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); if (gollumEvent != null && gollumEvent.Pages != null) { foreach (var page in gollumEvent.Pages) { var p = page; eventBlock.Body.Add(new AnchorBlock(page.PageName, () => GoToUrlCommand.Execute(p.HtmlUrl))); eventBlock.Body.Add(new TextBlock(" - " + page.Action + "\n")); } eventBlock.Multilined = true; } } /* * ISSUE COMMENT EVENT */ else if (eventModel.PayloadObject is EventModel.IssueCommentEvent) { var commentEvent = (EventModel.IssueCommentEvent)eventModel.PayloadObject; if (commentEvent.Issue.PullRequest != null && !string.IsNullOrEmpty(commentEvent.Issue.PullRequest.HtmlUrl)) { eventBlock.Tapped = () => GoToPullRequest(repoId, commentEvent.Issue.Number); eventBlock.Header.Add(new TextBlock(" commented on pull request ")); } else { eventBlock.Tapped = () => GoToIssue(repoId, commentEvent.Issue.Number); eventBlock.Header.Add(new TextBlock(" commented on issue ")); } eventBlock.Header.Add(new AnchorBlock("#" + commentEvent.Issue.Number, eventBlock.Tapped)); eventBlock.Header.Add(new TextBlock(" in ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); eventBlock.Body.Add(new TextBlock(commentEvent.Comment.Body.Replace('\n', ' ').Replace("\r", "").Trim())); } /* * ISSUES EVENT */ else if (eventModel.PayloadObject is EventModel.IssuesEvent) { var issueEvent = (EventModel.IssuesEvent)eventModel.PayloadObject; eventBlock.Tapped = () => GoToIssue(repoId, issueEvent.Issue.Number); if (string.Equals(issueEvent.Action, "opened", StringComparison.OrdinalIgnoreCase)) eventBlock.Header.Add(new TextBlock(" opened issue ")); else if (string.Equals(issueEvent.Action, "closed", StringComparison.OrdinalIgnoreCase)) eventBlock.Header.Add(new TextBlock(" closed issue ")); else if (string.Equals(issueEvent.Action, "reopened", StringComparison.OrdinalIgnoreCase)) eventBlock.Header.Add(new TextBlock(" reopened issue ")); eventBlock.Header.Add(new AnchorBlock("#" + issueEvent.Issue.Number, eventBlock.Tapped)); eventBlock.Header.Add(new TextBlock(" in ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); eventBlock.Body.Add(new TextBlock(issueEvent.Issue.Title.Trim())); } /* * MEMBER EVENT */ else if (eventModel.PayloadObject is EventModel.MemberEvent) { var memberEvent = (EventModel.MemberEvent)eventModel.PayloadObject; eventBlock.Tapped = () => GoToRepositoryCommand.Execute(eventModel.Repo); if (memberEvent.Action.Equals("added")) eventBlock.Header.Add(new TextBlock(" added ")); else if (memberEvent.Action.Equals("removed")) eventBlock.Header.Add(new TextBlock(" removed ")); var memberName = memberEvent.Member?.Login; if (memberName != null) eventBlock.Header.Add(new AnchorBlock(memberName, () => GoToUser(memberName))); else eventBlock.Header.Add(new TextBlock("")); eventBlock.Header.Add(new TextBlock(" as a collaborator")); if (ReportRepository) { eventBlock.Header.Add(new TextBlock(" to ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); } } /* * PUBLIC EVENT */ else if (eventModel.PayloadObject is EventModel.PublicEvent) { eventBlock.Tapped = () => GoToRepositoryCommand.Execute(eventModel.Repo); if (ReportRepository) { eventBlock.Header.Add(new TextBlock(" has open sourced ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); } else eventBlock.Header.Add(new TextBlock(" has been open sourced this repository!")); } /* * PULL REQUEST EVENT */ else if (eventModel.PayloadObject is EventModel.PullRequestEvent) { var pullEvent = (EventModel.PullRequestEvent)eventModel.PayloadObject; eventBlock.Tapped = () => GoToPullRequest(repoId, pullEvent.Number); if (pullEvent.Action.Equals("closed")) eventBlock.Header.Add(new TextBlock(" closed pull request ")); else if (pullEvent.Action.Equals("opened")) eventBlock.Header.Add(new TextBlock(" opened pull request ")); else if (pullEvent.Action.Equals("synchronize")) eventBlock.Header.Add(new TextBlock(" synchronized pull request ")); else if (pullEvent.Action.Equals("reopened")) eventBlock.Header.Add(new TextBlock(" reopened pull request ")); eventBlock.Header.Add(new AnchorBlock("#" + pullEvent.PullRequest.Number, eventBlock.Tapped)); eventBlock.Header.Add(new TextBlock(" in ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); eventBlock.Body.Add(new TextBlock(pullEvent.PullRequest.Title)); } /* * PULL REQUEST REVIEW COMMENT EVENT */ else if (eventModel.PayloadObject is EventModel.PullRequestReviewCommentEvent) { var commentEvent = (EventModel.PullRequestReviewCommentEvent)eventModel.PayloadObject; eventBlock.Tapped = () => GoToPullRequests(repoId); eventBlock.Header.Add(new TextBlock(" commented on pull request ")); if (ReportRepository) { eventBlock.Header.Add(new TextBlock(" in ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); } eventBlock.Body.Add(new TextBlock(commentEvent.Comment.Body.Replace('\n', ' ').Replace("\r", "").Trim())); } /* * PUSH EVENT */ else if (eventModel.PayloadObject is EventModel.PushEvent) { var pushEvent = (EventModel.PushEvent)eventModel.PayloadObject; string branchRef = null; if (!string.IsNullOrEmpty(pushEvent.Ref)) { var lastSlash = pushEvent.Ref.LastIndexOf("/", StringComparison.Ordinal) + 1; branchRef = pushEvent.Ref.Substring(lastSlash); } if (eventModel.Repo != null) eventBlock.Tapped = () => GoToCommits(eventModel.Repo, pushEvent.Ref); eventBlock.Header.Add(new TextBlock(" pushed to ")); if (branchRef != null) eventBlock.Header.Add(new AnchorBlock(branchRef, () => GoToBranch(repoId, branchRef))); if (ReportRepository) { eventBlock.Header.Add(new TextBlock(" at ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); } if (pushEvent.Commits != null) { foreach (var commit in pushEvent.Commits) { var desc = (commit.Message ?? ""); var sha = commit.Sha; var firstNewLine = desc.IndexOf("\n"); if (firstNewLine <= 0) firstNewLine = desc.Length; desc = desc.Substring(0, firstNewLine); var shortSha = commit.Sha; if (shortSha.Length > 6) shortSha = shortSha.Substring(0, 6); eventBlock.Body.Add(new AnchorBlock(shortSha, () => GoToChangeset(repoId, sha))); eventBlock.Body.Add(new TextBlock(" - " + desc + "\n")); eventBlock.Multilined = true; } } } var teamAddEvent = eventModel.PayloadObject as EventModel.TeamAddEvent; if (teamAddEvent != null) { eventBlock.Header.Add(new TextBlock(" added ")); if (teamAddEvent.User != null) eventBlock.Header.Add(new AnchorBlock(teamAddEvent.User.Login, () => GoToUser(teamAddEvent.User.Login))); else if (teamAddEvent.Repo != null) eventBlock.Header.Add(CreateRepositoryTextBlock(new EventModel.RepoModel { Id = teamAddEvent.Repo.Id, Name = teamAddEvent.Repo.FullName, Url = teamAddEvent.Repo.Url })); else return null; if (teamAddEvent.Team == null) return eventBlock; eventBlock.Header.Add(new TextBlock(" to team ")); eventBlock.Header.Add(new AnchorBlock(teamAddEvent.Team.Name, () => { })); return eventBlock; } var watchEvent = eventModel.PayloadObject as EventModel.WatchEvent; if (watchEvent != null) { eventBlock.Tapped = () => GoToRepositoryCommand.Execute(eventModel.Repo); eventBlock.Header.Add(watchEvent.Action.Equals("started") ? new TextBlock(" starred ") : new TextBlock(" unstarred ")); eventBlock.Header.Add(CreateRepositoryTextBlock(eventModel.Repo)); return eventBlock; } var releaseEvent = eventModel.PayloadObject as EventModel.ReleaseEvent; if (releaseEvent != null) { eventBlock.Tapped = () => GoToUrlCommand.Execute(releaseEvent.Release.HtmlUrl); eventBlock.Header.Add(new TextBlock(" " + releaseEvent.Action + " release " + releaseEvent.Release.Name)); return eventBlock; } return eventBlock; } private TextBlock CreateRepositoryTextBlock(EventModel.RepoModel repoModel) { //Most likely indicates a deleted repository if (repoModel == null) return new TextBlock("Unknown Repository"); if (repoModel.Name == null) return new TextBlock(""); var repoSplit = repoModel.Name.Split('/'); if (repoSplit.Length < 2) return new TextBlock(repoModel.Name); // var repoOwner = repoSplit[0]; // var repoName = repoSplit[1]; return new AnchorBlock(repoModel.Name, () => GoToRepositoryCommand.Execute(repoModel)); } public class EventBlock { public IList Header { get; private set; } public IList Body { get; private set; } public Action Tapped { get; set; } public bool Multilined { get; set; } public EventBlock() { Header = new List(6); Body = new List(); } } public class TextBlock { public string Text { get; set; } public TextBlock() { } public TextBlock(string text) { Text = text; } } public class AnchorBlock : TextBlock { public AnchorBlock(string text, Action tapped) : base(text) { Tapped = tapped; } public Action Tapped { get; set; } public AnchorBlock(Action tapped) { Tapped = tapped; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Events/NewsViewModel.cs ================================================ using System.Collections.Generic; using GitHubSharp.Models; namespace CodeHub.Core.ViewModels.Events { public class NewsViewModel : BaseEventsViewModel { protected override GitHubSharp.GitHubRequest> CreateRequest(int page, int perPage) { return this.GetApplication().Client.Users[this.GetApplication().Account.Username].GetReceivedEvents(page, perPage); } } } ================================================ FILE: CodeHub.Core/ViewModels/Events/OrganizationEventsViewModel.cs ================================================ using System.Collections.Generic; using GitHubSharp; using GitHubSharp.Models; namespace CodeHub.Core.ViewModels.Events { public class OrganizationEventsViewModel : BaseEventsViewModel { public string OrganizationName { get; private set; } public string Username { get; private set; } public void Init(NavObject navobject) { Username = navobject.Username; OrganizationName = navobject.OrganizagtionName; } protected override GitHubRequest> CreateRequest(int page, int perPage) { return this.GetApplication().Client.Users[Username].GetOrganizationEvents(OrganizationName, page, perPage); } public class NavObject { public string Username { get; set; } public string OrganizagtionName { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Events/RepositoryEventsViewModel.cs ================================================ using System.Collections.Generic; using GitHubSharp; using GitHubSharp.Models; namespace CodeHub.Core.ViewModels.Events { public class RepositoryEventsViewModel : BaseEventsViewModel { public string Repository { get; private set; } public string Username { get; private set; } public void Init(NavObject navObject) { Username = navObject.Username; Repository = navObject.Repository; } protected override GitHubRequest> CreateRequest(int page, int perPage) { return this.GetApplication().Client.Users[Username].Repositories[Repository].GetEvents(page, perPage); } public class NavObject { public string Username { get; set; } public string Repository { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Events/UserEventsViewModel.cs ================================================ using System.Collections.Generic; using GitHubSharp; using GitHubSharp.Models; namespace CodeHub.Core.ViewModels.Events { public class UserEventsViewModel : BaseEventsViewModel { public string Username { get; private set; } public void Init(NavObject navObject) { Username = navObject.Username; } protected override GitHubRequest> CreateRequest(int page, int perPage) { return this.GetApplication().Client.Users[Username].GetEvents(page, perPage); } public class NavObject { public string Username { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/FilterGroup.cs ================================================ using System.Collections.Generic; using System.Linq; namespace CodeHub.Core.ViewModels { public class FilterGroup : IGrouping { readonly List _elements; public FilterGroup(string key, List elements) { Key = key; _elements = elements; } public IEnumerator GetEnumerator() { return this._elements.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } public string Key { get; private set; } } } ================================================ FILE: CodeHub.Core/ViewModels/FilterModel.cs ================================================ using System; namespace CodeHub.Core.ViewModels { [Serializable] public abstract class FilterModel { public abstract TF Clone(); } } ================================================ FILE: CodeHub.Core/ViewModels/FilterableCollectionViewModel.cs ================================================ using CodeHub.Core.Data; using CodeHub.Core.Services; using MvvmCross.Platform; namespace CodeHub.Core.ViewModels { public class FilterableCollectionViewModel : CollectionViewModel, IFilterableViewModel where TF : FilterModel, new() { protected TF _filter; private readonly string _filterKey; public TF Filter { get { return _filter; } set { _filter = value; RaisePropertyChanged(() => Filter); } } public FilterableCollectionViewModel(string filterKey) { _filterKey = filterKey; var application = Mvx.Resolve(); var accounts = Mvx.Resolve(); _filter = application.Account.GetFilter(_filterKey) ?? new TF(); accounts.Save(application.Account).ToBackground(); } public void ApplyFilter(TF filter, bool saveAsDefault = false) { Filter = filter; if (saveAsDefault) { var application = Mvx.Resolve(); application.Account.SetFilter(_filterKey, _filter); application.UpdateActiveAccount().ToBackground(); } } } } ================================================ FILE: CodeHub.Core/ViewModels/Gists/GistCreateViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Threading.Tasks; using CodeHub.Core.Messages; using ReactiveUI; using System.Reactive; using CodeHub.Core.Services; using Octokit; namespace CodeHub.Core.ViewModels.Gists { public class GistCreateViewModel : BaseViewModel { private readonly IMessageService _messageService; private string _description; private bool _public; private IDictionary _files = new Dictionary(); private bool _saving; public bool IsSaving { get { return _saving; } private set { this.RaiseAndSetIfChanged(ref _saving, value); } } public string Description { get { return _description; } set { this.RaiseAndSetIfChanged(ref _description, value); } } public bool Public { get { return _public; } set { this.RaiseAndSetIfChanged(ref _public, value); } } public IDictionary Files { get { return _files; } set { this.RaiseAndSetIfChanged(ref _files, value); } } public ReactiveCommand SaveCommand { get; } public ReactiveCommand CancelCommand { get; } public GistCreateViewModel(IMessageService messageService = null) { _messageService = messageService ?? GetService(); CancelCommand = ReactiveCommand.Create(() => { }); SaveCommand = ReactiveCommand.CreateFromTask(Save); SaveCommand.ThrownExceptions.Subscribe(x => DisplayAlert(x.Message)); } private async Task Save() { if (_files.Count == 0) throw new Exception("You cannot create a Gist without atleast one file! Please correct and try again."); try { var newGist = new NewGist() { Description = Description ?? string.Empty, Public = Public }; foreach (var kv in Files) newGist.Files.Add(kv.Key, kv.Value); IsSaving = true; var gist = await this.GetApplication().GitHubClient.Gist.Create(newGist); _messageService.Send(new GistAddMessage(gist)); return gist; } finally { IsSaving = false; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Gists/GistItemViewModel.cs ================================================ using System; using ReactiveUI; using CodeHub.Core.Utilities; using Humanizer; using System.Reactive; using Octokit; using System.Linq; namespace CodeHub.Core.ViewModels.Gists { public class GistItemViewModel : ReactiveObject, ICanGoToViewModel { public GitHubAvatar Avatar { get; } public string Title { get; } public string Description { get; } public DateTimeOffset UpdatedAt { get; } public string UpdatedString { get; } public ReactiveCommand GoToCommand { get; } public string Id { get; } public Gist Gist { get; } private static string GetGistTitle(Gist gist) { var title = (gist.Owner == null) ? "Anonymous" : gist.Owner.Login; if (gist.Files.Count > 0) title = gist.Files.First().Key; return title; } public GistItemViewModel(Gist gist, Action gotoAction) { Gist = gist; Id = gist.Id; Title = GetGistTitle(gist); Description = string.IsNullOrEmpty(gist.Description) ? "Gist " + gist.Id : gist.Description; Avatar = new GitHubAvatar(gist.Owner?.AvatarUrl); UpdatedAt = gist.UpdatedAt; UpdatedString = UpdatedAt.Humanize(); GoToCommand = ReactiveCommand.Create(() => gotoAction(this)); } } } ================================================ FILE: CodeHub.Core/ViewModels/Gists/GistViewModel.cs ================================================ using System; using System.Threading.Tasks; using System.Windows.Input; using CodeHub.Core.ViewModels.User; using MvvmCross.Core.ViewModels; using Octokit; namespace CodeHub.Core.ViewModels.Gists { public class GistViewModel : LoadableViewModel { private readonly CollectionViewModel _comments = new CollectionViewModel(); private Gist _gist; private bool _starred; public string Id { get; private set; } public Gist Gist { get { return _gist; } set { this.RaiseAndSetIfChanged(ref _gist, value); } } public bool IsStarred { get { return _starred; } private set { this.RaiseAndSetIfChanged(ref _starred, value); } } public CollectionViewModel Comments { get { return _comments; } } public ICommand GoToUserCommand { get { return new MvxCommand(() => ShowViewModel(new UserViewModel.NavObject { Username = Gist.Owner.Login }), () => Gist != null && Gist.Owner != null); } } public ICommand GoToHtmlUrlCommand { get { return new MvxCommand(() => GoToUrlCommand.Execute(_gist.HtmlUrl), () => _gist != null); } } public ICommand ForkCommand { get { return new MvxCommand(() => ForkGist()); } } public ICommand ToggleStarCommand { get { return new MvxCommand(() => ToggleStarred(), () => Gist != null); } } public static GistViewModel FromGist(Gist gist) { return new GistViewModel { Gist = gist, Id = gist.Id }; } public void Init(NavObject navObject) { Id = navObject.Id; } private async Task ToggleStarred() { try { var request = IsStarred ? this.GetApplication().Client.Gists[Id].Unstar() : this.GetApplication().Client.Gists[Id].Star(); await this.GetApplication().Client.ExecuteAsync(request); IsStarred = !IsStarred; } catch { DisplayAlert("Unable to start gist. Please try again."); } } public async Task ForkGist() { var data = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Gists[Id].ForkGist()); var forkedGist = data.Data; ShowViewModel(new GistViewModel.NavObject { Id = forkedGist.Id }); } protected override async Task Load() { Comments.Items.Clear(); this.GetApplication().GitHubClient.Gist.IsStarred(Id) .ToBackground(x => IsStarred = x); this.GetApplication().GitHubClient.Gist.Comment.GetAllForGist(Id) .ToBackground(Comments.Items.AddRange); Gist = await this.GetApplication().GitHubClient.Gist.Get(Id); } public async Task Edit(GistUpdate editModel) { Gist = await this.GetApplication().GitHubClient.Gist.Edit(Id, editModel); } public class NavObject { public string Id { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Gists/GistsViewModel.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using ReactiveUI; using CodeHub.Core.Services; using System.Reactive; using System; using Octokit; using System.Reactive.Linq; using Splat; using CodeHub.Core.Messages; namespace CodeHub.Core.ViewModels.Gists { public class CurrentUserGistsViewModel : GistsViewModel { private readonly IDisposable _addToken; public CurrentUserGistsViewModel(string username, IMessageService messageService = null) : base(ApiUrls.UsersGists(username)) { messageService = messageService ?? Locator.Current.GetService(); _addToken = messageService.Listen(msg => Gists.Insert(0, msg.Gist)); } } public class GistsViewModel : ReactiveObject { private readonly IApplicationService _applicationService; private readonly IAlertDialogService _dialogService; private readonly ReactiveList _internalItems = new ReactiveList(resetChangeThreshold: double.MaxValue); public ReactiveCommand LoadCommand { get; } public ReactiveCommand LoadMoreCommand { get; } public IReadOnlyReactiveList Items { get; private set; } protected ReactiveList Gists => _internalItems; public ReactiveCommand ItemSelected { get; } private ObservableAsPropertyHelper _hasMore; public bool HasMore => _hasMore.Value; private Uri _nextPage; private Uri NextPage { get { return _nextPage; } set { this.RaiseAndSetIfChanged(ref _nextPage, value); } } private string _searchText; public string SearchText { get { return _searchText; } set { this.RaiseAndSetIfChanged(ref _searchText, value); } } private readonly ObservableAsPropertyHelper _isEmpty; public bool IsEmpty => _isEmpty.Value; public static GistsViewModel CreatePublicGistsViewModel() => new GistsViewModel(ApiUrls.PublicGists()); public static GistsViewModel CreateStarredGistsViewModel() => new GistsViewModel(ApiUrls.StarredGists()); public static GistsViewModel CreateUserGistsViewModel(string username) => new GistsViewModel(ApiUrls.UsersGists(username)); public GistsViewModel( Uri uri, IApplicationService applicationService = null, IAlertDialogService dialogService = null) { _applicationService = applicationService ?? Locator.Current.GetService(); _dialogService = dialogService ?? Locator.Current.GetService(); NextPage = uri; var showDescription = _applicationService.Account.ShowRepositoryDescriptionInList; ItemSelected = ReactiveCommand.Create(x => x); var gistItems = _internalItems.CreateDerivedCollection( x => new GistItemViewModel(x, GoToItem)); var searchUpdated = this.WhenAnyValue(x => x.SearchText) .Throttle(TimeSpan.FromMilliseconds(400), RxApp.MainThreadScheduler); Items = gistItems.CreateDerivedCollection( x => x, x => x.Title.ContainsKeyword(SearchText) || x.Description.ContainsKeyword(SearchText), signalReset: searchUpdated); LoadCommand = ReactiveCommand.CreateFromTask(async t => { _internalItems.Clear(); var parameters = new Dictionary { ["per_page"] = 100.ToString() }; var items = await RetrieveItems(uri, parameters); _internalItems.AddRange(items); return items.Count > 0; }); var canLoadMore = this.WhenAnyValue(x => x.NextPage).Select(x => x != null); LoadMoreCommand = ReactiveCommand.CreateFromTask(async _ => { var items = await RetrieveItems(NextPage); _internalItems.AddRange(items); return items.Count > 0; }, canLoadMore); LoadCommand.Select(_ => _internalItems.Count == 0) .ToProperty(this, x => x.IsEmpty, out _isEmpty, true); LoadCommand.ThrownExceptions.Subscribe(LoadingError); LoadMoreCommand.ThrownExceptions.Subscribe(LoadingError); _hasMore = this.WhenAnyValue(x => x.NextPage) .Select(x => x != null) .ToProperty(this, x => x.HasMore); } private void LoadingError(Exception err) { _dialogService.Alert("Error Loading", err.Message).ToBackground(); } private void GoToItem(GistItemViewModel item) { ItemSelected.ExecuteNow(item); } private async Task> RetrieveItems( Uri repositoriesUri, IDictionary parameters = null) { try { var connection = _applicationService.GitHubClient.Connection; var ret = await connection.Get>(repositoriesUri, parameters, "application/json"); NextPage = ret.HttpResponse.ApiInfo.Links.ContainsKey("next") ? ret.HttpResponse.ApiInfo.Links["next"] : null; return ret.Body; } catch { NextPage = null; throw; } } } } ================================================ FILE: CodeHub.Core/ViewModels/ICanGoToViewModel.cs ================================================ using System.Reactive; using ReactiveUI; namespace CodeHub.Core.ViewModels { public interface ICanGoToViewModel { ReactiveCommand GoToCommand { get; } } } ================================================ FILE: CodeHub.Core/ViewModels/IFilterableViewModel.cs ================================================ namespace CodeHub.Core.ViewModels { public interface IFilterableViewModel where TFilter : FilterModel, new() { TFilter Filter { get; } void ApplyFilter(TFilter filter, bool saveAsDefault = false); } } ================================================ FILE: CodeHub.Core/ViewModels/IListViewModel.cs ================================================ using System; using System.Reactive; using ReactiveUI; namespace CodeHub.Core.ViewModels { public interface IListViewModel { ReactiveCommand LoadCommand { get; } ReactiveCommand LoadMoreCommand { get; } IReadOnlyReactiveList Items { get; } bool HasMore { get; } string SearchText { get; } } } ================================================ FILE: CodeHub.Core/ViewModels/ILoadableViewModel.cs ================================================ using ReactiveUI; using System.Reactive; namespace CodeHub.Core.ViewModels { public interface ILoadableViewModel { ReactiveCommand LoadCommand { get; } } } ================================================ FILE: CodeHub.Core/ViewModels/IProvidesSearchKeyword.cs ================================================ namespace CodeHub.Core.ViewModels { public interface IProvidesSearchKeyword { string SearchKeyword { get; set; } } } ================================================ FILE: CodeHub.Core/ViewModels/Issues/BaseIssuesViewModel.cs ================================================ using System; using CodeHub.Core.ViewModels; using GitHubSharp.Models; using CodeHub.Core.Filters; using System.Windows.Input; using MvvmCross.Core.ViewModels; using CodeHub.Core.ViewModels.PullRequests; using System.Collections.Generic; using System.Linq; using CodeHub.Core.Utils; namespace CodeHub.Core.ViewModels.Issues { public abstract class BaseIssuesViewModel : LoadableViewModel, IBaseIssuesViewModel where TFilterModel : BaseIssuesFilterModel, new() { protected FilterableCollectionViewModel _issues; public FilterableCollectionViewModel Issues { get { return _issues; } } public ICommand GoToIssueCommand { get { return new MvxCommand(x => { var isPullRequest = x.PullRequest != null && !(string.IsNullOrEmpty(x.PullRequest.HtmlUrl)); var s1 = x.Url.Substring(x.Url.IndexOf("/repos/") + 7); var issuesIndex = s1.LastIndexOf("/issues"); issuesIndex = issuesIndex < 0 ? 0 : issuesIndex; var repoId = RepositoryIdentifier.FromFullName(s1.Substring(0, issuesIndex)); if (repoId == null) return; if (isPullRequest) ShowViewModel(new PullRequestViewModel.NavObject { Username = repoId.Owner, Repository = repoId.Name, Id = x.Number }); else ShowViewModel(new IssueViewModel.NavObject { Username = repoId.Owner, Repository = repoId.Name, Id = x.Number }); }); } } protected virtual List> Group(IEnumerable model) { var order = Issues.Filter.SortType; if (order == BaseIssuesFilterModel.Sort.Comments) { var a = Issues.Filter.Ascending ? model.OrderBy(x => x.Comments) : model.OrderByDescending(x => x.Comments); var g = a.GroupBy(x => FilterGroup.IntegerCeilings.First(r => r > x.Comments)).ToList(); return FilterGroup.CreateNumberedGroup(g, "Comments"); } if (order == BaseIssuesFilterModel.Sort.Updated) { var a = Issues.Filter.Ascending ? model.OrderBy(x => x.UpdatedAt) : model.OrderByDescending(x => x.UpdatedAt); var g = a.GroupBy(x => FilterGroup.IntegerCeilings.First(r => r > x.UpdatedAt.TotalDaysAgo())); return FilterGroup.CreateNumberedGroup(g, "Days Ago", "Updated"); } if (order == BaseIssuesFilterModel.Sort.Created) { var a = Issues.Filter.Ascending ? model.OrderBy(x => x.CreatedAt) : model.OrderByDescending(x => x.CreatedAt); var g = a.GroupBy(x => FilterGroup.IntegerCeilings.First(r => r > x.CreatedAt.TotalDaysAgo())); return FilterGroup.CreateNumberedGroup(g, "Days Ago", "Created"); } return null; } } public interface IBaseIssuesViewModel : IMvxViewModel { ICommand GoToIssueCommand { get; } } } ================================================ FILE: CodeHub.Core/ViewModels/Issues/IssueAddViewModel.cs ================================================ using System; using MvvmCross.Core.ViewModels; using System.Threading.Tasks; using CodeHub.Core.Messages; using System.Linq; using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssueAddViewModel : IssueModifyViewModel { private readonly IMessageService _messageService; public IssueAddViewModel(IMessageService messageService) : base(messageService) { _messageService = messageService; } protected override async Task Save() { if (string.IsNullOrEmpty(IssueTitle)) { DisplayAlert("Unable to save the issue: you must provide a title!"); return; } try { string assignedTo = AssignedTo == null ? null : AssignedTo.Login; int? milestone = null; if (Milestone != null) milestone = Milestone.Number; string[] labels = Labels.Items.Select(x => x.Name).ToArray(); var content = Content ?? string.Empty; IsSaving = true; var data = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues.Create(IssueTitle, content, assignedTo, milestone, labels)); _messageService.Send(new IssueAddMessage(data.Data)); ChangePresentation(new MvxClosePresentationHint(this)); } catch { DisplayAlert("Unable to save new issue! Please try again."); } finally { IsSaving = false; } } public void Init(NavObject navObject) { base.Init(navObject.Username, navObject.Repository); } public class NavObject { public string Username { get; set; } public string Repository { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Issues/IssueAssignedToViewModel.cs ================================================ using System; using GitHubSharp.Models; using System.Threading.Tasks; using CodeHub.Core.Messages; using MvvmCross.Core.ViewModels; using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssueAssignedToViewModel : LoadableViewModel { private readonly IMessageService _messageService; private BasicUserModel _selectedUser; public BasicUserModel SelectedUser { get { return _selectedUser; } set { _selectedUser = value; RaisePropertyChanged(() => SelectedUser); } } private bool _isSaving; public bool IsSaving { get { return _isSaving; } private set { _isSaving = value; RaisePropertyChanged(() => IsSaving); } } private readonly CollectionViewModel _users = new CollectionViewModel(); public CollectionViewModel Users { get { return _users; } } public string Username { get; private set; } public string Repository { get; private set; } public long Id { get; private set; } public bool SaveOnSelect { get; private set; } public IssueAssignedToViewModel(IMessageService messageService) { _messageService = messageService; } public void Init(NavObject navObject) { Username = navObject.Username; Repository = navObject.Repository; Id = navObject.Id; SaveOnSelect = navObject.SaveOnSelect; SelectedUser = TxSevice.Get() as BasicUserModel; this.Bind(x => x.SelectedUser).Subscribe(x => SelectUser(x)); } private async Task SelectUser(BasicUserModel x) { if (SaveOnSelect) { try { IsSaving = true; var assignee = x != null ? x.Login : null; var updateReq = this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].UpdateAssignee(assignee); var newIssue = await this.GetApplication().Client.ExecuteAsync(updateReq); _messageService.Send(new IssueEditMessage(newIssue.Data)); } catch { DisplayAlert("Unable to assign issue to selected user! Please try again."); } finally { IsSaving = false; } } else { _messageService.Send(new SelectedAssignedToMessage(x)); } ChangePresentation(new MvxClosePresentationHint(this)); } protected override Task Load() { return Users.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].GetAssignees()); } public class NavObject { public string Username { get; set; } public string Repository { get; set; } public long Id { get; set; } public bool SaveOnSelect { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Issues/IssueEditViewModel.cs ================================================ using MvvmCross.Core.ViewModels; using System.Threading.Tasks; using GitHubSharp.Models; using System; using CodeHub.Core.Messages; using System.Linq; using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssueEditViewModel : IssueModifyViewModel { private readonly IMessageService _messageService; private IssueModel _issue; private bool _open; public bool IsOpen { get { return _open; } set { this.RaiseAndSetIfChanged(ref _open, value); } } public IssueModel Issue { get { return _issue; } set { this.RaiseAndSetIfChanged(ref _issue, value); } } public long Id { get; private set; } public IssueEditViewModel(IMessageService messageService) : base(messageService) { _messageService = messageService; } protected override async Task Save() { try { if (string.IsNullOrEmpty(IssueTitle)) throw new Exception("Issue must have a title!"); string assignedTo = AssignedTo == null ? null : AssignedTo.Login; int? milestone = null; if (Milestone != null) milestone = Milestone.Number; string[] labels = Labels.Items.Select(x => x.Name).ToArray(); var content = Content ?? string.Empty; var state = IsOpen ? "open" : "closed"; var retried = false; IsSaving = true; // For some reason github needs to try again during an internal server error tryagain: try { var data = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Issue.Number].Update(IssueTitle, content, state, assignedTo, milestone, labels)); _messageService.Send(new IssueEditMessage(data.Data)); } catch (GitHubSharp.InternalServerException) { if (retried) throw; //Do nothing. Something is wrong with github's service retried = true; goto tryagain; } ChangePresentation(new MvxClosePresentationHint(this)); } catch { DisplayAlert("Unable to save the issue! Please try again"); } finally { IsSaving = false; } // //There is a wierd bug in GitHub when editing an existing issue and the assignedTo is null // catch (GitHubSharp.InternalServerException) // { // if (ExistingIssue != null && assignedTo == null) // tryEditAgain = true; // else // throw; // } // // if (tryEditAgain) // { // var response = await Application.Client.ExecuteAsync(Application.Client.Users[Username].Repositories[RepoSlug].Issues[ExistingIssue.Number].Update(title, content, state, assignedTo, milestone, labels)); // model = response.Data; // } } // protected override Task Load(bool forceCacheInvalidation) // { // if (forceCacheInvalidation || Issue == null) // return Task.Run(() => this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].Get(), forceCacheInvalidation, response => Issue = response.Data)); // return Task.Delay(0); // } public void Init(NavObject navObject) { base.Init(navObject.Username, navObject.Repository); Id = navObject.Id; Issue = GetService().Get() as IssueModel; if (Issue != null) { IssueTitle = Issue.Title; AssignedTo = Issue.Assignee; Milestone = Issue.Milestone; Labels.Items.Reset(Issue.Labels); Content = Issue.Body; IsOpen = string.Equals(Issue.State, "open"); } } public class NavObject { public string Username { get; set; } public string Repository { get; set; } public long Id { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Issues/IssueLabelsViewModel.cs ================================================ using System.Threading.Tasks; using GitHubSharp.Models; using System.Collections.Generic; using CodeHub.Core.Messages; using System.Linq; using System.Windows.Input; using MvvmCross.Core.ViewModels; using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssueLabelsViewModel : LoadableViewModel { private readonly IMessageService _messageService; private IEnumerable _originalLables; private bool _isSaving; public bool IsSaving { get { return _isSaving; } private set { _isSaving = value; RaisePropertyChanged(() => IsSaving); } } private readonly CollectionViewModel _labels = new CollectionViewModel(); public CollectionViewModel Labels { get { return _labels; } } private readonly CollectionViewModel _selectedLabels = new CollectionViewModel(); public CollectionViewModel SelectedLabels { get { return _selectedLabels; } } public string Username { get; private set; } public string Repository { get; private set; } public long Id { get; private set; } public bool SaveOnSelect { get; private set; } public void Init(NavObject navObject) { Username = navObject.Username; Repository = navObject.Repository; Id = navObject.Id; SaveOnSelect = navObject.SaveOnSelect; _originalLables = GetService().Get() as IEnumerable; SelectedLabels.Items.Reset(_originalLables); } public ICommand SaveLabelChoices { get { return new MvxCommand(() => SelectLabels(SelectedLabels)); } } public IssueLabelsViewModel(IMessageService messageService) { _messageService = messageService; } private async Task SelectLabels(IEnumerable x) { //If nothing has changed, dont do anything... if (_originalLables != null && _originalLables.Count() == x.Count() && _originalLables.Intersect(x).Count() == x.Count()) { ChangePresentation(new MvxClosePresentationHint(this)); return; } if (SaveOnSelect) { try { IsSaving = true; var labels = x != null ? x.Select(y => y.Name).ToArray() : null; var updateReq = this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].UpdateLabels(labels); var newIssue = await this.GetApplication().Client.ExecuteAsync(updateReq); _messageService.Send(new IssueEditMessage(newIssue.Data)); } catch { DisplayAlert("Unable to save labels! Please try again."); } finally { IsSaving = false; } } else { _messageService.Send(new SelectIssueLabelsMessage(SelectedLabels.Items)); } ChangePresentation(new MvxClosePresentationHint(this)); } protected override Task Load() { return Labels.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].Labels.GetAll()); } public class NavObject { public string Username { get; set; } public string Repository { get; set; } public long Id { get; set; } public bool SaveOnSelect { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Issues/IssueMilestonesViewModel.cs ================================================ using System.Threading.Tasks; using GitHubSharp.Models; using CodeHub.Core.Messages; using System; using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssueMilestonesViewModel : LoadableViewModel { private readonly IMessageService _messageService; private MilestoneModel _selectedMilestone; public MilestoneModel SelectedMilestone { get { return _selectedMilestone; } set { _selectedMilestone = value; RaisePropertyChanged(() => SelectedMilestone); } } private bool _isSaving; public bool IsSaving { get { return _isSaving; } private set { _isSaving = value; RaisePropertyChanged(() => IsSaving); } } private readonly CollectionViewModel _milestones = new CollectionViewModel(); public CollectionViewModel Milestones { get { return _milestones; } } public string Username { get; private set; } public string Repository { get; private set; } public long Id { get; private set; } public bool SaveOnSelect { get; private set; } public IssueMilestonesViewModel(IMessageService messageService) { _messageService = messageService; } public void Init(NavObject navObject) { Username = navObject.Username; Repository = navObject.Repository; Id = navObject.Id; SaveOnSelect = navObject.SaveOnSelect; SelectedMilestone = TxSevice.Get() as MilestoneModel; this.Bind(x => x.SelectedMilestone).Subscribe(x => SelectMilestone(x)); } private async Task SelectMilestone(MilestoneModel x) { if (SaveOnSelect) { try { IsSaving = true; int? milestone = null; if (x != null) milestone = x.Number; var updateReq = this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].UpdateMilestone(milestone); var newIssue = await this.GetApplication().Client.ExecuteAsync(updateReq); _messageService.Send(new IssueEditMessage(newIssue.Data)); } catch { DisplayAlert("Unable to to save milestone! Please try again."); } finally { IsSaving = false; } } else { _messageService.Send(new SelectedMilestoneMessage(x)); } ChangePresentation(new MvvmCross.Core.ViewModels.MvxClosePresentationHint(this)); } protected override Task Load() { return Milestones.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].Milestones.GetAll()); } public class NavObject { public string Username { get; set; } public string Repository { get; set; } public long Id { get; set; } public bool SaveOnSelect { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Issues/IssueModifyViewModel.cs ================================================ using System; using GitHubSharp.Models; using System.Windows.Input; using MvvmCross.Core.ViewModels; using System.Threading.Tasks; using CodeHub.Core.Messages; using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public abstract class IssueModifyViewModel : BaseViewModel { private readonly IMessageService _messageService; private string _title; private string _content; private BasicUserModel _assignedTo; private readonly CollectionViewModel _labels = new CollectionViewModel(); private MilestoneModel _milestone; private IDisposable _labelsToken, _milestoneToken, _assignedToken; private bool _isSaving; public string IssueTitle { get { return _title; } set { this.RaiseAndSetIfChanged(ref _title, value); } } public string Content { get { return _content; } set { this.RaiseAndSetIfChanged(ref _content, value); } } public MilestoneModel Milestone { get { return _milestone; } set { this.RaiseAndSetIfChanged(ref _milestone, value); } } public CollectionViewModel Labels { get { return _labels; } } public BasicUserModel AssignedTo { get { return _assignedTo; } set { this.RaiseAndSetIfChanged(ref _assignedTo, value); } } public bool IsSaving { get { return _isSaving; } protected set { this.RaiseAndSetIfChanged(ref _isSaving, value); } } public string Username { get; private set; } public string Repository { get; private set; } public ICommand GoToLabelsCommand { get { return new MvxCommand(() => { GetService().Add(Labels); ShowViewModel(new IssueLabelsViewModel.NavObject { Username = Username, Repository = Repository }); }); } } public ICommand GoToMilestonesCommand { get { return new MvxCommand(() => { GetService().Add(Milestone); ShowViewModel(new IssueMilestonesViewModel.NavObject { Username = Username, Repository = Repository }); }); } } public ICommand GoToAssigneeCommand { get { return new MvxCommand(() => { GetService().Add(AssignedTo); ShowViewModel(new IssueAssignedToViewModel.NavObject { Username = Username, Repository = Repository }); }); } } public ICommand SaveCommand { get { return new MvxCommand(() => Save()); } } public IssueModifyViewModel(IMessageService messageService) { _messageService = messageService; } protected void Init(string username, string repository) { Username = username; Repository = repository; _labelsToken = _messageService.Listen(x => Labels.Items.Reset(x.Labels)); _milestoneToken = _messageService.Listen(x => Milestone = x.Milestone); _assignedToken = _messageService.Listen(x => AssignedTo = x.User); } protected abstract Task Save(); } } ================================================ FILE: CodeHub.Core/ViewModels/Issues/IssueViewModel.cs ================================================ using System.Threading.Tasks; using GitHubSharp.Models; using System.Windows.Input; using CodeHub.Core.Messages; using CodeHub.Core.Services; using System; using MvvmCross.Core.ViewModels; using System.Reactive.Linq; using CodeHub.Core.ViewModels.User; using System.Reactive; using Splat; using System.Reactive.Threading.Tasks; using System.Collections.Generic; using System.Linq; namespace CodeHub.Core.ViewModels.Issues { public class IssueViewModel : LoadableViewModel { private IDisposable _editToken; private readonly IFeaturesService _featuresService; private readonly IApplicationService _applicationService; private readonly IMessageService _messageService; private readonly IMarkdownService _markdownService; public long Id { get; private set; } public string Username { get; private set; } public string Repository { get; private set; } private string _markdownDescription; public string MarkdownDescription { get { return _markdownDescription; } private set { this.RaiseAndSetIfChanged(ref _markdownDescription, value); } } private bool? _isClosed; public bool? IsClosed { get { return _isClosed; } private set { this.RaiseAndSetIfChanged(ref _isClosed, value); } } private bool _shouldShowPro; public bool ShouldShowPro { get { return _shouldShowPro; } protected set { this.RaiseAndSetIfChanged(ref _shouldShowPro, value); } } private bool _isCollaborator; public bool IsCollaborator { get { return _isCollaborator; } private set { this.RaiseAndSetIfChanged(ref _isCollaborator, value); } } private IssueModel _issueModel; public IssueModel Issue { get { return _issueModel; } private set { this.RaiseAndSetIfChanged(ref _issueModel, value); } } private bool _isModifying; public bool IsModifying { get { return _isModifying; } set { this.RaiseAndSetIfChanged(ref _isModifying, value); } } private int? _participants; public int? Participants { get { return _participants; } set { this.RaiseAndSetIfChanged(ref _participants, value); } } private IReadOnlyList _comments; public IReadOnlyList Comments { get { return _comments ?? new List(); } private set { this.RaiseAndSetIfChanged(ref _comments, value); } } private IReadOnlyList _events; public IReadOnlyList Events { get { return _events ?? new List(); } private set { this.RaiseAndSetIfChanged(ref _events, value); } } public ReactiveUI.ReactiveCommand GoToOwner { get; } public ICommand GoToAssigneeCommand { get { return new MvxCommand(() => { GetService().Add(Issue.Assignee); ShowViewModel(new IssueAssignedToViewModel.NavObject { Username = Username, Repository = Repository, Id = Id, SaveOnSelect = true }); }, () => IsCollaborator); } } public ICommand GoToMilestoneCommand { get { return new MvxCommand(() => { GetService().Add(Issue.Milestone); ShowViewModel(new IssueMilestonesViewModel.NavObject { Username = Username, Repository = Repository, Id = Id, SaveOnSelect = true }); }, () => IsCollaborator); } } public ICommand GoToLabelsCommand { get { return new MvxCommand(() => { GetService().Add(Issue.Labels); ShowViewModel(new IssueLabelsViewModel.NavObject { Username = Username, Repository = Repository, Id = Id, SaveOnSelect = true }); }, () => IsCollaborator); } } public ICommand GoToEditCommand { get { return new MvxCommand(() => { GetService().Add(Issue); ShowViewModel(new IssueEditViewModel.NavObject { Username = Username, Repository = Repository, Id = Id }); }, () => Issue != null && IsCollaborator); } } public ICommand ToggleStateCommand { get { return new MvxCommand(() => ToggleState(Issue.State == "open"), () => Issue != null); } } protected override Task Load() { if (_featuresService.IsProEnabled) ShouldShowPro = false; else { _applicationService .GitHubClient.Repository.Get(Username, Repository) .ToBackground(x => ShouldShowPro = x.Private && !_featuresService.IsProEnabled); } _applicationService .GitHubClient.Issue.Comment.GetAllForIssue(Username, Repository, (int)Id) .ToBackground(x => Comments = x); _applicationService .GitHubClient.Issue.Events.GetAllForIssue(Username, Repository, (int)Id) .ToBackground(events => { Events = events; Participants = events.Select(x => x.Actor?.Login).Distinct().Count(); }); _applicationService .GitHubClient.Repository.Collaborator.IsCollaborator(Username, Repository, _applicationService.Account.Username) .ToBackground(x => IsCollaborator = x); return this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].Get(), response => Issue = response.Data); } public IssueViewModel( IApplicationService applicationService = null, IFeaturesService featuresService = null, IMessageService messageService = null, IMarkdownService markdownService = null) { _applicationService = applicationService ?? Locator.Current.GetService(); _featuresService = featuresService ?? Locator.Current.GetService(); _messageService = messageService ?? Locator.Current.GetService(); _markdownService = markdownService ?? Locator.Current.GetService(); this.Bind(x => x.Issue, true) .Where(x => x != null) .Select(x => string.Equals(x.State, "closed")) .Subscribe(x => IsClosed = x); this.Bind(x => x.Issue, true) .SelectMany(issue => _markdownService.Convert(issue?.Body).ToObservable()) .Subscribe(x => MarkdownDescription = x); GoToOwner = ReactiveUI.ReactiveCommand.Create( () => ShowViewModel(new UserViewModel.NavObject { Username = Issue?.User?.Login }), this.Bind(x => x.Issue, true).Select(x => x != null)); } public void Init(NavObject navObject) { Username = navObject.Username; Repository = navObject.Repository; Id = navObject.Id; _editToken = _messageService.Listen(x => { if (x.Issue == null || x.Issue.Number != Issue.Number) return; Issue = x.Issue; }); } public async Task AddComment(string text) { try { var comment = await _applicationService.GitHubClient.Issue.Comment.Create(Username, Repository, (int)Id, text); var newCommentList = new List(Comments) { comment }; Comments = newCommentList; return true; } catch (Exception e) { DisplayAlert(e.Message); return false; } } private async Task ToggleState(bool closed) { try { IsModifying = true; var data = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Issue.Number].UpdateState(closed ? "closed" : "open")); _messageService.Send(new IssueEditMessage(data.Data)); } catch (Exception e) { DisplayAlert("Unable to " + (closed ? "close" : "open") + " the item. " + e.Message); } finally { IsModifying = false; } } public class NavObject { public string Username { get; set; } public string Repository { get; set; } public long Id { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Issues/IssuesViewModel.cs ================================================ using System; using System.Threading.Tasks; using CodeHub.Core.Filters; using GitHubSharp.Models; using System.Windows.Input; using MvvmCross.Core.ViewModels; using CodeHub.Core.Messages; using System.Linq; using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class IssuesViewModel : BaseIssuesViewModel { private readonly IMessageService _messageService; private IDisposable _addToken, _editToken; public string Username { get; private set; } public string Repository { get; private set; } public ICommand GoToNewIssueCommand { get { return new MvxCommand(() => ShowViewModel(new IssueAddViewModel.NavObject { Username = Username, Repository = Repository })); } } public IssuesViewModel(IMessageService messageService) { _messageService = messageService; } public void Init(NavObject nav) { Username = nav.Username; Repository = nav.Repository; _issues = new FilterableCollectionViewModel("IssuesViewModel:" + Username + "/" + Repository); _issues.GroupingFunction = Group; _issues.Bind(x => x.Filter).Subscribe(_ => LoadCommand.Execute(true)); _addToken = _messageService.Listen(x => { if (x.Issue == null || !DoesIssueBelong(x.Issue)) return; Issues.Items.Insert(0, x.Issue); }); _editToken = _messageService.Listen(x => { if (x.Issue == null || !DoesIssueBelong(x.Issue)) return; var item = Issues.Items.FirstOrDefault(y => y.Number == x.Issue.Number); if (item == null) return; var index = Issues.Items.IndexOf(item); using (Issues.DeferRefresh()) { Issues.Items.RemoveAt(index); Issues.Items.Insert(index, x.Issue); } }); } protected override Task Load() { string direction = _issues.Filter.Ascending ? "asc" : "desc"; string state = _issues.Filter.Open ? "open" : "closed"; string sort = _issues.Filter.SortType == IssuesFilterModel.Sort.None ? null : _issues.Filter.SortType.ToString().ToLower(); string labels = string.IsNullOrEmpty(_issues.Filter.Labels) ? null : _issues.Filter.Labels; string assignee = string.IsNullOrEmpty(_issues.Filter.Assignee) ? null : _issues.Filter.Assignee; string creator = string.IsNullOrEmpty(_issues.Filter.Creator) ? null : _issues.Filter.Creator; string mentioned = string.IsNullOrEmpty(_issues.Filter.Mentioned) ? null : _issues.Filter.Mentioned; string milestone = _issues.Filter.Milestone == null ? null : _issues.Filter.Milestone.Value; var request = this.GetApplication().Client.Users[Username].Repositories[Repository].Issues.GetAll(sort: sort, labels: labels, state: state, direction: direction, assignee: assignee, creator: creator, mentioned: mentioned, milestone: milestone); return Issues.SimpleCollectionLoad(request); } public void CreateIssue(IssueModel issue) { if (!DoesIssueBelong(issue)) return; Issues.Items.Add(issue); } private bool DoesIssueBelong(IssueModel model) { if (Issues.Filter == null) return true; if (Issues.Filter.Open != model.State.Equals("open")) return false; return true; } public class NavObject { public string Username { get; set; } public string Repository { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Issues/MyIssuesViewModel.cs ================================================ using System.Threading.Tasks; using CodeHub.Core.Filters; using GitHubSharp.Models; using System.Collections.Generic; using System.Linq; using System; using CodeHub.Core.Messages; using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.Issues { public class MyIssuesViewModel : BaseIssuesViewModel { private IDisposable _editToken; private int _selectedFilter; public int SelectedFilter { get { return _selectedFilter; } set { _selectedFilter = value; RaisePropertyChanged(() => SelectedFilter); } } public MyIssuesViewModel(IMessageService messageService = null) { messageService = messageService ?? GetService(); _issues = new FilterableCollectionViewModel("MyIssues"); _issues.GroupingFunction = Group; _issues.Bind(x => x.Filter).Subscribe(_ => LoadCommand.Execute(false)); this.Bind(x => x.SelectedFilter).Subscribe(x => { if (x == 0) _issues.Filter = MyIssuesFilterModel.CreateOpenFilter(); else if (x == 1) _issues.Filter = MyIssuesFilterModel.CreateClosedFilter(); }); _editToken = messageService.Listen(x => { if (x.Issue == null) return; var item = Issues.Items.FirstOrDefault(y => y.Number == x.Issue.Number); if (item == null) return; var index = Issues.Items.IndexOf(item); using (Issues.DeferRefresh()) { Issues.Items.RemoveAt(index); Issues.Items.Insert(index, x.Issue); } }); } protected override List> Group(IEnumerable model) { var group = base.Group(model); if (group == null) { try { var regex = new System.Text.RegularExpressions.Regex("repos/(.+)/issues/"); return model.GroupBy(x => regex.Match(x.Url).Groups[1].Value).ToList(); } catch { return null; } } return group; } protected override Task Load() { string filter = Issues.Filter.FilterType.ToString().ToLower(); string direction = Issues.Filter.Ascending ? "asc" : "desc"; string state = Issues.Filter.Open ? "open" : "closed"; string sort = Issues.Filter.SortType == MyIssuesFilterModel.Sort.None ? null : Issues.Filter.SortType.ToString().ToLower(); string labels = string.IsNullOrEmpty(Issues.Filter.Labels) ? null : Issues.Filter.Labels; var request = this.GetApplication().Client.AuthenticatedUser.Issues.GetAll(sort: sort, labels: labels, state: state, direction: direction, filter: filter); return Issues.SimpleCollectionLoad(request); } } } ================================================ FILE: CodeHub.Core/ViewModels/LoadableViewModel.cs ================================================ using GitHubSharp; using System.Threading.Tasks; using MvvmCross.Core.ViewModels; using CodeHub.Core.ViewModels; using System.Windows.Input; using System.Net; using System; namespace CodeHub.Core.ViewModels { public abstract class LoadableViewModel : BaseViewModel { public ICommand LoadCommand { get; } private bool _isLoading; public bool IsLoading { get { return _isLoading; } private set { this.RaiseAndSetIfChanged(ref _isLoading, value); } } private async Task LoadResource() { var retry = false; while (true) { if (retry) await Task.Delay(100); try { await Load(); return; } catch (WebException) { if (!retry) retry = true; else throw; } } } protected async Task ExecuteLoadResource() { try { await LoadResource(); } catch (System.IO.IOException) { DisplayAlert("Unable to communicate with GitHub as the transmission was interrupted! Please try again."); } catch (StatusCodeException e) { DisplayAlert(e.Message); } } protected LoadableViewModel() { LoadCommand = new MvxCommand(x => HandleLoadCommand(), _ => !IsLoading); } private async Task HandleLoadCommand() { try { IsLoading = true; await ExecuteLoadResource(); } catch (OperationCanceledException e) { // The operation was canceled... Don't worry System.Diagnostics.Debug.WriteLine("The operation was canceled: " + e.Message); } catch (Exception e) { DisplayAlert("The request to load this item did not complete successfuly! " + e.Message); } finally { IsLoading = false; } } protected abstract Task Load(); } } ================================================ FILE: CodeHub.Core/ViewModels/MarkdownAccessoryViewModel.cs ================================================ using System; using ReactiveUI; using CodeHub.Core.Services; using System.IO; using System.Threading.Tasks; using System.Reactive.Linq; using System.Reactive; using Splat; using Plugin.Media.Abstractions; namespace CodeHub.Core.ViewModels { public class MarkdownAccessoryViewModel : ReactiveObject { private const string IMGUR_UPLOAD_WARN = "IMGUR_UPLOAD_WARN"; private const string IMGUR_UPLOAD_WARN_MESSAGE = "Because GitHub's image upload API is not public images you upload here are hosted by Imgur. " + "Please be aware of this when posting confidential information"; public ReactiveCommand PostToImgurCommand { get; private set; } public MarkdownAccessoryViewModel( IImgurService imgurService = null, IMedia mediaPicker = null, IAlertDialogService alertDialog = null) { imgurService = imgurService ?? Locator.Current.GetService(); mediaPicker = mediaPicker ?? Plugin.Media.CrossMedia.Current; alertDialog = alertDialog ?? Locator.Current.GetService(); PostToImgurCommand = ReactiveCommand.CreateFromTask(async _ => { if (!Settings.HasSeenImgurUploadWarn) { Settings.HasSeenImgurUploadWarn = true; await alertDialog.Alert("Please Read!", IMGUR_UPLOAD_WARN_MESSAGE); } var photo = await mediaPicker.PickPhotoAsync(new PickMediaOptions { CompressionQuality = 80 }); var memoryStream = new MemoryStream(); await photo.GetStream().CopyToAsync(memoryStream); using (alertDialog.Activate("Uploading...")) { var model = await imgurService.SendImage(memoryStream.ToArray()); if (model == null || model.Data == null || model.Data.Link == null) throw new InvalidOperationException("Unable to upload to Imgur. Please try again later."); return model.Data.Link; } }); PostToImgurCommand.ThrownExceptions .Where(x => !(x is TaskCanceledException)) .Subscribe(x => alertDialog.Alert("Upload Error", x.Message)); } } } ================================================ FILE: CodeHub.Core/ViewModels/Notifications/NotificationsViewModel.cs ================================================ using System; using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using MvvmCross.Core.ViewModels; using CodeHub.Core.Filters; using CodeHub.Core.ViewModels.Issues; using CodeHub.Core.ViewModels.PullRequests; using CodeHub.Core.Messages; using CodeHub.Core.ViewModels.Changesets; using CodeHub.Core.Utils; using CodeHub.Core.Services; using CodeHub.Core.ViewModels.Repositories; namespace CodeHub.Core.ViewModels.Notifications { public class NotificationsViewModel : LoadableViewModel { private readonly IApplicationService _applicationService; private readonly IMessageService _messageService; private readonly FilterableCollectionViewModel _notifications; private ICommand _readAllCommand; private ICommand _readReposCommand; private int _shownIndex; private bool _isMarking; public FilterableCollectionViewModel Notifications { get { return _notifications; } } public int ShownIndex { get { return _shownIndex; } set { this.RaiseAndSetIfChanged(ref _shownIndex, value); } } public bool IsMarking { get { return _isMarking; } set { this.RaiseAndSetIfChanged(ref _isMarking, value); } } public ICommand ReadRepositoriesCommand { get { return _readReposCommand ?? (_readReposCommand = new MvxAsyncCommand(x => MarkRepoAsRead(x))); } } public ICommand ReadAllCommand { get { return _readAllCommand ?? (_readAllCommand = new MvxAsyncCommand(() => MarkAllAsRead(), () => ShownIndex != 2 && !IsLoading && !IsMarking && Notifications.Any())); } } public ICommand GoToNotificationCommand { get { return new MvxCommand(GoToNotification); } } private void GoToNotification(Octokit.Notification x) { var subject = x.Subject.Type.ToLower(); if (subject.Equals("issue")) { Read(x).ToBackground(); var node = x.Subject.Url.Substring(x.Subject.Url.LastIndexOf('/') + 1); ShowViewModel(new IssueViewModel.NavObject { Username = x.Repository.Owner.Login,Repository = x.Repository.Name, Id = long.Parse(node) }); } else if (subject.Equals("pullrequest")) { Read(x).ToBackground(); var node = x.Subject.Url.Substring(x.Subject.Url.LastIndexOf('/') + 1); ShowViewModel(new PullRequestViewModel.NavObject { Username = x.Repository.Owner.Login, Repository = x.Repository.Name, Id = long.Parse(node) }); } else if (subject.Equals("commit")) { Read(x).ToBackground(); var node = x.Subject.Url.Substring(x.Subject.Url.LastIndexOf('/') + 1); ShowViewModel(new ChangesetViewModel.NavObject { Username = x.Repository.Owner.Login, Repository = x.Repository.Name, Node = node }); } else if (subject.Equals("release")) { Read(x).ToBackground(); ShowViewModel(new RepositoryViewModel.NavObject { Username = x.Repository.Owner.Login, Repository = x.Repository.Name }); } } public NotificationsViewModel( IMessageService messageService = null, IApplicationService applicationService = null) { _messageService = messageService ?? GetService(); _applicationService = applicationService ?? GetService(); _notifications = new FilterableCollectionViewModel("Notifications"); _notifications.GroupingFunction = (n) => n.GroupBy(x => x.Repository.FullName); _notifications.Bind(x => x.Filter).Subscribe(_ => LoadCommand.Execute(false)); this.Bind(x => x.ShownIndex).Subscribe(x => { if (x == 0) _notifications.Filter = NotificationsFilterModel.CreateUnreadFilter(); else if (x == 1) _notifications.Filter = NotificationsFilterModel.CreateParticipatingFilter(); else _notifications.Filter = NotificationsFilterModel.CreateAllFilter(); ((IMvxCommand)ReadAllCommand).RaiseCanExecuteChanged(); }); this.Bind(x => x.IsLoading).Subscribe(_ => ((IMvxCommand)ReadAllCommand).RaiseCanExecuteChanged()); if (_notifications.Filter.Equals(NotificationsFilterModel.CreateUnreadFilter())) _shownIndex = 0; else if (_notifications.Filter.Equals(NotificationsFilterModel.CreateParticipatingFilter())) _shownIndex = 1; else _shownIndex = 2; } protected override async Task Load() { var req = new Octokit.NotificationsRequest { All = Notifications.Filter.All, Participating = Notifications.Filter.Participating }; var notifications = await _applicationService.GitHubClient.Activity.Notifications.GetAllForCurrent(req); Notifications.Items.Reset(notifications); UpdateAccountNotificationsCount(); } private async Task Read(Octokit.Notification model) { // If its already read, ignore it if (!model.Unread) return; try { if (!int.TryParse(model.Id, out int id)) return; await _applicationService.GitHubClient.Activity.Notifications.MarkAsRead(id); if (_shownIndex != 2) Notifications.Items.Remove(model); UpdateAccountNotificationsCount(); } catch { DisplayAlert("Unable to mark notification as read. Please try again."); } } private async Task MarkRepoAsRead(string repo) { try { IsMarking = true; var repoId = RepositoryIdentifier.FromFullName(repo); if (repoId == null) return; await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Notifications.MarkRepoAsRead(repoId.Owner, repoId.Name)); Notifications.Items.RemoveRange(Notifications.Items.Where(x => string.Equals(x.Repository.FullName, repo, StringComparison.OrdinalIgnoreCase)).ToList()); UpdateAccountNotificationsCount(); } catch { DisplayAlert("Unable to mark repositories' notifications as read. Please try again."); } finally { IsMarking = false; } } private async Task MarkAllAsRead() { // Make sure theres some sort of notification if (!Notifications.Any()) return; try { IsMarking = true; await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Notifications.MarkAsRead()); Notifications.Items.Clear(); UpdateAccountNotificationsCount(); } catch { DisplayAlert("Unable to mark all notifications as read. Please try again."); } finally { IsMarking = false; } } private void UpdateAccountNotificationsCount() { // Only update if we're looking at if (!Notifications.Filter.All && !Notifications.Filter.Participating) { var count = Notifications.Items.Sum(x => x.Unread ? 1 : 0); _messageService.Send(new NotificationCountMessage(count)); } } } } ================================================ FILE: CodeHub.Core/ViewModels/Organizations/OrganizationViewModel.cs ================================================ using System.Threading.Tasks; using System.Windows.Input; using MvvmCross.Core.ViewModels; using CodeHub.Core.ViewModels.Events; using GitHubSharp.Models; namespace CodeHub.Core.ViewModels.Organizations { public class OrganizationViewModel : LoadableViewModel { private UserModel _userModel; public string Name { get; private set; } public void Init(NavObject navObject) { Name = navObject.Name; } public UserModel Organization { get { return _userModel; } private set { this.RaiseAndSetIfChanged(ref _userModel, value); } } public ICommand GoToTeamsCommand { get { return new MvxCommand(() => ShowViewModel(new TeamsViewModel.NavObject { Name = Name })); } } public ICommand GoToEventsCommand { get { return new MvxCommand(() => ShowViewModel(new UserEventsViewModel.NavObject { Username = Name })); } } protected override Task Load() { return this.RequestModel(this.GetApplication().Client.Organizations[Name].Get(), response => Organization = response.Data); } public class NavObject { public string Name { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Organizations/OrganizationsViewModel.cs ================================================ using System.Threading.Tasks; using System.Windows.Input; using CodeHub.Core.ViewModels; using GitHubSharp.Models; using MvvmCross.Core.ViewModels; namespace CodeHub.Core.ViewModels.Organizations { public class OrganizationsViewModel : LoadableViewModel { public CollectionViewModel Organizations { get; } public string Username { get; private set; } public void Init(NavObject navObject) { Username = navObject.Username; } public OrganizationsViewModel() { Title = "Organizations"; Organizations = new CollectionViewModel(); } public ICommand GoToOrganizationCommand { get { return new MvxCommand(x => ShowViewModel(new OrganizationViewModel.NavObject { Name = x.Login }));} } protected override Task Load() { return Organizations.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].GetOrganizations()); } public class NavObject { public string Username { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Organizations/TeamsViewModel.cs ================================================ using GitHubSharp.Models; using System.Threading.Tasks; namespace CodeHub.Core.ViewModels.Organizations { public class TeamsViewModel : LoadableViewModel { public CollectionViewModel Teams { get; } public string OrganizationName { get; private set; } public TeamsViewModel() { Title = "Teams"; Teams = new CollectionViewModel(); } public void Init(NavObject navObject) { OrganizationName = navObject.Name; } protected override Task Load() { return Teams.SimpleCollectionLoad(this.GetApplication().Client.Organizations[OrganizationName].GetTeams()); } public class NavObject { public string Name { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/PullRequests/PullRequestCommitsViewModel.cs ================================================ using GitHubSharp.Models; using GitHubSharp; using System.Collections.Generic; using CodeHub.Core.ViewModels.Changesets; using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.PullRequests { public class PullRequestCommitsViewModel : ChangesetsViewModel { public long PullRequestId { get; private set; } public PullRequestCommitsViewModel(IApplicationService applicationService, IFeaturesService featuresService) : base(applicationService, featuresService) { } public void Init(NavObject navObject) { base.Init(navObject); PullRequestId = navObject.PullRequestId; } protected override GitHubRequest> GetRequest() { return this.GetApplication().Client.Users[Username].Repositories[Repository].PullRequests[PullRequestId].GetCommits(); } public new class NavObject : CommitsViewModel.NavObject { public long PullRequestId { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/PullRequests/PullRequestFilesViewModel.cs ================================================ using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using CodeHub.Core.Services; using CodeHub.Core.ViewModels.Source; using GitHubSharp.Models; using MvvmCross.Core.ViewModels; using MvvmCross.Platform; namespace CodeHub.Core.ViewModels.PullRequests { public class PullRequestFilesViewModel : LoadableViewModel { private readonly CollectionViewModel _files = new CollectionViewModel(); public CollectionViewModel Files { get { return _files; } } public long PullRequestId { get; private set; } public string Username { get; private set; } public string Repository { get; private set; } public string Sha { get; private set; } public void Init(NavObject navObject) { Username = navObject.Username; Repository = navObject.Repository; PullRequestId = navObject.PullRequestId; Sha = navObject.Sha; _files.GroupingFunction = (x) => x.GroupBy(y => { var filename = "/" + y.Filename; return filename.Substring(0, filename.LastIndexOf("/", System.StringComparison.Ordinal) + 1); }).OrderBy(y => y.Key); } protected override Task Load() { return Files.SimpleCollectionLoad(this.GetApplication().Client.Users[Username].Repositories[Repository].PullRequests[PullRequestId].GetFiles()); } public class NavObject { public string Username { get; set; } public string Repository { get; set; } public string Sha { get; set; } public long PullRequestId { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/PullRequests/PullRequestViewModel.cs ================================================ using System; using System.Threading.Tasks; using System.Windows.Input; using MvvmCross.Core.ViewModels; using GitHubSharp.Models; using CodeHub.Core.Services; using CodeHub.Core.ViewModels.Issues; using CodeHub.Core.Messages; using System.Reactive.Linq; using CodeHub.Core.ViewModels.User; using System.Reactive; using System.Reactive.Threading.Tasks; using System.Collections.Generic; namespace CodeHub.Core.ViewModels.PullRequests { public class PullRequestViewModel : LoadableViewModel { private readonly IApplicationService _applicationService; private readonly IMessageService _messageService; private IDisposable _issueEditSubscription; private IDisposable _pullRequestEditSubscription; private readonly IFeaturesService _featuresService; private readonly IMarkdownService _markdownService; public long Id { get; private set; } public string Username { get; private set; } public string Repository { get; private set; } private string _markdownDescription; public string MarkdownDescription { get { return _markdownDescription; } private set { this.RaiseAndSetIfChanged(ref _markdownDescription, value); } } private bool _canPush; public bool CanPush { get { return _canPush; } private set { this.RaiseAndSetIfChanged(ref _canPush, value); } } private bool _isCollaborator; public bool IsCollaborator { get { return _isCollaborator; } private set { this.RaiseAndSetIfChanged(ref _isCollaborator, value); } } private bool _merged; public bool Merged { get { return _merged; } set { this.RaiseAndSetIfChanged(ref _merged, value); } } private IssueModel _issueModel; public IssueModel Issue { get { return _issueModel; } private set { this.RaiseAndSetIfChanged(ref _issueModel, value); } } private PullRequestModel _model; public PullRequestModel PullRequest { get { return _model; } private set { this.RaiseAndSetIfChanged(ref _model, value); } } private bool _isModifying; public bool IsModifying { get { return _isModifying; } set { this.RaiseAndSetIfChanged(ref _isModifying, value); } } private bool? _isClosed; public bool? IsClosed { get { return _isClosed; } private set { this.RaiseAndSetIfChanged(ref _isClosed, value); } } private IReadOnlyList _comments; public IReadOnlyList Comments { get { return _comments ?? new List(); } private set { this.RaiseAndSetIfChanged(ref _comments, value); } } private IReadOnlyList _events; public IReadOnlyList Events { get { return _events ?? new List(); } private set { this.RaiseAndSetIfChanged(ref _events, value); } } private ICommand _goToAssigneeCommand; public ICommand GoToAssigneeCommand { get { if (_goToAssigneeCommand == null) { var cmd = new MvxCommand(() => { GetService().Add(Issue.Assignee); ShowViewModel(new IssueAssignedToViewModel.NavObject { Username = Username, Repository = Repository, Id = Id, SaveOnSelect = true }); }, () => Issue != null && IsCollaborator); this.Bind(x => Issue).Subscribe(_ => cmd.RaiseCanExecuteChanged()); _goToAssigneeCommand = cmd; } return _goToAssigneeCommand; } } private ICommand _goToMilestoneCommand; public ICommand GoToMilestoneCommand { get { if (_goToMilestoneCommand == null) { var cmd = new MvxCommand(() => { GetService().Add(Issue.Milestone); ShowViewModel(new IssueMilestonesViewModel.NavObject { Username = Username, Repository = Repository, Id = Id, SaveOnSelect = true }); }, () => Issue != null && IsCollaborator); this.Bind(x => Issue).Subscribe(_ => cmd.RaiseCanExecuteChanged()); _goToMilestoneCommand = cmd; } return _goToMilestoneCommand; } } private ICommand _goToLabelsCommand; public ICommand GoToLabelsCommand { get { if (_goToLabelsCommand == null) { var cmd = new MvxCommand(() => { GetService().Add(Issue.Labels); ShowViewModel(new IssueLabelsViewModel.NavObject { Username = Username, Repository = Repository, Id = Id, SaveOnSelect = true }); }, () => Issue != null && IsCollaborator); this.Bind(x => Issue).Subscribe(_ => cmd.RaiseCanExecuteChanged()); _goToLabelsCommand = cmd; } return _goToLabelsCommand; } } public ICommand GoToEditCommand { get { return new MvxCommand(() => { GetService().Add(Issue); ShowViewModel(new IssueEditViewModel.NavObject { Username = Username, Repository = Repository, Id = Id }); }, () => Issue != null && IsCollaborator); } } public ICommand ToggleStateCommand { get { return new MvxCommand(() => ToggleState(PullRequest.State == "open")); } } public ReactiveUI.ReactiveCommand GoToOwner { get; } public ICommand GoToCommitsCommand { get { return new MvxCommand(() => ShowViewModel(new PullRequestCommitsViewModel.NavObject { Username = Username, Repository = Repository, PullRequestId = Id })); } } public ICommand GoToFilesCommand { get { return new MvxCommand(() => { ShowViewModel(new PullRequestFilesViewModel.NavObject { Username = Username, Repository = Repository, PullRequestId = Id, Sha = PullRequest.Head?.Sha }); }); } } public PullRequestViewModel( IApplicationService applicationService, IFeaturesService featuresService, IMessageService messageService, IMarkdownService markdownService) { _applicationService = applicationService; _featuresService = featuresService; _messageService = messageService; _markdownService = markdownService; this.Bind(x => x.PullRequest, true) .Where(x => x != null) .Select(x => string.Equals(x.State, "closed")) .Subscribe(x => IsClosed = x); this.Bind(x => x.Issue, true) .SelectMany(issue => _markdownService.Convert(issue?.Body).ToObservable()) .Subscribe(x => MarkdownDescription = x); GoToOwner = ReactiveUI.ReactiveCommand.Create( () => ShowViewModel(new UserViewModel.NavObject { Username = Issue?.User?.Login }), this.Bind(x => x.Issue, true).Select(x => x != null)); } public void Init(NavObject navObject) { Username = navObject.Username; Repository = navObject.Repository; Id = navObject.Id; _issueEditSubscription = _messageService.Listen(x => { if (x.Issue == null || x.Issue.Number != Id) return; Issue = x.Issue; }); _pullRequestEditSubscription = _messageService.Listen(x => { if (x.PullRequest == null || x.PullRequest.Number != Id) return; PullRequest = x.PullRequest; }); } public async Task AddComment(string text) { try { var comment = await _applicationService.GitHubClient.Issue.Comment.Create(Username, Repository, (int)Id, text); var newCommentList = new List(Comments) { comment }; Comments = newCommentList; return true; } catch (Exception e) { DisplayAlert(e.Message); return false; } } private async Task ToggleState(bool closed) { try { IsModifying = true; var data = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].PullRequests[Id].UpdateState(closed ? "closed" : "open")); _messageService.Send(new PullRequestEditMessage(data.Data)); } catch (Exception e) { DisplayAlert("Unable to " + (closed ? "close" : "open") + " the item. " + e.Message); } finally { IsModifying = false; } } private bool _shouldShowPro; public bool ShouldShowPro { get { return _shouldShowPro; } protected set { this.RaiseAndSetIfChanged(ref _shouldShowPro, value); } } protected override Task Load() { ShouldShowPro = false; _applicationService .GitHubClient.Issue.Comment.GetAllForIssue(Username, Repository, (int)Id) .ToBackground(x => Comments = x); _applicationService .GitHubClient.Issue.Events.GetAllForIssue(Username, Repository, (int)Id) .ToBackground(x => Events = x); var pullRequest = this.GetApplication().Client.Users[Username].Repositories[Repository].PullRequests[Id].Get(); var t1 = this.RequestModel(pullRequest, response => PullRequest = response.Data); this.RequestModel(this.GetApplication().Client.Users[Username].Repositories[Repository].Issues[Id].Get(), response => Issue = response.Data).ToBackground(); _applicationService .GitHubClient.Repository.Get(Username, Repository) .ToBackground(x => { CanPush = x.Permissions.Push; ShouldShowPro = x.Private && !_featuresService.IsProEnabled; }); _applicationService .GitHubClient.Repository.Collaborator.IsCollaborator(Username, Repository, _applicationService.Account.Username) .ToBackground(x => IsCollaborator = x); return t1; } public async Task Merge() { try { var response = await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[Repository].PullRequests[Id].Merge(string.Empty)); if (!response.Data.Merged) throw new Exception(response.Data.Message); } catch (Exception e) { this.AlertService.Alert("Unable to Merge!", e.Message).ToBackground(); } await Load(); } public ICommand MergeCommand { get { return new MvxCommand(() => Merge(), CanMerge); } } private bool CanMerge() { if (PullRequest == null) return false; var isClosed = string.Equals(PullRequest.State, "closed", StringComparison.OrdinalIgnoreCase); var isMerged = PullRequest.Merged.GetValueOrDefault(); return CanPush && !isClosed && !isMerged; } public class NavObject { public string Username { get; set; } public string Repository { get; set; } public long Id { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/PullRequests/PullRequestsViewModel.cs ================================================ using System; using System.Threading.Tasks; using System.Windows.Input; using MvvmCross.Core.ViewModels; using GitHubSharp.Models; using CodeHub.Core.Messages; using System.Linq; using CodeHub.Core.Services; namespace CodeHub.Core.ViewModels.PullRequests { public class PullRequestsViewModel : LoadableViewModel { private readonly IMessageService _messageService; private IDisposable _pullRequestEditSubscription; private readonly CollectionViewModel _pullrequests = new CollectionViewModel(); public CollectionViewModel PullRequests { get { return _pullrequests; } } public string Username { get; private set; } public string Repository { get; private set; } private int _selectedFilter; public int SelectedFilter { get { return _selectedFilter; } set { _selectedFilter = value; RaisePropertyChanged(() => SelectedFilter); } } public ICommand GoToPullRequestCommand { get { return new MvxCommand(x => ShowViewModel(new PullRequestViewModel.NavObject { Username = Username, Repository = Repository, Id = x.Number })); } } public PullRequestsViewModel(IMessageService messageService) { _messageService = messageService; this.Bind(x => x.SelectedFilter).Subscribe(_ => LoadCommand.Execute(null)); } public void Init(NavObject navObject) { Username = navObject.Username; Repository = navObject.Repository; PullRequests.FilteringFunction = x => { var state = SelectedFilter == 0 ? "open" : "closed"; return x.Where(y => y.State == state); }; _pullRequestEditSubscription = _messageService.Listen(x => { if (x.PullRequest == null) return; var index = PullRequests.Items.IndexOf(x.PullRequest); if (index < 0) return; PullRequests.Items[index] = x.PullRequest; PullRequests.Refresh(); }); } protected override Task Load() { var state = SelectedFilter == 0 ? "open" : "closed"; var request = this.GetApplication().Client.Users[Username].Repositories[Repository].PullRequests.GetAll(state: state); return PullRequests.SimpleCollectionLoad(request); } public class NavObject { public string Username { get; set; } public string Repository { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Repositories/RepositoriesViewModel.cs ================================================ using ReactiveUI; using System.Reactive; using CodeHub.Core.Services; using Octokit; using Splat; using System; using System.Threading.Tasks; using System.Collections.Generic; using System.Reactive.Linq; using System.Linq; namespace CodeHub.Core.ViewModels.Repositories { public class RepositoriesViewModel : ReactiveObject { private readonly IApplicationService _applicationService; private readonly IAlertDialogService _dialogService; private readonly ReactiveList _internalItems = new ReactiveList(resetChangeThreshold: double.MaxValue); public ReactiveCommand LoadCommand { get; } public ReactiveCommand LoadMoreCommand { get; } public IReadOnlyReactiveList Items { get; private set; } public ReactiveCommand RepositoryItemSelected { get; } private ObservableAsPropertyHelper _hasMore; public bool HasMore => _hasMore.Value; private Uri _nextPage; private Uri NextPage { get { return _nextPage; } set { this.RaiseAndSetIfChanged(ref _nextPage, value); } } private string _searchText; public string SearchText { get { return _searchText; } set { this.RaiseAndSetIfChanged(ref _searchText, value); } } private readonly ObservableAsPropertyHelper _isEmpty; public bool IsEmpty => _isEmpty.Value; public static RepositoriesViewModel CreateWatchedViewModel() => new RepositoriesViewModel(ApiUrls.Watched()); public static RepositoriesViewModel CreateStarredViewModel() => new RepositoriesViewModel(ApiUrls.Starred()); public static RepositoriesViewModel CreateForkedViewModel(string username, string repository) => new RepositoriesViewModel(ApiUrls.RepositoryForks(username, repository)); public static RepositoriesViewModel CreateOrganizationViewModel(string org) => new RepositoriesViewModel(ApiUrls.OrganizationRepositories(org)); public static RepositoriesViewModel CreateTeamViewModel(int id) => new RepositoriesViewModel(ApiUrls.TeamRepositories(id)); public static RepositoriesViewModel CreateMineViewModel() => new RepositoriesViewModel(ApiUrls.Repositories(), false, "owner,collaborator"); public static RepositoriesViewModel CreateUsersViewModel(string username) { var applicationService = Locator.Current.GetService(); var isCurrent = string.Equals(applicationService.Account.Username, username, StringComparison.OrdinalIgnoreCase); return isCurrent ? CreateMineViewModel() : new RepositoriesViewModel(ApiUrls.Repositories(username)); } public RepositoriesViewModel( Uri repositoriesUri, bool showOwner = true, string affiliation = null, IApplicationService applicationService = null, IAlertDialogService dialogService = null) { _applicationService = applicationService ?? Locator.Current.GetService(); _dialogService = dialogService ?? Locator.Current.GetService(); NextPage = repositoriesUri; var showDescription = _applicationService.Account.ShowRepositoryDescriptionInList; RepositoryItemSelected = ReactiveCommand.Create(x => x); var repositoryItems = _internalItems.CreateDerivedCollection( x => new RepositoryItemViewModel(x, showOwner, showDescription, GoToRepository)); var searchUpdated = this.WhenAnyValue(x => x.SearchText) .Throttle(TimeSpan.FromMilliseconds(400), RxApp.MainThreadScheduler); Items = repositoryItems .CreateDerivedCollection( x => x, x => x.Name.ContainsKeyword(SearchText), signalReset: searchUpdated); LoadCommand = ReactiveCommand.CreateFromTask(async t => { _internalItems.Clear(); var parameters = new Dictionary { ["per_page"] = 75.ToString() }; if (affiliation != null) { parameters["affiliation"] = affiliation; } var items = await RetrieveRepositories(repositoriesUri, parameters); _internalItems.AddRange(items); return items.Count > 0; }); var canLoadMore = this.WhenAnyValue(x => x.NextPage).Select(x => x != null); LoadMoreCommand = ReactiveCommand.CreateFromTask(async _ => { var items = await RetrieveRepositories(NextPage); _internalItems.AddRange(items); return items.Count > 0; }, canLoadMore); LoadCommand.Select(_ => _internalItems.Count == 0) .ToProperty(this, x => x.IsEmpty, out _isEmpty, true); LoadCommand.ThrownExceptions.Subscribe(LoadingError); LoadMoreCommand.ThrownExceptions.Subscribe(LoadingError); _hasMore = this.WhenAnyValue(x => x.NextPage) .Select(x => x != null) .ToProperty(this, x => x.HasMore); } private void LoadingError(Exception err) { var message = err.Message; var baseException = err.GetInnerException(); if (baseException is System.Net.Sockets.SocketException) { message = "Unable to communicate with GitHub. " + baseException.Message; } _dialogService.Alert("Error Loading", message).ToBackground(); } private void GoToRepository(RepositoryItemViewModel item) { RepositoryItemSelected.ExecuteNow(item); } private async Task> RetrieveRepositories( Uri repositoriesUri, IDictionary parameters = null) { var connection = _applicationService.GitHubClient.Connection; var ret = await connection.Get>(repositoriesUri, parameters, "application/json"); NextPage = ret.HttpResponse.ApiInfo.Links.ContainsKey("next") ? ret.HttpResponse.ApiInfo.Links["next"] : null; return ret.Body; } } } ================================================ FILE: CodeHub.Core/ViewModels/Repositories/RepositoryItemViewModel.cs ================================================ using ReactiveUI; using System; using CodeHub.Core.Utilities; using System.Diagnostics; using System.Reactive; namespace CodeHub.Core.ViewModels.Repositories { [DebuggerDisplay("{Owner}/{Name}")] public class RepositoryItemViewModel : ReactiveObject, ICanGoToViewModel { public string Name => Repository.Name; public string Owner => Repository.Owner?.Login ?? string.Empty; public GitHubAvatar Avatar => new GitHubAvatar(Repository.Owner?.AvatarUrl); public string Description { get; } public string Stars => Repository.StargazersCount.ToString(); public string Forks => Repository.ForksCount.ToString(); public bool ShowOwner { get; } public Octokit.Repository Repository { get; } public ReactiveCommand GoToCommand { get; } public RepositoryItemViewModel(Octokit.Repository repository, bool showOwner, bool showDescription, Action gotoCommand) { if (showDescription) { if (!string.IsNullOrEmpty(repository.Description) && repository.Description.IndexOf(':') >= 0) Description = Emojis.FindAndReplace(repository.Description); else Description = repository.Description; } else { Description = null; } Repository = repository; ShowOwner = showOwner; GoToCommand = ReactiveCommand.Create(() => gotoCommand?.Invoke(this)); } } } ================================================ FILE: CodeHub.Core/ViewModels/Repositories/RepositoryViewModel.cs ================================================ using System.Collections.Generic; using System.Threading.Tasks; using System.Windows.Input; using MvvmCross.Core.ViewModels; using GitHubSharp.Models; using CodeHub.Core.ViewModels.User; using CodeHub.Core.ViewModels.Events; using CodeHub.Core.ViewModels.Changesets; using System.Linq; using System; using CodeHub.Core.Services; using Splat; using CodeHub.Core.ViewModels.Source; namespace CodeHub.Core.ViewModels.Repositories { public class RepositoryViewModel : LoadableViewModel { private readonly IApplicationService _applicationService; public string Username { get; private set; } public string RepositoryName { get; private set; } public string ImageUrl { get; set; } private bool? _starred; public bool? IsStarred { get { return _starred; } private set { this.RaiseAndSetIfChanged(ref _starred, value); } } private bool? _watched; public bool? IsWatched { get { return _watched; } private set { this.RaiseAndSetIfChanged(ref _watched, value); } } private Octokit.Repository _repository; public Octokit.Repository Repository { get { return _repository; } set { this.RaiseAndSetIfChanged(ref _repository, value); } } private Octokit.Readme _readme; public Octokit.Readme Readme { get { return _readme; } private set { this.RaiseAndSetIfChanged(ref _readme, value); } } private IReadOnlyList _branches; public IReadOnlyList Branches { get { return _branches; } private set { this.RaiseAndSetIfChanged(ref _branches, value); } } public RepositoryViewModel(IApplicationService applicationService = null) { _applicationService = applicationService ?? Locator.Current.GetService(); } public void Init(NavObject navObject) { Username = navObject.Username; Title = RepositoryName = navObject.Repository; } public ICommand GoToEventsCommand { get { return new MvxCommand(() => ShowViewModel(new RepositoryEventsViewModel.NavObject { Username = Username, Repository = RepositoryName })); } } public ICommand GoToIssuesCommand { get { return new MvxCommand(() => ShowViewModel(new Issues.IssuesViewModel.NavObject { Username = Username, Repository = RepositoryName })); } } public ICommand GoToPullRequestsCommand { get { return new MvxCommand(() => ShowViewModel(new PullRequests.PullRequestsViewModel.NavObject { Username = Username, Repository = RepositoryName })); } } public ICommand GoToHtmlUrlCommand { get { return new MvxCommand(() => ShowViewModel(new WebBrowserViewModel.NavObject { Url = Repository.HtmlUrl }), () => Repository != null); } } public ICommand PinCommand { get { return new MvxCommand(PinRepository, () => Repository != null); } } private void PinRepository() { var repoOwner = Repository.Owner.Login; var repoName = Repository.Name; var account = this.GetApplication().Account; var pinnedRepository = account.PinnedRepositories .FirstOrDefault(x => string.Equals(repoName, x.Name, StringComparison.OrdinalIgnoreCase) && string.Equals(repoOwner, x.Owner, StringComparison.OrdinalIgnoreCase)); //Is it pinned already or not? if (pinnedRepository == null) { account.PinnedRepositories.Add(new Data.PinnedRepository { Name = repoName, Owner = repoOwner, ImageUri = ImageUrl, Slug = repoName }); _applicationService.UpdateActiveAccount().ToBackground(); } else { account.PinnedRepositories.Remove(pinnedRepository); _applicationService.UpdateActiveAccount().ToBackground(); } } protected override async Task Load() { _applicationService.GitHubClient.Repository.Content .GetReadme(Username, RepositoryName).ToBackground(x => Readme = x); _applicationService.GitHubClient.Repository.Branch .GetAll(Username, RepositoryName).ToBackground(x => Branches = x); _applicationService.GitHubClient.Activity.Starring .CheckStarred(Username, RepositoryName).ToBackground(x => IsStarred = x); _applicationService.GitHubClient.Activity.Watching .CheckWatched(Username, RepositoryName).ToBackground(x => IsWatched = x); var retrieveRepository = _applicationService.GitHubClient.Repository.Get(Username, RepositoryName); if (Repository == null) Repository = await retrieveRepository; else retrieveRepository.ToBackground(repo => Repository = repo); } public ICommand ToggleWatchCommand { get { return new MvxCommand(() => ToggleWatch(), () => IsWatched != null); } } private async Task ToggleWatch() { if (IsWatched == null) return; try { if (IsWatched.Value) await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[RepositoryName].StopWatching()); else await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[RepositoryName].Watch()); IsWatched = !IsWatched; } catch { DisplayAlert("Unable to toggle repository as " + (IsWatched.Value ? "unwatched" : "watched") + "! Please try again."); } } public ICommand ToggleStarCommand { get { return new MvxCommand(() => ToggleStar(), () => IsStarred != null); } } public bool IsPinned { get { var repos = this.GetApplication().Account.PinnedRepositories; return repos.Any(x => string.Equals(x.Owner, Username, StringComparison.OrdinalIgnoreCase) && string.Equals(x.Slug, RepositoryName, StringComparison.OrdinalIgnoreCase)); } } private async Task ToggleStar() { if (IsStarred == null) return; try { if (IsStarred.Value) await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[RepositoryName].Unstar()); else await this.GetApplication().Client.ExecuteAsync(this.GetApplication().Client.Users[Username].Repositories[RepositoryName].Star()); IsStarred = !IsStarred; } catch { DisplayAlert("Unable to " + (IsStarred.Value ? "unstar" : "star") + " this repository! Please try again."); } } public class NavObject { public string Username { get; set; } public string Repository { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Repositories/TrendingRepositoriesViewModel.cs ================================================ using System; using System.Threading.Tasks; using System.Collections.Generic; using CodeHub.Core.Data; using ReactiveUI; using System.Reactive; using System.Reactive.Linq; using System.Linq; using CodeHub.Core.Services; using Splat; namespace CodeHub.Core.ViewModels.Repositories { public class RepositoriesTrendingViewModel : ReactiveObject { private readonly Language _defaultLanguage = new Language("All Languages", null); private readonly ITrendingRepository _trendingRepository; private readonly IAlertDialogService _dialogService; private static readonly Tuple[] Times = { Tuple.Create("Daily", "daily"), Tuple.Create("Weekly", "weekly"), Tuple.Create("Monthly", "monthly"), }; private IList>> _items; public IList>> Items { get { return _items ?? new List>>(); } private set { this.RaiseAndSetIfChanged(ref _items, value); } } private Language _selectedLanguage; public Language SelectedLanguage { get { return _selectedLanguage; } set { this.RaiseAndSetIfChanged(ref _selectedLanguage, value); } } public ReactiveCommand LoadCommand { get; } public ReactiveCommand RepositoryItemSelected { get; } public RepositoriesTrendingViewModel( ITrendingRepository trendingRepository = null, IAlertDialogService dialogService = null) { _trendingRepository = trendingRepository ?? new TrendingRepository(); _dialogService = dialogService ?? Locator.Current.GetService(); RepositoryItemSelected = ReactiveCommand.Create(x => x); LoadCommand = ReactiveCommand.CreateFromTask(Load); LoadCommand.ThrownExceptions.Subscribe(LoadingError); SelectedLanguage = _defaultLanguage; this.WhenAnyValue(x => x.SelectedLanguage) .Skip(1) .Select(_ => Unit.Default) .Do(_ => Items = null) .InvokeReactiveCommand(LoadCommand); } private void LoadingError(Exception err) { var message = err.Message; var baseException = err.GetInnerException(); if (baseException is System.Net.Sockets.SocketException) { message = "Unable to communicate with GitHub. " + baseException.Message; } _dialogService.Alert("Error Loading", message).ToBackground(); } private async Task Load() { var items = new List>>(); foreach (var t in Times) { var repos = await _trendingRepository.GetTrendingRepositories(t.Item2, SelectedLanguage.Slug); var viewModels = repos .Select(x => new RepositoryItemViewModel(x, true, true, vm => RepositoryItemSelected.ExecuteNow(vm))) .ToList(); items.Add(Tuple.Create(t.Item1, viewModels as IList)); } Items = items; } } } ================================================ FILE: CodeHub.Core/ViewModels/Search/ExploreViewModel.cs ================================================ using ReactiveUI; namespace CodeHub.Core.ViewModels.Search { public class ExploreViewModel : ReactiveObject { public RepositoryExploreViewModel Repositories { get; } = new RepositoryExploreViewModel(); public UserExploreViewModel Users { get; } = new UserExploreViewModel(); private SearchType _searchFilter = SearchType.Repositories; public SearchType SearchFilter { get { return _searchFilter; } set { this.RaiseAndSetIfChanged(ref _searchFilter, value); } } public enum SearchType { Repositories = 0, Users } } } ================================================ FILE: CodeHub.Core/ViewModels/Search/RepositoryExploreViewModel.cs ================================================ using System; using System.Reactive; using CodeHub.Core.Services; using ReactiveUI; using Octokit; using System.Reactive.Linq; using Splat; using CodeHub.Core.ViewModels.Repositories; namespace CodeHub.Core.ViewModels.Search { public class RepositoryExploreViewModel : ReactiveObject { private readonly IApplicationService _applicationService; public ReactiveCommand SearchCommand { get; } public IReadOnlyReactiveList Items { get; private set; } public ReactiveCommand RepositoryItemSelected { get; } private string _searchText; public string SearchText { get { return _searchText; } set { this.RaiseAndSetIfChanged(ref _searchText, value); } } public RepositoryExploreViewModel( IApplicationService applicationService = null, IAlertDialogService dialogService = null) { _applicationService = applicationService ?? Locator.Current.GetService(); dialogService = dialogService ?? Locator.Current.GetService(); RepositoryItemSelected = ReactiveCommand.Create(x => x); var showDescription = _applicationService.Account.ShowRepositoryDescriptionInList; var internalItems = new ReactiveList(resetChangeThreshold: double.MaxValue); Items = internalItems.CreateDerivedCollection(x => new RepositoryItemViewModel(x, true, showDescription, y => RepositoryItemSelected.ExecuteNow(y))); var canSearch = this.WhenAnyValue(x => x.SearchText).Select(x => !string.IsNullOrEmpty(x)); SearchCommand = ReactiveCommand.CreateFromTask(async t => { try { internalItems.Clear(); var request = new SearchRepositoriesRequest(SearchText); var response = await _applicationService.GitHubClient.Search.SearchRepo(request); internalItems.Reset(response.Items); } catch (Exception e) { var msg = string.Format("Unable to search for {0}. Please try again.", SearchText); throw new Exception(msg, e); } }, canSearch); SearchCommand .ThrownExceptions .Subscribe(err => dialogService.Alert("Error Searching", err.Message).ToBackground()); } } } ================================================ FILE: CodeHub.Core/ViewModels/Search/UserExploreViewModel.cs ================================================ using System; using CodeHub.Core.Services; using ReactiveUI; using Octokit; using CodeHub.Core.ViewModels.Users; using System.Reactive; using System.Reactive.Linq; using Humanizer; using Splat; namespace CodeHub.Core.ViewModels.Search { public class UserExploreViewModel : ReactiveObject { public ReactiveCommand SearchCommand { get; } public IReadOnlyReactiveList Items { get; private set; } public ReactiveCommand RepositoryItemSelected { get; } private string _searchText; public string SearchText { get { return _searchText; } set { this.RaiseAndSetIfChanged(ref _searchText, value); } } public UserExploreViewModel(IApplicationService applicationService = null) { applicationService = applicationService ?? Locator.Current.GetService(); var items = new ReactiveList(); var itemSelected = ReactiveCommand.Create(x => x); Items = items.CreateDerivedCollection( x => new UserItemViewModel(x, y => itemSelected.ExecuteNow(y))); RepositoryItemSelected = itemSelected; var canSearch = this.WhenAnyValue(x => x.SearchText).Select(x => !string.IsNullOrEmpty(x)); SearchCommand = ReactiveCommand.CreateFromTask(async t => { try { items.Clear(); var request = new SearchUsersRequest(SearchText); var response = await applicationService.GitHubClient.Search.SearchUsers(request); items.Reset(response.Items); } catch (Exception e) { var msg = string.Format("Unable to search for {0}. Please try again.", SearchText.Humanize()); throw new Exception(msg, e); } }, canSearch); } } } ================================================ FILE: CodeHub.Core/ViewModels/Source/EditSourceViewModel.cs ================================================ using System; using System.Threading.Tasks; using CodeHub.Core.Messages; using CodeHub.Core.Services; using MvvmCross.Core.ViewModels; namespace CodeHub.Core.ViewModels.Source { public class EditSourceViewModel : LoadableViewModel { private readonly IMessageService _messageService; private string _text; public string Text { get { return _text; } private set { this.RaiseAndSetIfChanged(ref _text, value); } } public string Username { get; private set; } public string Repository { get; private set; } public string Path { get; private set; } public string BlobSha { get; private set; } public string Branch { get; private set; } public EditSourceViewModel(IMessageService messageService = null) { _messageService = messageService ?? GetService(); } public void Init(NavObject navObject) { Username = navObject.Username; Repository = navObject.Repository; Path = navObject.Path ?? string.Empty; Branch = navObject.Branch ?? "master"; if (!Path.StartsWith("/", StringComparison.Ordinal)) Path = "/" + Path; } protected override async Task Load() { var request = this.GetApplication().Client.Users[Username].Repositories[Repository].GetContentFile(Path, Branch); var data = await this.GetApplication().Client.ExecuteAsync(request); BlobSha = data.Data.Sha; Text = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(data.Data.Content)); } public async Task Commit(string data, string message) { var request = this.GetApplication().Client.Users[Username].Repositories[Repository].UpdateContentFile(Path, message, data, BlobSha, Branch); var response = await this.GetApplication().Client.ExecuteAsync(request); _messageService.Send(new SourceEditMessage { OldSha = BlobSha, Data = data, Update = response.Data }); } public class NavObject { public string Username { get; set; } public string Repository { get; set; } public string Path { get; set; } public string Branch { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Users/UserItemViewModel.cs ================================================ using ReactiveUI; using System; using CodeHub.Core.Utilities; using System.Reactive; namespace CodeHub.Core.ViewModels.Users { public class UserItemViewModel : ReactiveObject, ICanGoToViewModel { public string Login => User.Login; public string Name => User.Name; public GitHubAvatar Avatar => new GitHubAvatar(User.AvatarUrl); public ReactiveCommand GoToCommand { get; } public Octokit.User User { get; } internal UserItemViewModel(Octokit.User user, Action gotoAction) { User = user; GoToCommand = ReactiveCommand.Create(() => gotoAction?.Invoke(this)); } } } ================================================ FILE: CodeHub.Core/ViewModels/Users/UserViewModel.cs ================================================ using System.Threading.Tasks; using System.Windows.Input; using CodeHub.Core.Services; using CodeHub.Core.ViewModels.Events; using CodeHub.Core.ViewModels.Organizations; using MvvmCross.Core.ViewModels; using Splat; namespace CodeHub.Core.ViewModels.User { public class UserViewModel : LoadableViewModel { private readonly IApplicationService _applicationService = Locator.Current.GetService(); public string Username { get; private set; } private Octokit.User _user; public Octokit.User User { get { return _user; } set { this.RaiseAndSetIfChanged(ref _user, value); } } private bool _isFollowing; public bool IsFollowing { get { return _isFollowing; } private set { this.RaiseAndSetIfChanged(ref _isFollowing, value); } } public bool IsLoggedInUser { get { return string.Equals(Username, this.GetApplication().Account.Username); } } public ICommand GoToEventsCommand { get { return new MvxCommand(() => ShowViewModel(new UserEventsViewModel.NavObject { Username = Username })); } } public ICommand GoToOrganizationsCommand { get { return new MvxCommand(() => ShowViewModel(new OrganizationsViewModel.NavObject { Username = Username })); } } public ICommand ToggleFollowingCommand { get { return new MvxCommand(() => ToggleFollowing().ToBackground()); } } private async Task ToggleFollowing() { try { if (IsFollowing) await _applicationService.GitHubClient.User.Followers.Unfollow(Username); else await _applicationService.GitHubClient.User.Followers.Follow(Username); IsFollowing = !IsFollowing; } catch { DisplayAlert("Unable to follow user! Please try again."); } } public void Init(NavObject navObject) { Title = Username = navObject.Username; } protected override async Task Load() { _applicationService.GitHubClient.User.Followers .IsFollowingForCurrent(Username).ToBackground(x => IsFollowing = x); if (User != null) _applicationService.GitHubClient.User.Get(Username).ToBackground(x => User = x); else User = await _applicationService.GitHubClient.User.Get(Username); } public class NavObject { public string Username { get; set; } } } } ================================================ FILE: CodeHub.Core/ViewModels/Users/UsersViewModel.cs ================================================ using System; using System.Collections.Generic; using System.Reactive; using System.Reactive.Linq; using System.Threading.Tasks; using CodeHub.Core.Services; using Octokit; using ReactiveUI; using Splat; namespace CodeHub.Core.ViewModels.Users { public class UsersViewModel : ReactiveObject { private readonly IApplicationService _applicationService; private readonly IAlertDialogService _dialogService; private readonly ReactiveList _internalItems = new ReactiveList(resetChangeThreshold: double.MaxValue); public ReactiveCommand LoadCommand { get; } public ReactiveCommand LoadMoreCommand { get; } public IReadOnlyReactiveList Items { get; private set; } public ReactiveCommand ItemSelected { get; } private ObservableAsPropertyHelper _hasMore; public bool HasMore => _hasMore.Value; private Uri _nextPage; private Uri NextPage { get { return _nextPage; } set { this.RaiseAndSetIfChanged(ref _nextPage, value); } } private string _searchText; public string SearchText { get { return _searchText; } set { this.RaiseAndSetIfChanged(ref _searchText, value); } } private readonly ObservableAsPropertyHelper _isEmpty; public bool IsEmpty => _isEmpty.Value; public static UsersViewModel CreateWatchersViewModel(string owner, string name) => new UsersViewModel(ApiUrls.Watchers(owner, name)); public static UsersViewModel CreateFollowersViewModel(string user) => new UsersViewModel(ApiUrls.Followers(user)); public static UsersViewModel CreateFollowingViewModel(string user) => new UsersViewModel(ApiUrls.Following(user)); public static UsersViewModel CreateTeamMembersViewModel(int id) => new UsersViewModel(ApiUrls.TeamMembers(id)); public static UsersViewModel CreateOrgMembersViewModel(string org) => new UsersViewModel(ApiUrls.Members(org)); public static UsersViewModel CreateStargazersViewModel(string owner, string name) => new UsersViewModel(ApiUrls.Stargazers(owner, name)); public static UsersViewModel CreateCollaboratorsViewModel(string owner, string name) => new UsersViewModel(ApiUrls.RepoCollaborators(owner, name)); public UsersViewModel( Uri uri, IApplicationService applicationService = null, IAlertDialogService dialogService = null) { _applicationService = applicationService ?? Locator.Current.GetService(); _dialogService = dialogService ?? Locator.Current.GetService(); NextPage = uri; var showDescription = _applicationService.Account.ShowRepositoryDescriptionInList; ItemSelected = ReactiveCommand.Create(x => x); var userItems = _internalItems.CreateDerivedCollection( x => new UserItemViewModel(x, GoToUser)); var searchUpdated = this.WhenAnyValue(x => x.SearchText) .Throttle(TimeSpan.FromMilliseconds(400), RxApp.MainThreadScheduler); Items = userItems .CreateDerivedCollection( x => x, x => x.Login.ContainsKeyword(SearchText), signalReset: searchUpdated); LoadCommand = ReactiveCommand.CreateFromTask(async t => { _internalItems.Clear(); var parameters = new Dictionary(); parameters["per_page"] = 100.ToString(); var items = await RetrieveItems(uri, parameters); _internalItems.AddRange(items); return items.Count > 0; }); var canLoadMore = this.WhenAnyValue(x => x.NextPage).Select(x => x != null); LoadMoreCommand = ReactiveCommand.CreateFromTask(async _ => { var items = await RetrieveItems(NextPage); _internalItems.AddRange(items); return items.Count > 0; }, canLoadMore); LoadCommand.Select(_ => _internalItems.Count == 0) .ToProperty(this, x => x.IsEmpty, out _isEmpty, true); LoadCommand.ThrownExceptions.Subscribe(LoadingError); LoadMoreCommand.ThrownExceptions.Subscribe(LoadingError); _hasMore = this.WhenAnyValue(x => x.NextPage) .Select(x => x != null) .ToProperty(this, x => x.HasMore); } private void LoadingError(Exception err) { _dialogService.Alert("Error Loading", err.Message).ToBackground(); } private void GoToUser(UserItemViewModel item) { ItemSelected.ExecuteNow(item); } private async Task> RetrieveItems( Uri repositoriesUri, IDictionary parameters = null) { try { var connection = _applicationService.GitHubClient.Connection; var ret = await connection.Get>(repositoriesUri, parameters, "application/json"); NextPage = ret.HttpResponse.ApiInfo.Links.ContainsKey("next") ? ret.HttpResponse.ApiInfo.Links["next"] : null; return ret.Body; } catch { NextPage = null; throw; } } } } ================================================ FILE: CodeHub.Core/ViewModels/ViewModelExtensions.cs ================================================ using System; using System.Threading.Tasks; using MvvmCross.Core.ViewModels; using GitHubSharp; using System.Collections.Generic; using CodeHub.Core.Services; using System.ComponentModel; using System.Collections.Specialized; using MvvmCross.Platform; using System.Reactive.Linq; using System.Reactive; using System.Reactive.Disposables; namespace CodeHub.Core.ViewModels { public static class ViewModelExtensions { public static async Task RequestModel(this MvxViewModel viewModel, GitHubRequest request, Action> update) where TRequest : new() { var application = Mvx.Resolve(); var result = await application.Client.ExecuteAsync(request); update(result); } public static void CreateMore(this MvxViewModel viewModel, GitHubResponse response, Action assignMore, Action newDataAction) where T : new() { if (response.More == null) { assignMore(null); return; } Action task = () => { var moreResponse = Mvx.Resolve().Client.ExecuteAsync(response.More).Result; viewModel.CreateMore(moreResponse, assignMore, newDataAction); newDataAction(moreResponse.Data); }; assignMore(task); } public static Task SimpleCollectionLoad(this CollectionViewModel viewModel, GitHubRequest> request) where T : new() { var weakVm = new WeakReference>(viewModel); return viewModel.RequestModel(request, response => { weakVm.Get()?.CreateMore(response, m => { var weak = weakVm.Get(); if (weak != null) weak.MoreItems = m; }, viewModel.Items.AddRange); weakVm.Get()?.Items.Reset(response.Data); }); } } } public static class BindExtensions { public static IObservable Bind(this T viewModel, System.Linq.Expressions.Expression> outExpr, bool activate = false) where T : INotifyPropertyChanged { var expr = (System.Linq.Expressions.MemberExpression) outExpr.Body; var prop = (System.Reflection.PropertyInfo) expr.Member; var name = prop.Name; var comp = outExpr.Compile(); var ret = Observable.FromEventPattern(t => viewModel.PropertyChanged += t, t => viewModel.PropertyChanged -= t) .Where(x => string.Equals(x.EventArgs.PropertyName, name)) .Select(x => comp(viewModel)); if (!activate) return ret; var o = Observable.Create(obs => { try { obs.OnNext(comp(viewModel)); } catch (Exception e) { obs.OnError(e); } obs.OnCompleted(); return Disposable.Empty; }); return o.Concat(ret); } public static IObservable BindCollection(this T viewModel, System.Linq.Expressions.Expression> outExpr, bool activate = false) where T : INotifyPropertyChanged { var exp = outExpr.Compile(); var m = exp(viewModel); var ret = Observable.FromEventPattern(t => m.CollectionChanged += t, t => m.CollectionChanged -= t) .Select(_ => Unit.Default); return activate ? ret.StartWith(Unit.Default) : ret; } } ================================================ FILE: CodeHub.Core/ViewModels/WebBrowserViewModel.cs ================================================ namespace CodeHub.Core.ViewModels { public class WebBrowserViewModel : BaseViewModel { public string Url { get; private set; } public void Init(NavObject navObject) { Url = navObject.Url; } public class NavObject { public string Url { get; set; } } } } ================================================ FILE: CodeHub.Core/packages.config ================================================  ================================================ FILE: CodeHub.iOS/AkavacheSqliteLinkerOverride.cs ================================================ using System; using Akavache.Sqlite3; // Note: This class file is *required* for iOS to work correctly, and is // also a good idea for Android if you enable "Link All Assemblies". namespace CodeHub.iOS { [Preserve] public static class LinkerPreserve { static LinkerPreserve() { throw new Exception(typeof(SQLitePersistentBlobCache).FullName); } } public class PreserveAttribute : Attribute { } } ================================================ FILE: CodeHub.iOS/AppDelegate.cs ================================================ using System.Collections.Generic; using System; using MvvmCross.Core.ViewModels; using Foundation; using UIKit; using CodeHub.Core.Utils; using CodeHub.Core.Services; using System.Threading.Tasks; using System.Linq; using ObjCRuntime; using MvvmCross.iOS.Platform; using MvvmCross.Platform; using MvvmCross.Core.Views; using System.Net.Http; using CodeHub.iOS.Services; using ReactiveUI; using CodeHub.Core.Messages; using CodeHub.iOS.XCallback; using System.Reactive.Linq; using Splat; using Microsoft.AppCenter; using Microsoft.AppCenter.Analytics; using Microsoft.AppCenter.Crashes; namespace CodeHub.iOS { [Register("AppDelegate")] public class AppDelegate : MvxApplicationDelegate { public string DeviceToken; public override UIWindow Window { get; set; } public IosViewPresenter Presenter { get; private set; } public static AppDelegate Instance => UIApplication.SharedApplication.Delegate as AppDelegate; /// /// This is the main entry point of the application. /// /// The args. public static void Main(string[] args) { UIApplication.Main(args, null, "AppDelegate"); } /// /// Finished the launching. /// /// The app. /// The options. /// True or false. public override bool FinishedLaunching(UIApplication app, NSDictionary options) { AppCenter.Start("eef367be-437c-4c67-abe0-79779b3b8392", typeof(Analytics), typeof(Crashes)); Window = new UIWindow(UIScreen.MainScreen.Bounds); Presenter = new IosViewPresenter(this.Window); var setup = new Setup(this, Presenter); setup.Initialize(); var culture = new System.Globalization.CultureInfo("en"); System.Threading.Thread.CurrentThread.CurrentCulture = culture; System.Threading.Thread.CurrentThread.CurrentUICulture = culture; System.Globalization.CultureInfo.DefaultThreadCurrentCulture = culture; System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = culture; // Setup theme UIApplication.SharedApplication.SetStatusBarStyle(UIStatusBarStyle.LightContent, true); Theme.Setup(); Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); Locator.CurrentMutable.RegisterConstant(Mvx.Resolve()); Locator.CurrentMutable.RegisterLazySingleton( () => new ImgurService(), typeof(IImgurService)); var features = Mvx.Resolve(); var purchaseService = Mvx.Resolve(); purchaseService.ThrownExceptions.Subscribe(ex => { var error = new Core.UserError("Error Purchasing", ex.Message); Core.Interactions.Errors.Handle(error).Subscribe(); }); Core.Interactions.Errors.RegisterHandler(interaction => { var error = interaction.Input; AlertDialogService.ShowAlert(error.Title, error.Message); interaction.SetOutput(System.Reactive.Unit.Default); }); #if DEBUG features.ActivateProDirect(); #endif //options = new NSDictionary (UIApplication.LaunchOptionsRemoteNotificationKey, //new NSDictionary ("r", "octokit/octokit.net", "i", "739", "u", "thedillonb")); if (options != null) { if (options.ContainsKey(UIApplication.LaunchOptionsRemoteNotificationKey)) { var remoteNotification = options[UIApplication.LaunchOptionsRemoteNotificationKey] as NSDictionary; if(remoteNotification != null) { HandleNotification(remoteNotification, true); } } } // Set the client constructor GitHubSharp.Client.ClientConstructor = () => new HttpClient(new CustomHttpMessageHandler()); if (!Core.Settings.HasSeenWelcome) { Core.Settings.HasSeenWelcome = true; var welcomeViewController = new ViewControllers.Walkthrough.WelcomePageViewController(); welcomeViewController.WantsToDimiss += GoToStartupView; TransitionToViewController(welcomeViewController); } else { GoToStartupView(); } Window.MakeKeyAndVisible(); // Notifications don't work on teh simulator so don't bother if (Runtime.Arch != Arch.SIMULATOR && features.IsProEnabled) RegisterUserForNotifications(); return true; } public void RegisterUserForNotifications() { var notificationTypes = UIUserNotificationSettings.GetSettingsForTypes (UIUserNotificationType.Alert | UIUserNotificationType.Sound, null); UIApplication.SharedApplication.RegisterUserNotificationSettings(notificationTypes); } private void GoToStartupView() { TransitionToViewController(new ViewControllers.Application.StartupViewController()); MessageBus .Current.Listen() .ObserveOn(RxApp.MainThreadScheduler) .Select(_ => new ViewControllers.Application.StartupViewController()) .Subscribe(TransitionToViewController); } public void TransitionToViewController(UIViewController viewController) { UIView.Transition(Window, 0.35, UIViewAnimationOptions.TransitionCrossDissolve, () => Window.RootViewController = viewController, null); } class CustomHttpMessageHandler : DelegatingHandler { public CustomHttpMessageHandler() : base(new HttpClientHandler()) { } protected override Task SendAsync (HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { if (!string.Equals(request.Method.ToString(), "get", StringComparison.OrdinalIgnoreCase)) NSUrlCache.SharedCache.RemoveAllCachedResponses(); return base.SendAsync(request, cancellationToken); } } public override void ReceivedRemoteNotification(UIApplication application, NSDictionary userInfo) { if (application.ApplicationState == UIApplicationState.Active) return; HandleNotification(userInfo, false); } private void HandleNotification(NSDictionary data, bool fromBootup) { try { var viewDispatcher = Mvx.Resolve(); var appService = Mvx.Resolve(); var accountsService = Mvx.Resolve(); var repoId = RepositoryIdentifier.FromFullName(data["r"].ToString()); var parameters = new Dictionary() {{"Username", repoId?.Owner}, {"Repository", repoId?.Name}}; MvxViewModelRequest request; if (data.ContainsKey(new NSString("c"))) { request = MvxViewModelRequest.GetDefaultRequest(); parameters.Add("Node", data["c"].ToString()); parameters.Add("ShowRepository", "True"); } else if (data.ContainsKey(new NSString("i"))) { request = MvxViewModelRequest.GetDefaultRequest(); parameters.Add("Id", data["i"].ToString()); } else if (data.ContainsKey(new NSString("p"))) { request = MvxViewModelRequest.GetDefaultRequest(); parameters.Add("Id", data["p"].ToString()); } else { request = MvxViewModelRequest.GetDefaultRequest(); } request.ParameterValues = parameters; request.PresentationValues = new Dictionary { { Core.PresentationValues.SlideoutRootPresentation, string.Empty } }; var username = data["u"].ToString(); if (appService.Account == null || !appService.Account.Username.Equals(username)) { var accounts = accountsService.GetAccounts().Result.ToList(); var user = accounts.FirstOrDefault(x => x.Username.Equals(username)); if (user != null) { appService.DeactivateUser(); accountsService.SetActiveAccount(user).Wait(); } } appService.SetUserActivationAction(() => viewDispatcher.ShowViewModel(request)); if (appService.Account == null && !fromBootup) { MessageBus.Current.SendMessage(new LogoutMessage()); } } catch (Exception e) { Console.WriteLine("Handle Notifications issue: " + e); } } public override void DidRegisterUserNotificationSettings (UIApplication application, UIUserNotificationSettings notificationSettings) { application.RegisterForRemoteNotifications (); } public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken) { DeviceToken = deviceToken.Description.Trim('<', '>').Replace(" ", ""); var app = Mvx.Resolve(); var accounts = Mvx.Resolve(); if (app.Account != null && !app.Account.IsPushNotificationsEnabled.HasValue) { Mvx.Resolve().Register().ToBackground(); app.Account.IsPushNotificationsEnabled = true; accounts.Save(app.Account); } } public override void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error) { AlertDialogService.ShowAlert("Error Registering for Notifications", error.LocalizedDescription); } public override bool OpenUrl(UIApplication application, NSUrl url, string sourceApplication, NSObject annotation) { var uri = new Uri(url.AbsoluteString); if (uri.Host == "x-callback-url") { XCallbackProvider.Handle(new XCallbackQuery(url.AbsoluteString)); return true; } else { var path = url.AbsoluteString.Replace("codehub://", ""); var queryMarker = path.IndexOf("?", StringComparison.Ordinal); if (queryMarker > 0) path = path.Substring(0, queryMarker); if (!path.EndsWith("/", StringComparison.Ordinal)) path += "/"; // var first = path.Substring(0, path.IndexOf("/", StringComparison.Ordinal)); return UrlRouteProvider.Handle(path); } } } } ================================================ FILE: CodeHub.iOS/CodeHub.iOS.csproj ================================================  Debug iPhoneSimulator {B061316A-F386-4FE2-93B7-555584234FF8} {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Exe CodeHub.iOS Resources CodeHubiOS Xamarin.iOS v1.0 true full false bin\iPhoneSimulator\Debug DEBUG prompt 4 false SdkOnly true x86_64 Entitlements.plist iPhone Developer true NSUrlSessionHandler Default false true true full false bin\iPhone\Debug DEBUG prompt 4 false iPhone Developer true ARM64 true true Entitlements.plist none True bin\iPhone\Ad-Hoc prompt 4 False iPhone Distribution True ARMv7, ARM64 Automatic:AdHoc Entitlements.plist --linkskip=CodeHubiOS --linkskip=CodeHub.Core --linkskip=GitHubSharp SdkOnly NSUrlSessionHandler Default none True bin\iPhone\AppStore prompt 4 False iPhone Distribution: Dillon Buchanan (T39PW4C23Z) ARMv7, ARM64 true Entitlements.plist --linkskip=CodeHubiOS --linkskip=CodeHub.Core --linkskip=GitHubSharp SdkOnly NSUrlSessionHandler IssueCellView.cs NewsCellView.cs RepositoryCellView.cs PrivateRepositoryViewController.cs FeedbackCellView.cs ..\packages\MonoTouch.SlideoutNavigation.1.0.1\lib\Xamarin.iOS10\MonoTouch.Slideout.dll ..\packages\Xamarin.SDWebImage.3.7.5\lib\Xamarin.iOS\SDWebImage.dll ..\packages\Rx-Interfaces.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Interfaces.dll ..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll ..\packages\Rx-Linq.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Linq.dll ..\packages\Rx-PlatformServices.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll ..\packages\Splat.1.6.2\lib\Xamarin.iOS10\Splat.dll ..\packages\GitHubClient.1.0.15\lib\portable45-net45+win8+wpa81\GitHubSharp.dll lib\Xamarin.TTTAttributedLabel.dll ..\packages\PCLStorage.1.0.2\lib\portable-Xamarin.iOS+Xamarin.Mac\PCLStorage.dll ..\packages\PCLStorage.1.0.2\lib\portable-Xamarin.iOS+Xamarin.Mac\PCLStorage.Abstractions.dll ..\packages\akavache.core.5.0.0\lib\Xamarin.iOS10\Akavache.dll ..\packages\akavache.sqlite3.5.0.0\lib\Portable-Net45+Win8+WP8+Wpa81\Akavache.Sqlite3.dll ..\packages\MvvmCross.Platform.4.4.0\lib\Xamarin.iOS10\MvvmCross.Platform.dll ..\packages\MvvmCross.Platform.4.4.0\lib\Xamarin.iOS10\MvvmCross.Platform.iOS.dll ..\packages\MvvmCross.Core.4.4.0\lib\Xamarin.iOS10\MvvmCross.Core.dll ..\packages\MvvmCross.Core.4.4.0\lib\Xamarin.iOS10\MvvmCross.iOS.dll ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Binding.dll ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Binding.iOS.dll ..\packages\MvvmCross.Binding.4.4.0\lib\Xamarin.iOS10\MvvmCross.Localization.dll ..\packages\Humanizer.Core.2.2.0\lib\netstandard1.0\Humanizer.dll ..\packages\reactiveui-core.7.4.0\lib\Xamarin.iOS10\ReactiveUI.dll ..\packages\Newtonsoft.Json.10.0.3\lib\netstandard1.3\Newtonsoft.Json.dll ..\packages\BTProgressHUD.1.2.0.6\lib\Xamarin.iOS10\BTProgressHUD.dll ..\packages\SQLitePCLRaw.core.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.core.dll ..\packages\SQLitePCLRaw.lib.e_sqlite3.ios_unified.static.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.lib.e_sqlite3.dll ..\packages\SQLitePCLRaw.provider.internal.ios_unified.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.provider.internal.dll ..\packages\SQLitePCLRaw.bundle_e_sqlite3.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.batteries_e_sqlite3.dll ..\packages\SQLitePCLRaw.bundle_e_sqlite3.1.1.9\lib\Xamarin.iOS10\SQLitePCLRaw.batteries_v2.dll ..\packages\Xam.Plugins.Settings.3.1.1\lib\Xamarin.iOS10\Plugin.Settings.Abstractions.dll ..\packages\Xam.Plugins.Settings.3.1.1\lib\Xamarin.iOS10\Plugin.Settings.dll ..\packages\Octokit.0.29.0\lib\netstandard1.1\Octokit.dll ..\packages\Microsoft.AppCenter.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.dll ..\packages\Microsoft.AppCenter.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.iOS.Bindings.dll ..\packages\Microsoft.AppCenter.Analytics.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.Analytics.dll ..\packages\Microsoft.AppCenter.Analytics.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.Analytics.iOS.Bindings.dll ..\packages\Microsoft.AppCenter.Crashes.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.Crashes.dll ..\packages\Microsoft.AppCenter.Crashes.1.5.0\lib\Xamarin.iOS10\Microsoft.AppCenter.Crashes.iOS.Bindings.dll ..\packages\Plugin.Permissions.2.2.1\lib\Xamarin.iOS10\Plugin.Permissions.Abstractions.dll ..\packages\Plugin.Permissions.2.2.1\lib\Xamarin.iOS10\Plugin.Permissions.dll ..\packages\Xam.Plugin.Media.3.1.3\lib\Xamarin.iOS10\Plugin.Media.Abstractions.dll ..\packages\Xam.Plugin.Media.3.1.3\lib\Xamarin.iOS10\Plugin.Media.dll Resources\octicons.ttf {B7970173-9022-466B-B57A-7AB1E1F3145F} CodeHub.Core {B01CF3C6-51DF-4CAE-A07C-E4BC907833D7} CodeHub ================================================ FILE: CodeHub.iOS/CodeHubIcon.psd ================================================ [File too large to display: 27.0 MB] ================================================ FILE: CodeHub.iOS/DialogElements/BooleanElement.cs ================================================ using System; using UIKit; using CodeHub.iOS.DialogElements; using System.Reactive.Subjects; using System.Reactive.Linq; namespace CodeHub.iOS.DialogElements { public class BooleanElement : Element { private readonly Subject _changedSubject = new Subject(); private bool _value; public IObservable Changed { get { return _changedSubject.AsObservable(); } } private string _caption; public string Caption { get { return _caption; } set { if (_caption == value) return; _caption = value; var cell = GetActiveCell(); if (cell != null && cell.TextLabel != null) cell.TextLabel.Text = value ?? string.Empty; } } public bool Value { get { return _value; } set { if (_value == value) return; _value = value; _changedSubject.OnNext(value); var cell = GetActiveCell() as BooleanCellView; if (cell != null) cell.Switch.On = value; } } public BooleanElement (string caption, bool value = false) { Caption = caption; _value = value; } public override UITableViewCell GetCell (UITableView tv) { var cell = tv.DequeueReusableCell("boolean_element") as BooleanCellView ?? new BooleanCellView(); cell.Switch.On = Value; cell.BackgroundColor = StringElement.BgColor; cell.TextLabel.Font = UIFont.PreferredBody; cell.TextLabel.TextColor = StringElement.DefaultTitleColor; cell.TextLabel.Text = Caption; var weakThis = new WeakReference(this); cell.Switch.ValueChanged += UpdateValueChanged(weakThis); return cell; } private static EventHandler UpdateValueChanged(WeakReference weakThis) { return new EventHandler((s, _) => { BooleanElement parent; if (weakThis.TryGetTarget(out parent)) parent.Value = ((UISwitch)s).On; }); } private class BooleanCellView : UITableViewCell { public UISwitch Switch { get; } public BooleanCellView() : base(UITableViewCellStyle.Default, "boolean_element") { Switch = new UISwitch(); Switch.BackgroundColor = UIColor.Clear; SelectionStyle = UITableViewCellSelectionStyle.None; } public override void WillMoveToSuperview(UIView newsuper) { base.WillMoveToSuperview(newsuper); AccessoryView = newsuper == null ? null : Switch; } } } } ================================================ FILE: CodeHub.iOS/DialogElements/ChangesetElement.cs ================================================ using CodeHub.iOS.Views; namespace CodeHub.iOS.DialogElements { public class ChangesetElement : StringElement { private readonly int? _added; private readonly int? _removed; public ChangesetElement(string title, string subtitle, int? added, int? removed) : base(title, subtitle, UIKit.UITableViewCellStyle.Subtitle) { Accessory = UIKit.UITableViewCellAccessory.DisclosureIndicator; LineBreakMode = UIKit.UILineBreakMode.TailTruncation; Lines = 1; _added = added; _removed = removed; } protected override string GetKey(int style) { return "changeset"; } protected override UIKit.UITableViewCell CreateTableViewCell(UIKit.UITableViewCellStyle style, string key) { return new ChangesetCell(key); } public override UIKit.UITableViewCell GetCell(UIKit.UITableView tv) { var cell = base.GetCell(tv); var addRemove = ((ChangesetCell)cell).AddRemoveView; addRemove.Added = _added; addRemove.Removed = _removed; addRemove.SetNeedsDisplay(); return cell; } /// Bastardized version. I'll redo this code later... private sealed class ChangesetCell : UIKit.UITableViewCell { public AddRemoveView AddRemoveView { get; private set; } public ChangesetCell(string key) : base(UIKit.UITableViewCellStyle.Subtitle, key) { AddRemoveView = new AddRemoveView(); ContentView.AddSubview(AddRemoveView); TextLabel.LineBreakMode = UIKit.UILineBreakMode.TailTruncation; } public override void LayoutSubviews() { base.LayoutSubviews(); var addRemoveX = ContentView.Frame.Width - 90f; var addRemoveY = (ContentView.Frame.Height / 2) - 9f; AddRemoveView.Frame = new CoreGraphics.CGRect(addRemoveX, addRemoveY, 80f, 18f); var textFrame = TextLabel.Frame; textFrame.Width = addRemoveX - textFrame.X - 5f; TextLabel.Frame = textFrame; } } } } ================================================ FILE: CodeHub.iOS/DialogElements/CommentElement.cs ================================================ using UIKit; using CodeHub.iOS.TableViewCells; using System; using CodeHub.Core.Utilities; namespace CodeHub.iOS.DialogElements { public class CommentElement : Element { private readonly string _title, _message, _avatar; private readonly DateTimeOffset _date; public CommentElement(string title, string message, DateTimeOffset date, string avatar) { _title = title; _message = message; _date = date; _avatar = avatar; } public override UITableViewCell GetCell (UITableView tv) { var c = tv.DequeueReusableCell(CommitCellView.Key) as CommitCellView ?? CommitCellView.Create(); c.Set(_title, _message, _date, new GitHubAvatar(_avatar)); return c; } public override bool Matches(string text) { return _message?.ToLower().Contains(text.ToLower()) ?? false; } } } ================================================ FILE: CodeHub.iOS/DialogElements/CommitElement.cs ================================================ using System; using GitHubSharp.Models; using Foundation; using UIKit; using CodeHub.Core.Utilities; using CodeHub.iOS.TableViewCells; namespace CodeHub.iOS.DialogElements { public class CommitElement : Element { private readonly Action _action; private readonly CommitModel _model; private readonly GitHubAvatar _avatar; private readonly string _name; private readonly string _description; public CommitElement(CommitModel model, Action action) { _model = model; _action = action; _avatar = new GitHubAvatar(_model.GenerateGravatarUrl()); _name = _model.GenerateCommiterName(); var msg = _model?.Commit?.Message ?? string.Empty; var firstLine = msg.IndexOf("\n", StringComparison.Ordinal); _description = firstLine > 0 ? msg.Substring(0, firstLine) : msg; } public override UITableViewCell GetCell (UITableView tv) { var c = tv.DequeueReusableCell(CommitCellView.Key) as CommitCellView ?? CommitCellView.Create(); var time = DateTimeOffset.MinValue; if (_model.Commit.Committer != null) time = _model.Commit.Committer.Date; c.Set(_name, _description, time, _avatar); return c; } public override bool Matches(string text) { return _model?.Commit?.Message?.ToLower().Contains(text.ToLower()) ?? false; } public override void Selected(UITableView tableView, NSIndexPath path) { base.Selected(tableView, path); _action?.Invoke(); } } } ================================================ FILE: CodeHub.iOS/DialogElements/DummyInputElement.cs ================================================ using UIKit; using CoreGraphics; using CodeHub.iOS.DialogElements; namespace CodeHub.iOS.DialogElements { class DummyInputElement : EntryElement { public bool SpellChecking { get; set; } public DummyInputElement(string name) : base(name, name, string.Empty) { SpellChecking = true; } protected override UITextField CreateTextField(CGRect frame) { var txt = base.CreateTextField(frame); txt.AutocorrectionType = SpellChecking ? UITextAutocorrectionType.Default : UITextAutocorrectionType.No; txt.SpellCheckingType = SpellChecking ? UITextSpellCheckingType.Default : UITextSpellCheckingType.No; txt.AutocapitalizationType = SpellChecking ? txt.AutocapitalizationType : UITextAutocapitalizationType.None; return txt; } } } ================================================ FILE: CodeHub.iOS/DialogElements/Element.cs ================================================ using System; using Foundation; using UIKit; namespace CodeHub.iOS.DialogElements { public abstract class Element { private WeakReference
_weakSection; public Section Section => _weakSection?.Get(); internal void SetSection(Section section) { _weakSection = new WeakReference
(section); } protected Element() { } public abstract UITableViewCell GetCell(UITableView tv); public virtual void Deselected (UITableView tableView, NSIndexPath path) { } public virtual void Selected (UITableView tableView, NSIndexPath path) { tableView.DeselectRow (path, true); } public RootElement GetRootElement() { var section = Section; return section == null ? null : section.Root; } public UITableView GetContainerTableView() { var root = GetRootElement (); return root == null ? null : root.TableView; } public UITableViewCell GetActiveCell() { var tv = GetContainerTableView(); if (tv == null) return null; var path = IndexPath; return path == null ? null : tv.CellAt(path); } public NSIndexPath IndexPath { get { if (Section == null || Section.Root == null) return null; int row = 0; foreach (var element in Section) { if (element == this) { int nsect = 0; foreach (var sect in Section.Root) { if (Section == sect){ return NSIndexPath.FromRowSection (row, nsect); } nsect++; } } row++; } return null; } } public virtual bool Matches (string text) { return false; } } } ================================================ FILE: CodeHub.iOS/DialogElements/EntryElement.cs ================================================ using System; using UIKit; using CoreGraphics; using Foundation; using System.Reactive.Linq; using System.Reactive.Subjects; namespace CodeHub.iOS.DialogElements { public class EntryElement : Element { private string _currentValue; /// /// The value of the EntryElement /// public string Value { get { return _currentValue; } set { if (string.Equals(_currentValue, value)) return; _currentValue = value; if (entry != null) entry.Text = value; _changedSubject.OnNext(value); } } private string _caption; public string Caption { get { return _caption; } set { if (_caption == value) return; _caption = value; var cell = GetActiveCell(); if (cell != null && cell.TextLabel != null) cell.TextLabel.Text = value ?? string.Empty; } } public UIKeyboardType KeyboardType { get { return keyboardType; } set { keyboardType = value; if (entry != null) entry.KeyboardType = value; } } /// /// The type of Return Key that is displayed on the /// keyboard, you can change this to use this for /// Done, Return, Save, etc. keys on the keyboard /// public UIReturnKeyType? ReturnKeyType { get { return returnKeyType; } set { returnKeyType = value; if (entry != null && returnKeyType.HasValue) entry.ReturnKeyType = returnKeyType.Value; } } public UITextAutocapitalizationType AutocapitalizationType { get { return autocapitalizationType; } set { autocapitalizationType = value; if (entry != null) entry.AutocapitalizationType = value; } } public UITextAutocorrectionType AutocorrectionType { get { return autocorrectionType; } set { autocorrectionType = value; if (entry != null) this.autocorrectionType = value; } } public UITextFieldViewMode ClearButtonMode { get { return clearButtonMode; } set { clearButtonMode = value; if (entry != null) entry.ClearButtonMode = value; } } public UITextAlignment TextAlignment { get { return textalignment; } set{ textalignment = value; if (entry != null) { entry.TextAlignment = textalignment; } } } UITextAlignment textalignment = UITextAlignment.Left; UIKeyboardType keyboardType = UIKeyboardType.Default; UIReturnKeyType? returnKeyType = null; UITextAutocapitalizationType autocapitalizationType = UITextAutocapitalizationType.Sentences; UITextAutocorrectionType autocorrectionType = UITextAutocorrectionType.Default; UITextFieldViewMode clearButtonMode = UITextFieldViewMode.Never; bool isPassword, becomeResponder; UITextField entry; string placeholder; private readonly Subject _changedSubject = new Subject(); public event Func ShouldReturn; public UIFont TitleFont { get; set; } public UIFont EntryFont { get; set; } public UIColor TitleColor { get; set; } public IObservable Changed { get { return _changedSubject.AsObservable(); } } public EntryElement (string caption, string placeholder, string value) : this(caption, placeholder, value, false) { } public EntryElement (string caption, string placeholder, string value, bool isPassword) { TitleFont = UIFont.PreferredBody; EntryFont = UIFont.PreferredBody; TitleColor = StringElement.DefaultTitleColor; Value = value; Caption = caption; this.isPassword = isPassword; this.placeholder = placeholder; } // // Computes the X position for the entry by aligning all the entries in the Section // CGSize ComputeEntryPosition (UITableView tv, UITableViewCell cell) { if (Section.EntryAlignment.Width != 0) return Section.EntryAlignment; // If all EntryElements have a null Caption, align UITextField with the Caption // offset of normal cells (at 10px). var max = new CGSize (-15, UIStringDrawing.StringSize ("M", TitleFont).Height); foreach (var e in Section){ var ee = e as EntryElement; if (ee == null) continue; if (ee.Caption != null) { var size = UIStringDrawing.StringSize (ee.Caption, TitleFont); if (size.Width > max.Width) max = size; } } Section.EntryAlignment = new CGSize (25f + (nfloat)Math.Min (max.Width, 160), max.Height); return Section.EntryAlignment; } protected virtual UITextField CreateTextField (CGRect frame) { return new UITextField (frame) { AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleLeftMargin, Placeholder = placeholder ?? "", SecureTextEntry = isPassword, Text = Value ?? "", Tag = 1, TextAlignment = textalignment, ClearButtonMode = ClearButtonMode, Font = EntryFont, }; } static NSString cellkey = new NSString ("EntryElement"); UITableViewCell cell; public override UITableViewCell GetCell (UITableView tv) { if (cell == null) { cell = new UITableViewCell (UITableViewCellStyle.Default, cellkey); cell.SelectionStyle = UITableViewCellSelectionStyle.None; } cell.TextLabel.Text = Caption; var offset = (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone) ? 20 : 90; cell.Frame = new CGRect(cell.Frame.X, cell.Frame.Y, tv.Frame.Width-offset, cell.Frame.Height); CGSize size = ComputeEntryPosition (tv, cell); var yOffset = (cell.ContentView.Bounds.Height - size.Height) / 2 - 1; var width = cell.ContentView.Bounds.Width - size.Width; if (textalignment == UITextAlignment.Right) { // Add padding if right aligned width -= 10; } var entryFrame = new CGRect (size.Width, yOffset + 2f, width, size.Height); if (entry == null) { entry = CreateTextField (entryFrame); entry.ValueChanged += (sender, e) => Value = entry.Text; entry.EditingChanged += (sender, e) => Value = entry.Text; entry.Ended += (sender, e) => Value = entry.Text; entry.AllEditingEvents += (sender, e) => Value = entry.Text; entry.ShouldReturn += delegate { if (ShouldReturn != null) return ShouldReturn (); RootElement root = GetRootElement(); EntryElement focus = null; if (root == null) return true; foreach (var s in root) { foreach (var e in s) { if (e == this) { focus = this; } else if (focus != null && e is EntryElement) { focus = e as EntryElement; break; } } if (focus != null && focus != this) break; } if (focus != this) focus.BecomeFirstResponder (true); else focus.ResignFirstResponder (true); return true; }; entry.Started += delegate { EntryElement self = null; if (!returnKeyType.HasValue) { var returnType = UIReturnKeyType.Default; foreach (var e in Section) { if (e == this) self = this; else if (self != null && e is EntryElement) returnType = UIReturnKeyType.Next; } entry.ReturnKeyType = returnType; } else entry.ReturnKeyType = returnKeyType.Value; tv.ScrollToRow (IndexPath, UITableViewScrollPosition.Middle, true); }; cell.ContentView.AddSubview (entry); } else entry.Frame = entryFrame; if (becomeResponder){ entry.BecomeFirstResponder (); becomeResponder = false; } entry.KeyboardType = KeyboardType; entry.AutocapitalizationType = AutocapitalizationType; entry.AutocorrectionType = AutocorrectionType; cell.TextLabel.Text = Caption; cell.TextLabel.Font = TitleFont; cell.TextLabel.TextColor = TitleColor; return cell; } public override void Selected (UITableView tableView, NSIndexPath indexPath) { BecomeFirstResponder(true); base.Selected(tableView, indexPath); } public override bool Matches (string text) { return (Value != null && Value.IndexOf(text, StringComparison.CurrentCultureIgnoreCase) != -1) || base.Matches (text); } /// /// Makes this cell the first responder (get the focus) /// /// /// Whether scrolling to the location of this cell should be animated /// public virtual void BecomeFirstResponder (bool animated) { becomeResponder = true; var tv = GetContainerTableView (); if (tv == null) return; tv.ScrollToRow (IndexPath, UITableViewScrollPosition.Middle, animated); if (entry != null){ entry.BecomeFirstResponder (); becomeResponder = false; } } public virtual void ResignFirstResponder (bool animated) { becomeResponder = false; var tv = GetContainerTableView (); if (tv == null) return; tv.ScrollToRow (IndexPath, UITableViewScrollPosition.Middle, animated); if (entry != null) entry.ResignFirstResponder (); } } } ================================================ FILE: CodeHub.iOS/DialogElements/ExpandingInputElement.cs ================================================ using System; using UIKit; using Foundation; using CoreGraphics; using System.Reactive.Linq; using CodeHub.iOS.Views; using System.Reactive.Subjects; namespace CodeHub.iOS.DialogElements { public class ExpandingInputElement : Element, IElementSizing { private string _val; private readonly string _description; private IDisposable _textEditEnded; private IDisposable _textEditChanged; private readonly Subject _changedSubject = new Subject(); public string Value { get { return _val; } set { if (_val == value) return; _val = value; _changedSubject.OnNext(value); var cell = GetActiveCell() as CustomInputCell; if (cell != null) cell.TextView.Text = value; } } public IObservable Changed { get { return _changedSubject.AsObservable(); } } public UIFont Font { get; set; } public bool SpellChecking { get; set; } public Func AccessoryView { get; set; } public bool HiddenSeperator { get; set; } public ExpandingInputElement(string description) { SpellChecking = true; _description = description; Font = UIFont.PreferredBody; HiddenSeperator = true; } public override UITableViewCell GetCell(UITableView tv) { var cell = tv.DequeueReusableCell(CustomInputCell.Key) as CustomInputCell; if (cell == null) { cell = new CustomInputCell(_description); cell.SelectionStyle = UITableViewCellSelectionStyle.None; cell.TextView.Font = Font; cell.TextView.InputAccessoryView = AccessoryView != null ? AccessoryView(cell.TextView) : new UIView(); cell.TextView.AutocorrectionType = SpellChecking ? UITextAutocorrectionType.Default : UITextAutocorrectionType.No; cell.TextView.SpellCheckingType = SpellChecking ? UITextSpellCheckingType.Default : UITextSpellCheckingType.No; cell.TextView.AutocapitalizationType = SpellChecking ? UITextAutocapitalizationType.Sentences : UITextAutocapitalizationType.None; } cell.HiddenSeperator = HiddenSeperator; if (_textEditEnded != null) _textEditEnded.Dispose(); _textEditEnded = Observable.FromEventPattern(x => cell.TextView.Ended += x, x => cell.TextView.Ended -= x) .Subscribe(x => Value = cell.TextView.Text); if (_textEditChanged != null) _textEditChanged.Dispose(); _textEditChanged = Observable.FromEventPattern(x => cell.TextView.Changed += x, x => cell.TextView.Changed -= x) .Subscribe(x => { Value = cell.TextView.Text; tv.BeginUpdates(); tv.EndUpdates(); var caret = cell.TextView.GetCaretRectForPosition(cell.TextView.SelectedTextRange.Start); var cursorRect = tv.ConvertRectFromView(caret, cell.TextView); var kk = cursorRect.Size; kk.Height += 8.0f; cursorRect.Size = kk; tv.ScrollRectToVisible(cursorRect, false); }); cell.TextView.Text = Value ?? string.Empty; return cell; } private class CustomInputCell : UITableViewCell { public static NSString Key = new NSString("CustomInputCell"); public static UIFont InputFont = UIFont.PreferredBody; public readonly UITextView TextView; public bool HiddenSeperator { get; set; } public CustomInputCell(string placeholder) : base(UITableViewCellStyle.Default, Key) { HiddenSeperator = true; TextView = new ExtendedUITextView() { Frame = new CGRect(12, 0, ContentView.Frame.Width - 24f, ContentView.Frame.Height), ScrollEnabled = false, Placeholder = placeholder }; TextView.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight; TextView.BackgroundColor = UIColor.Clear; TextView.Font = InputFont; TextView.TextColor = UIColor.Black; ContentView.Add(TextView); } public override void LayoutSubviews() { base.LayoutSubviews(); SeparatorInset = new UIEdgeInsets(0, HiddenSeperator ? Bounds.Width : 0, 0, 0); } } public nfloat GetHeight(UITableView tableView, NSIndexPath indexPath) { var str = new NSString(Value ?? string.Empty); var height = (int)str.StringSize(CustomInputCell.InputFont, new CGSize(tableView.Bounds.Width - 24f, 10000), UILineBreakMode.WordWrap).Height + 60f; return height > 120 ? height : 120; } } } ================================================ FILE: CodeHub.iOS/DialogElements/HtmlElement.cs ================================================ using System; using UIKit; using Foundation; using CoreGraphics; using System.Reactive.Subjects; using System.Reactive.Linq; using WebKit; using System.Threading.Tasks; namespace CodeHub.iOS.DialogElements { public class HtmlElement : Element, IElementSizing, IDisposable { private readonly ISubject _urlSubject = new Subject(); private readonly Lazy _webView; private nfloat _height; protected readonly NSString Key; public IObservable UrlRequested { get { return _urlSubject.AsObservable(); } } private WKWebView WebView { get { return _webView.Value; } } public bool HasValue { get; private set; } public nfloat Height { get { return _height; } } public void SetValue(string value) { if (value == null) { WebView.LoadHtmlString(string.Empty, NSBundle.MainBundle.BundleUrl); } else { WebView.LoadHtmlString(value, NSBundle.MainBundle.BundleUrl); } HasValue = value != null; } public void SetLayout() { WebView.SetNeedsLayout(); } private async Task GetSize() { if (HasValue) { try { var size = await WebView.EvaluateJavaScriptAsync("size();"); if (size != null) { nfloat newsize; if (nfloat.TryParse(size.ToString(), out newsize)) return newsize; } } catch { } } return _height; } private async Task RefreshSize() { var size = await GetSize(); if (_height != size) { _height = size; Reload(); } } public void Dispose() { WebView.RemoveFromSuperview(); WebView.Dispose(); } public async Task ForceResize() { var f = WebView.Frame; f.Height = 1; WebView.Frame = f; f.Height = _height = await GetSize(); WebView.Frame = f; } public HtmlElement (string cellKey) { Key = new NSString(cellKey); _height = 0f; _webView = new Lazy(() => { var bounds = UIScreen.MainScreen.Bounds; var webView = new WKWebView(new CGRect(0, 0, bounds.Width, 44f), new WKWebViewConfiguration()); webView.AutoresizingMask = UIViewAutoresizing.FlexibleHeight | UIViewAutoresizing.FlexibleWidth; webView.ScrollView.ScrollEnabled = false; webView.ScrollView.Bounces = false; webView.NavigationDelegate = new NavigationDelegate(this); return webView; }); } public void CheckHeight() { // var f = WebView.Frame; // f.Height = 1; // WebView.Frame = f; // f.Height = _height = await GetSize(); // WebView.Frame = f; Reload(); } public void Reload() { GetRootElement()?.Reload(this); } public nfloat GetHeight (UITableView tableView, NSIndexPath indexPath) { return _height; } public override UITableViewCell GetCell (UITableView tv) { var cell = tv.DequeueReusableCell (Key); if (cell == null){ cell = new UITableViewCell (UITableViewCellStyle.Default, Key); cell.SelectionStyle = UITableViewCellSelectionStyle.None; } WebView.Frame = new CGRect(0, 0, cell.ContentView.Frame.Width, cell.ContentView.Frame.Height); WebView.RemoveFromSuperview(); cell.ContentView.AddSubview (WebView); cell.ContentView.Layer.MasksToBounds = true; cell.ContentView.AutosizesSubviews = true; cell.SeparatorInset = new UIEdgeInsets(0, 0, 0, 0); return cell; } private void OnLoadFinished() { RefreshSize().ToBackground(); } private bool OnLoadStart(WKNavigationAction navigation) { if (navigation.Request.Url.AbsoluteString.StartsWith("app://resize")) { RefreshSize().ToBackground(); return false; } if (!navigation.Request.Url.AbsoluteString.StartsWith("file://")) { if (UrlRequested != null) _urlSubject.OnNext(navigation.Request.Url.AbsoluteString); return false; } return true; } private class NavigationDelegate : WKNavigationDelegate { private readonly WeakReference _parent; public NavigationDelegate(HtmlElement parent) { _parent = new WeakReference(parent); } public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation) { _parent.Get()?.OnLoadFinished(); } public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action decisionHandler) { var ret = _parent.Get()?.OnLoadStart(navigationAction) ?? true; decisionHandler(ret ? WKNavigationActionPolicy.Allow : WKNavigationActionPolicy.Cancel); } } } } ================================================ FILE: CodeHub.iOS/DialogElements/IElementSizing.cs ================================================ using UIKit; using Foundation; using System; namespace CodeHub.iOS.DialogElements { public interface IElementSizing { nfloat GetHeight (UITableView tableView, NSIndexPath indexPath); } } ================================================ FILE: CodeHub.iOS/DialogElements/IssueElement.cs ================================================ using System; using UIKit; using Foundation; using CodeHub.iOS.TableViewCells; namespace CodeHub.iOS.DialogElements { public class IssueElement : Element, IElementSizing { public UITableViewCellStyle Style { get; set; } public UIColor BackgroundColor { get; set; } public string Id { get; set; } public string Title { get; set; } public string Assigned { get; set; } public string Status { get; set; } public string Priority { get; set; } public string Kind { get; set; } public DateTimeOffset LastUpdated { get; set; } public IssueElement(string id, string title, string assigned, string status, string priority, string kind, DateTimeOffset lastUpdated) { if (string.IsNullOrEmpty(assigned)) assigned = "unassigned"; Id = id; Title = title; Assigned = assigned; Status = status; Priority = priority; Kind = kind; LastUpdated = lastUpdated; Style = UITableViewCellStyle.Default; } public nfloat GetHeight (UITableView tableView, NSIndexPath indexPath) { return 69f; } public event Action Tapped; public override UITableViewCell GetCell (UITableView tv) { var cell = tv.DequeueReusableCell(IssueCellView.Key) as IssueCellView ?? IssueCellView.Create(); cell.Bind(Title, Status, Priority, Assigned, LastUpdated, Id, Kind); return cell; } public override void Selected(UITableView tableView, NSIndexPath path) { base.Selected(tableView, path); Tapped?.Invoke(); } public override bool Matches(string text) { var id = Id ?? string.Empty; var title = Title ?? string.Empty; return id.IndexOf(text, StringComparison.OrdinalIgnoreCase) != -1 || title.IndexOf(text, StringComparison.OrdinalIgnoreCase) != -1; } } } ================================================ FILE: CodeHub.iOS/DialogElements/LabelElement.cs ================================================ using CoreGraphics; using UIKit; namespace CodeHub.iOS.DialogElements { public class LabelElement : StringElement { public string Name { get; private set; } public LabelElement(string name, string color) : base(name) { Name = name; Image = CreateImage(color); } private static UIImage CreateImage(string color) { try { var red = color.Substring(0, 2); var green = color.Substring(2, 2); var blue = color.Substring(4, 2); var redB = System.Convert.ToByte(red, 16); var greenB = System.Convert.ToByte(green, 16); var blueB = System.Convert.ToByte(blue, 16); var size = new CGSize(28f, 28f); var cgColor = UIColor.FromRGB(redB, greenB, blueB).CGColor; UIGraphics.BeginImageContextWithOptions(size, false, 0); var ctx = UIGraphics.GetCurrentContext(); ctx.SetLineWidth(1.0f); ctx.SetStrokeColor(cgColor); ctx.AddEllipseInRect(new CGRect(0, 0, size.Width, size.Height)); ctx.SetFillColor(cgColor); ctx.FillPath(); var image = UIGraphics.GetImageFromCurrentImageContext(); UIGraphics.EndImageContext(); return image; } catch { return null; } } } } ================================================ FILE: CodeHub.iOS/DialogElements/LoadMoreElement.cs ================================================ // // This cell does not perform cell recycling, do not use as // sample code for new elements. // using System; using CoreGraphics; using System.Threading; using CoreFoundation; using Foundation; using UIKit; namespace CodeHub.iOS.DialogElements { public class LoadMoreElement : OwnerDrawnElement { static NSString key = new NSString ("LoadMoreElement"); public string NormalCaption { get; set; } public string LoadingCaption { get; set; } public UIColor TextColor { get; set; } public UIColor BackgroundColor { get; set; } public bool AutoLoadOnVisible { get; set; } public event Action Tapped = null; public UIFont Font; UITextAlignment alignment = UITextAlignment.Center; bool animating; UILabel _caption; public LoadMoreElement () : base (UITableViewCellStyle.Default, key.ToString()) { } public LoadMoreElement (string normalCaption, string loadingCaption, Action tapped) : this (normalCaption, loadingCaption, tapped, UIFont.BoldSystemFontOfSize (16), UIColor.Black) { } public LoadMoreElement (string normalCaption, string loadingCaption, Action tapped, UIFont font, UIColor textColor) : base (UITableViewCellStyle.Default, key.ToString()) { NormalCaption = normalCaption; LoadingCaption = loadingCaption; Tapped += tapped; Font = font; TextColor = textColor; } public override void Draw(CGRect bounds, CoreGraphics.CGContext context, UIView view) { if (AutoLoadOnVisible) { LoadMore(); } } protected override void CellCreated(UITableViewCell cell, UIView view) { var activityIndicator = new UIActivityIndicatorView () { ActivityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray, Tag = 1 }; _caption = new UILabel () { AdjustsFontSizeToFitWidth = false, Tag = 2, HighlightedTextColor = UIColor.White, AutoresizingMask = UIViewAutoresizing.FlexibleWidth, }; cell.ContentView.AddSubview (_caption); cell.ContentView.AddSubview (activityIndicator); view.BackgroundColor = BackgroundColor ?? UIColor.Clear; } public override UITableViewCell GetCell (UITableView tv) { var cell = base.GetCell(tv); var activityIndicator = cell.ContentView.ViewWithTag (1) as UIActivityIndicatorView; var caption = cell.ContentView.ViewWithTag (2) as UILabel; if (Animating){ caption.Text = LoadingCaption; activityIndicator.Hidden = false; activityIndicator.StartAnimating (); } else { caption.Text = NormalCaption; activityIndicator.Hidden = true; activityIndicator.StopAnimating (); } if (BackgroundColor != null){ cell.ContentView.BackgroundColor = BackgroundColor ?? UIColor.Clear; } else { cell.ContentView.BackgroundColor = null; } caption.BackgroundColor = UIColor.Clear; caption.TextColor = TextColor ?? UIColor.Black; caption.Font = Font ?? UIFont.BoldSystemFontOfSize (16); caption.TextAlignment = Alignment; Layout (cell, activityIndicator, caption); return cell; } public bool Animating { get { return animating; } set { if (animating == value) return; animating = value; var cell = GetActiveCell (); if (cell == null) return; var activityIndicator = cell.ContentView.ViewWithTag (1) as UIActivityIndicatorView; var caption = cell.ContentView.ViewWithTag (2) as UILabel; if (value){ caption.Text = LoadingCaption; activityIndicator.Hidden = false; activityIndicator.StartAnimating (); } else { activityIndicator.StopAnimating (); activityIndicator.Hidden = true; caption.Text = NormalCaption; } Layout (cell, activityIndicator, caption); } } public override void Selected (UITableView tableView, NSIndexPath path) { base.Selected(tableView, path); LoadMore(); } private void LoadMore() { if (Animating) return; if (Tapped != null){ Animating = true; Tapped (this); } } CGSize GetTextSize (string text) { return new NSString (text).StringSize (Font, UIScreen.MainScreen.Bounds.Width, UILineBreakMode.TailTruncation); } public static int Padding = 10; public static int IndicatorSize = 20; public override nfloat Height(CGRect bounds) { return GetTextSize (Animating ? LoadingCaption : NormalCaption).Height + 2*Padding; } void Layout (UITableViewCell cell, UIActivityIndicatorView activityIndicator, UILabel caption) { var sbounds = cell.ContentView.Bounds; var size = GetTextSize (Animating ? LoadingCaption : NormalCaption); if (!activityIndicator.Hidden) activityIndicator.Frame = new CGRect ((sbounds.Width-size.Width)/2-IndicatorSize*2, Padding, IndicatorSize, IndicatorSize); caption.Frame = new CGRect (10, Padding, sbounds.Width-20, size.Height); } public UITextAlignment Alignment { get { return alignment; } set { alignment = value; } } public UITableViewCellAccessory Accessory { get; set; } } } ================================================ FILE: CodeHub.iOS/DialogElements/MenuElement.cs ================================================ using System; using UIKit; using CoreGraphics; namespace CodeHub.iOS.DialogElements { public class MenuElement : StringElement { private int _notificationNumber; public int NotificationNumber { get { return _notificationNumber; } set { if (value == _notificationNumber) return; _notificationNumber = value; var cell = GetActiveCell() as Cell; if (cell != null) cell.NotificationNumber = value; } } public MenuElement(string title, Action tapped, UIImage image, Uri imageUrl = null) : base(title) { Clicked.Subscribe(_ => tapped?.Invoke()); TextColor = UIColor.FromRGB(213, 213, 213); Image = image; ImageUri = imageUrl; Accessory = UITableViewCellAccessory.None; } //We want everything to be the same size as far as images go. //So, during layout, we'll resize the imageview and pin it to a specific size! private class Cell : UITableViewCell { private const float ImageSize = 20f; private readonly UILabel _numberView; private int _notificationNumber; public int NotificationNumber { get { return _notificationNumber; } set { _notificationNumber = value; SetNeedsLayout(); LayoutIfNeeded(); } } public Cell(UITableViewCellStyle style, string key) : base(style, key) { SelectedBackgroundView = new UIView { BackgroundColor = UIColor.FromRGB(25, 25, 25) }; _numberView = new UILabel { BackgroundColor = UIColor.FromRGB(54, 54, 54) }; _numberView.Layer.MasksToBounds = true; _numberView.Layer.CornerRadius = 5f; _numberView.TextAlignment = UITextAlignment.Center; _numberView.TextColor = UIColor.White; _numberView.Font = UIFont.SystemFontOfSize(12f); } public override void LayoutSubviews() { base.LayoutSubviews(); if (ImageView != null) { var center = ImageView.Center; ImageView.Frame = new CGRect(0, 0, ImageSize, ImageSize); ImageView.Center = new CGPoint(ImageSize, center.Y); if (TextLabel != null) { var frame = TextLabel.Frame; frame.X = ImageSize * 2; frame.Width += (TextLabel.Frame.X - frame.X); TextLabel.Frame = frame; } } if (NotificationNumber > 0) { _numberView.Frame = new CGRect(ContentView.Bounds.Width - 44, 11, 34, 22f); _numberView.Text = NotificationNumber.ToString(); AddSubview(_numberView); } else { _numberView.RemoveFromSuperview(); } } } public override UITableViewCell GetCell(UITableView tv) { var cell = base.GetCell(tv) as Cell; cell.NotificationNumber = NotificationNumber; cell.ImageView.Layer.CornerRadius = ImageUri != null ? (cell.ImageView.Frame.Height / 2) : 0; cell.BackgroundColor = UIColor.Clear; return cell; } protected override UITableViewCell CreateTableViewCell(UITableViewCellStyle style, string key) { var cell = new Cell(style, key); cell.ImageView.Layer.MasksToBounds = true; cell.ImageView.TintColor = UIColor.FromRGB(0xd5, 0xd5, 0xd5); return cell; } } } ================================================ FILE: CodeHub.iOS/DialogElements/MilestoneElement.cs ================================================ using System; using Foundation; using UIKit; using CodeHub.iOS.TableViewCells; namespace CodeHub.iOS.DialogElements { public class MilestoneElement : Element { private readonly string _title; private readonly int _openIssues; private readonly int _closedIssues; private readonly DateTimeOffset? _dueDate; public event Action Tapped; public UITableViewCellAccessory Accessory = UITableViewCellAccessory.None; public int Number { get; private set; } public MilestoneElement(int number, string title, int openIssues, int closedIssues, DateTimeOffset? dueDate) { Number = number; _title = title; _openIssues = openIssues; _closedIssues = closedIssues; _dueDate = dueDate; } public override UITableViewCell GetCell(UITableView tv) { var cell = tv.DequeueReusableCell(MilestoneTableViewCell.Key) as MilestoneTableViewCell ?? new MilestoneTableViewCell { SelectionStyle = UITableViewCellSelectionStyle.Blue }; cell.Accessory = Accessory; cell.Init(_title, _openIssues, _closedIssues, _dueDate); return cell; } public override void Selected(UITableView tableView, NSIndexPath path) { base.Selected(tableView, path); Tapped?.Invoke(); } } } ================================================ FILE: CodeHub.iOS/DialogElements/MultilinedElement.cs ================================================ using UIKit; using CodeHub.iOS.TableViewCells; using System.Reactive.Subjects; using System; using System.Reactive.Linq; using Foundation; namespace CodeHub.iOS.DialogElements { public class MultilinedElement : Element { private readonly Subject _tapped = new Subject(); private string _caption; public string Caption { get { return _caption; } set { if (_caption == value) return; _caption = value; var cell = GetActiveCell() as MultilinedCellView; if (cell != null) cell.Caption = value; } } private string _details; public string Details { get { return _details; } set { if (_details == value) return; _details = value; var cell = GetActiveCell() as MultilinedCellView; if (cell != null) cell.Details = value; } } public IObservable Clicked { get { return _tapped.AsObservable(); } } public MultilinedElement(string caption = null, string details = null) { Caption = caption; Details = details; } public override UITableViewCell GetCell(UITableView tv) { var cell = tv.DequeueReusableCell(MultilinedCellView.Key) as MultilinedCellView ?? MultilinedCellView.Create(); cell.Caption = Caption; cell.Details = Details; return cell; } public override void Selected (UITableView tableView, NSIndexPath indexPath) { base.Selected(tableView, indexPath); _tapped.OnNext(this); } } } ================================================ FILE: CodeHub.iOS/DialogElements/NewsFeedElement.cs ================================================ using System; using System.Collections.Generic; using CoreGraphics; using Foundation; using UIKit; using CodeHub.iOS.TableViewCells; using Humanizer; namespace CodeHub.iOS.DialogElements { public class NewsFeedElement : Element { private readonly string _time; private readonly Uri _imageUri; private readonly UIImage _actionImage; private readonly Action _tapped; private readonly bool _multilined; private readonly NSMutableAttributedString _attributedHeader; private readonly NSMutableAttributedString _attributedBody; private readonly List _headerLinks; private readonly List _bodyLinks; public static UIColor LinkColor = Theme.CurrentTheme.MainTitleColor; public Action WebLinkClicked; public class TextBlock { public string Value; public Action Tapped; public TextBlock() { } public TextBlock(string value) { Value = value; } public TextBlock(string value, Action tapped = null) : this (value) { Tapped = tapped; } } public NewsFeedElement(string imageUrl, DateTimeOffset time, IEnumerable headerBlocks, IEnumerable bodyBlocks, UIImage littleImage, Action tapped, bool multilined) { Uri.TryCreate(imageUrl, UriKind.Absolute, out _imageUri); _time = time.Humanize(); _actionImage = littleImage; _tapped = tapped; _multilined = multilined; var header = CreateAttributedStringFromBlocks(UIFont.PreferredBody, Theme.CurrentTheme.MainTextColor, headerBlocks); _attributedHeader = header.Item1; _headerLinks = header.Item2; var body = CreateAttributedStringFromBlocks(UIFont.PreferredSubheadline, Theme.CurrentTheme.MainSubtitleColor, bodyBlocks); _attributedBody = body.Item1; _bodyLinks = body.Item2; } private static Tuple> CreateAttributedStringFromBlocks(UIFont font, UIColor primaryColor, IEnumerable blocks) { var attributedString = new NSMutableAttributedString(); var links = new List(); nint lengthCounter = 0; int i = 0; CoreText.CTFont ctFont; try { ctFont = new CoreText.CTFont(font.FamilyName, font.PointSize); } catch { ctFont = CGFont.CreateWithFontName(font.Name).ToCTFont(font.PointSize); } foreach (var b in blocks) { UIColor color = null; if (b.Tapped != null) color = LinkColor; color = color ?? primaryColor; var str = new NSAttributedString(b.Value, new CoreText.CTStringAttributes() { ForegroundColor = color.CGColor, Font = ctFont }); attributedString.Append(str); var strLength = str.Length; if (b.Tapped != null) { var weakTapped = new WeakReference(b.Tapped); links.Add(new NewsCellView.Link { Range = new NSRange(lengthCounter, strLength), Callback = () => weakTapped.Get()?.Invoke(), Id = i++ }); } lengthCounter += strLength; } return new Tuple>(attributedString, links); } public override UITableViewCell GetCell (UITableView tv) { var cell = tv.DequeueReusableCell(NewsCellView.Key) as NewsCellView ?? NewsCellView.Create(); cell.Set(_imageUri, _time, _actionImage, _attributedHeader, _attributedBody, _headerLinks, _bodyLinks, WebLinkClicked, _multilined); return cell; } public override void Selected(UITableView tableView, NSIndexPath path) { base.Selected(tableView, path); _tapped?.Invoke(); } } } ================================================ FILE: CodeHub.iOS/DialogElements/OwnerDrawnElement.cs ================================================ using System; using CoreGraphics; using UIKit; using Foundation; namespace CodeHub.iOS.DialogElements { public abstract class OwnerDrawnElement : Element, IElementSizing { public string CellReuseIdentifier { get;set; } public UITableViewCellStyle Style { get;set; } public OwnerDrawnElement (UITableViewCellStyle style, string cellIdentifier) { this.CellReuseIdentifier = cellIdentifier; this.Style = style; } public nfloat GetHeight (UITableView tableView, NSIndexPath indexPath) { return Height(tableView.Bounds); } public override UITableViewCell GetCell (UITableView tv) { OwnerDrawnCell cell = tv.DequeueReusableCell(this.CellReuseIdentifier) as OwnerDrawnCell; if (cell == null) { cell = new OwnerDrawnCell(this, this.Style, this.CellReuseIdentifier); cell.AutosizesSubviews = true; cell.AutoresizingMask = UIViewAutoresizing.FlexibleWidth; CellCreated(cell, cell.view); } else { cell.Element = this; } cell.Update(); return cell; } protected virtual void CellCreated(UITableViewCell cell, UIView view) { } public abstract void Draw(CGRect bounds, CGContext context, UIView view); public abstract nfloat Height(CGRect bounds); class OwnerDrawnCell : UITableViewCell { public OwnerDrawnCellView view; public OwnerDrawnCell(OwnerDrawnElement element, UITableViewCellStyle style, string cellReuseIdentifier) : base(style, cellReuseIdentifier) { Element = element; } public OwnerDrawnElement Element { get { return view.Element; } set { if (view == null) { view = new OwnerDrawnCellView (value); ContentView.Add (view); } else { view.Element = value; } } } public void Update() { SetNeedsDisplay(); view.SetNeedsDisplay(); } public override void LayoutSubviews() { base.LayoutSubviews(); view.Frame = ContentView.Bounds; } } class OwnerDrawnCellView : UIView { OwnerDrawnElement element; public OwnerDrawnCellView(OwnerDrawnElement element) { this.element = element; this.AutosizesSubviews = true; this.AutoresizingMask = UIViewAutoresizing.FlexibleWidth; this.BackgroundColor = UIColor.Clear; } public OwnerDrawnElement Element { get { return element; } set { element = value; } } public void Update() { SetNeedsDisplay(); } public override void Draw (CGRect rect) { CGContext context = UIGraphics.GetCurrentContext(); element.Draw(rect, context, this); } } } } ================================================ FILE: CodeHub.iOS/DialogElements/PaginateElement.cs ================================================ using UIKit; namespace CodeHub.iOS.DialogElements { public class PaginateElement : LoadMoreElement { static PaginateElement() { Padding = 20; } public PaginateElement(string normal, string loading) { NormalCaption = normal; LoadingCaption = loading; Font = UIFont.PreferredBody; TextColor = StringElement.DefaultTitleColor; } protected override void CellCreated(UITableViewCell cell, UIView view) { base.CellCreated(cell, view); cell.BackgroundColor = UIColor.White; } } } ================================================ FILE: CodeHub.iOS/DialogElements/PullRequestElement.cs ================================================ using System; using GitHubSharp.Models; using Foundation; using CodeHub.iOS.Cells; using UIKit; using CodeHub.Core.Utilities; namespace CodeHub.iOS.DialogElements { public class PullRequestElement : Element { private readonly Action _action; private readonly PullRequestModel _model; public PullRequestElement(PullRequestModel model, Action action) { _model = model; _action = action; } public override UITableViewCell GetCell (UITableView tv) { var c = tv.DequeueReusableCell(PullRequestCellView.Key) as PullRequestCellView ?? PullRequestCellView.Create(); c.Set(_model.Title, _model.CreatedAt, new GitHubAvatar(_model.User?.AvatarUrl)); return c; } public override bool Matches(string text) { return _model.Title.ToLower().Contains(text.ToLower()); } public override void Selected(UITableView tableView, NSIndexPath path) { base.Selected(tableView, path); _action?.Invoke(); } } } ================================================ FILE: CodeHub.iOS/DialogElements/RootElement.cs ================================================ using System; using Foundation; using System.Collections.Generic; using UIKit; using System.Collections.ObjectModel; using System.Linq; namespace CodeHub.iOS.DialogElements { public class RootElement : IEnumerable
{ private readonly List
_sections = new List
(); private readonly WeakReference _tableView; public UITableView TableView { get { return _tableView.Get(); } } public IReadOnlyList
Sections { get { return new ReadOnlyCollection
(_sections); } } public RootElement(UITableView tableView) { _tableView = new WeakReference(tableView); } public int Count { get { return _sections.Count; } } public Section this [int idx] { get { return _sections[idx]; } } internal int IndexOf (Section target) { int idx = 0; foreach (Section s in _sections){ if (s == target) return idx; idx++; } return -1; } public void Add (Section section) { if (section == null) return; _sections.Add (section); section.Root = this; _tableView.Get()?.InsertSections (MakeIndexSet (_sections.Count-1, 1), UITableViewRowAnimation.None); } public void Add (IEnumerable
sections) { foreach (var s in sections) Add (s); } public void Add(params Section[] sections) { Add((IEnumerable
)sections); } NSIndexSet MakeIndexSet (int start, int count) { NSRange range; range.Location = start; range.Length = count; return NSIndexSet.FromNSRange (range); } public void Insert (int idx, UITableViewRowAnimation anim, params Section [] newSections) { if (idx < 0 || idx > _sections.Count) return; if (newSections == null) return; _tableView.Get()?.BeginUpdates (); int pos = idx; foreach (var s in newSections){ s.Root = this; _sections.Insert (pos++, s); } _tableView.Get()?.InsertSections (MakeIndexSet (idx, newSections.Length), anim); _tableView.Get()?.EndUpdates (); } public void Insert (int idx, Section section) { Insert (idx, UITableViewRowAnimation.None, section); } public void RemoveAt (int idx, UITableViewRowAnimation anim) { if (idx < 0 || idx >= _sections.Count) return; _sections.RemoveAt (idx); _tableView.Get()?.DeleteSections (NSIndexSet.FromIndex (idx), anim); } public void Remove (Section s) { if (s == null) return; int idx = _sections.IndexOf (s); if (idx == -1) return; RemoveAt (idx, UITableViewRowAnimation.Fade); } public void Remove (Section s, UITableViewRowAnimation anim) { if (s == null) return; int idx = _sections.IndexOf (s); if (idx == -1) return; RemoveAt (idx, anim); } public void Clear () { foreach (var s in _sections) s.Root = null; _sections.Clear(); _tableView.Get()?.ReloadData (); } public void Reset(IEnumerable
sections) { foreach (var s in _sections) s.Root = null; _sections.Clear(); foreach (var s in sections) { s.Root = this; _sections.Add(s); } _tableView.Get()?.ReloadData(); } public void Reset(params Section[] sections) { Reset((IEnumerable
)sections); } public void Reload (Element element) { Reload(new [] { element }); } public void Reload (Element[] elements) { var paths = (elements ?? Enumerable.Empty()) .Where(x => x.Section != null && x.Section.Root != null) .Select(x => x.IndexPath); try { _tableView.Get()?.BeginUpdates(); _tableView.Get()?.ReloadRows(paths.ToArray(), UITableViewRowAnimation.None); } finally { _tableView.Get()?.EndUpdates(); } } public IEnumerator
GetEnumerator() { return _sections.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } } } ================================================ FILE: CodeHub.iOS/DialogElements/Section.cs ================================================ using System.Collections.Generic; using UIKit; using CoreGraphics; using Foundation; using System.Collections.ObjectModel; namespace CodeHub.iOS.DialogElements { public class Section : IEnumerable { object header, footer; private readonly List _elements = new List (); public RootElement Root { get; internal set; } public IReadOnlyList Elements { get { return new ReadOnlyCollection(_elements); } } // X corresponds to the alignment, Y to the height of the password public CGSize EntryAlignment; public Section() { } public Section(UIView header, UIView footer = null) { HeaderView = header; FooterView = footer; } public Section(string header, string footer = null) { Header = header; Footer = footer; } /// /// The section header, as a string /// public string Header { get { return header as string; } set { header = value; } } /// /// The section footer, as a string. /// public string Footer { get { return footer as string; } set { footer = value; } } /// /// The section's header view. /// public UIView HeaderView { get { return header as UIView; } set { header = value; } } /// /// The section's footer view. /// public UIView FooterView { get { return footer as UIView; } set { footer = value; } } /// /// Adds a new child Element to the Section /// /// /// An element to add to the section. /// public void Add (Element element) { if (element == null) return; _elements.Add (element); element.SetSection(this); if (Root != null) InsertVisual (_elements.Count-1, UITableViewRowAnimation.None, 1); } public void Add(IEnumerable elements) { AddAll(elements); } /// /// Add version that can be used with LINQ /// /// /// An enumerable list that can be produced by something like: /// from x in ... select (Element) new MyElement (...) /// public int AddAll(IEnumerable elements) { int count = 0; foreach (var e in elements){ Add (e); count++; } return count; } /// /// Inserts a series of elements into the Section using the specified animation /// /// /// The index where the elements are inserted /// /// /// The animation to use /// /// /// A series of elements. /// public void Insert (int idx, UITableViewRowAnimation anim, params Element [] newElements) { if (newElements == null) return; int pos = idx; foreach (var e in newElements) { _elements.Insert (pos++, e); e.SetSection(this); } if (Root != null && Root.TableView != null) { if (anim == UITableViewRowAnimation.None) Root.TableView.ReloadData (); else InsertVisual (idx, anim, newElements.Length); } } public int Insert (int idx, UITableViewRowAnimation anim, IEnumerable newElements) { if (newElements == null) return 0; int pos = idx; int count = 0; foreach (var e in newElements) { _elements.Insert (pos++, e); e.SetSection(this); count++; } if (Root != null && Root.TableView != null) { if (anim == UITableViewRowAnimation.None) Root.TableView.ReloadData (); else InsertVisual (idx, anim, pos-idx); } return count; } void InsertVisual (int idx, UITableViewRowAnimation anim, int count) { if (Root == null || Root.TableView == null) return; int sidx = Root.IndexOf (this); var paths = new NSIndexPath [count]; for (int i = 0; i < count; i++) paths [i] = NSIndexPath.FromRowSection (idx+i, sidx); Root.TableView.InsertRows (paths, anim); } public void Insert (int index, params Element [] newElements) { Insert (index, UITableViewRowAnimation.None, newElements); } public void Remove (Element e, UITableViewRowAnimation animation = UITableViewRowAnimation.Automatic) { if (e == null) return; for (int i = _elements.Count; i > 0;) { i--; if (_elements [i] == e) { RemoveRange (i, 1, animation); e.SetSection(null); return; } } } public void Remove (int idx) { RemoveRange (idx, 1); } /// /// Remove a range of elements from the section with the given animation /// /// /// Starting position /// /// /// Number of elements to remove form the section /// /// /// The animation to use while removing the elements /// public void RemoveRange (int start, int count, UITableViewRowAnimation anim = UITableViewRowAnimation.Fade) { if (start < 0 || start >= _elements.Count) return; if (count == 0) return; if (start+count > _elements.Count) count = _elements.Count-start; _elements.RemoveRange (start, count); if (Root != null && Root.TableView != null) { int sidx = Root.IndexOf(this); var paths = new NSIndexPath [count]; for (int i = 0; i < count; i++) paths[i] = NSIndexPath.FromRowSection(start + i, sidx); Root.TableView.DeleteRows(paths, anim); //Root.TableView.ReloadData(); } } public int Count { get { return _elements.Count; } } public IEnumerator GetEnumerator() { return _elements.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } public Element this [int idx] { get { return _elements[idx]; } } public void Clear () { foreach (var e in _elements) e.SetSection(null); _elements.Clear(); if (Root != null && Root.TableView != null) Root.TableView.ReloadData (); } public void Reset(IEnumerable elements, UITableViewRowAnimation animation = UITableViewRowAnimation.Fade) { _elements.Clear(); _elements.AddRange(elements); foreach (var e in _elements) e.SetSection(this); if (Root != null && Root.TableView != null) Root.TableView.ReloadData(); } } } ================================================ FILE: CodeHub.iOS/DialogElements/SplitButtonElement.cs ================================================ using UIKit; using CoreGraphics; using System; using System.Collections.Generic; using System.Linq; using System.Reactive; using System.Reactive.Subjects; namespace CodeHub.iOS.DialogElements { public class SplitButtonElement : Element { public static UIColor CaptionColor = UIColor.FromRGB(41, 41, 41); public static UIFont CaptionFont = UIFont.SystemFontOfSize(14f); public static UIColor TextColor = UIColor.FromRGB(100, 100, 100); public static UIFont TextFont = UIFont.BoldSystemFontOfSize(14f); private readonly List