From 2d6bc0d85325b2ff275ec7303ed6050155270f4e Mon Sep 17 00:00:00 2001 From: Quinn Damerell Date: Tue, 19 Jan 2016 08:48:07 -0800 Subject: [PATCH] Refactoring out the flip post panel from flip view so the code has better separation. Also adding some nice animations for the header when viewing comments. --- BaconBackend/Collectors/DeferredCollector.cs | 27 +- BaconBackend/DataObjects/Post.cs | 129 - BaconBackend/Managers/UiSettingManager.cs | 28 + Baconit/Baconit.csproj | 16 +- .../Panels/WebPageContentPanel.xaml.cs | 2 +- .../HelperControls/EndDetectingListView.cs | 35 +- .../ExtendedVisualStateManger.cs | 64 + Baconit/MainPage.xaml.cs | 1 + Baconit/Panels/FlipView/FlipViewPanel.xaml | 53 + Baconit/Panels/FlipView/FlipViewPanel.xaml.cs | 948 ++++++++ .../FlipViewPostCommentManager.cs | 117 +- .../Panels/FlipView/FlipViewPostContext.cs | 69 + Baconit/Panels/FlipView/FlipViewPostItem.cs | 72 + .../FlipViewPostPanel.xaml} | 246 +- .../Panels/FlipView/FlipViewPostPanel.xaml.cs | 1537 ++++++++++++ Baconit/Panels/FlipViewPanel.xaml.cs | 2100 ----------------- Baconit/Panels/MessageInbox.xaml.cs | 1 + Baconit/Panels/Search.xaml.cs | 1 + .../SettingsPanels/FlipViewSettings.xaml | 6 + .../SettingsPanels/FlipViewSettings.xaml.cs | 11 + Baconit/Panels/SubredditPanel.xaml.cs | 1 + Baconit/Panels/UserProfile.xaml.cs | 1 + 22 files changed, 3076 insertions(+), 2389 deletions(-) create mode 100644 Baconit/HelperControls/ExtendedVisualStateManger.cs create mode 100644 Baconit/Panels/FlipView/FlipViewPanel.xaml create mode 100644 Baconit/Panels/FlipView/FlipViewPanel.xaml.cs rename Baconit/Panels/{ => FlipView}/FlipViewPostCommentManager.cs (88%) create mode 100644 Baconit/Panels/FlipView/FlipViewPostContext.cs create mode 100644 Baconit/Panels/FlipView/FlipViewPostItem.cs rename Baconit/Panels/{FlipViewPanel.xaml => FlipView/FlipViewPostPanel.xaml} (72%) create mode 100644 Baconit/Panels/FlipView/FlipViewPostPanel.xaml.cs delete mode 100644 Baconit/Panels/FlipViewPanel.xaml.cs diff --git a/BaconBackend/Collectors/DeferredCollector.cs b/BaconBackend/Collectors/DeferredCollector.cs index 925f334..a9bd5e8 100644 --- a/BaconBackend/Collectors/DeferredCollector.cs +++ b/BaconBackend/Collectors/DeferredCollector.cs @@ -8,6 +8,15 @@ namespace BaconBackend.Collectors { + /// + /// Indicates the state of this collector + /// + public enum DeferredLoadState + { + Subset, + All + }; + /// /// Used to support deferred loading of items from a collector. We get all of the items at once, /// but only return a subset of them at a time. @@ -16,15 +25,6 @@ namespace BaconBackend.Collectors /// public class DeferredCollector { - /// - /// Indicates the state of this collector - /// - enum DeferredLoadState - { - Subset, - All - }; - /// /// Fired when the state of the collector is changing. /// @@ -83,6 +83,15 @@ public bool PreLoadItems(bool forceUpdate = false, int requestCount = 50) return m_collector.Update(forceUpdate, requestCount); } + /// + /// Gets the current state of the deferred collector. + /// + /// + public DeferredLoadState GetState() + { + return m_state; + } + /// /// Loads all of the items the collector has. If we already have the items loaded we will just send them, /// if they aren't loaded we will wait for the collector to load them. diff --git a/BaconBackend/DataObjects/Post.cs b/BaconBackend/DataObjects/Post.cs index 8532f8d..80902bc 100644 --- a/BaconBackend/DataObjects/Post.cs +++ b/BaconBackend/DataObjects/Post.cs @@ -308,25 +308,6 @@ private static SolidColorBrush GetDarkenedAccentBrush() [JsonIgnore] public string FlipViewSecondary { get; set; } - - /// - /// Used by flip view to indicate when the post is visible - /// - [JsonIgnore] - public bool IsPostVisible - { - get - { - return m_isPostVisible; - } - set - { - SetProperty(ref m_isPostVisible, value); - } - } - [JsonIgnore] - bool m_isPostVisible = false; - /// /// Used by subreddit view to show unread comment count /// @@ -617,60 +598,6 @@ public Visibility GildedVisibility #region FlipView Vars - /// - /// The number of pixels the UI needs to display the post's header. - /// - [JsonIgnore] - public int HeaderSize - { - get - { - return m_headerSize; - } - set - { - SetProperty(ref m_headerSize, value); - } - } - [JsonIgnore] - int m_headerSize = 500; - - /// - /// The list of comments on this post. - /// - [JsonIgnore] - public ObservableCollection Comments - { - get - { - return m_comments; - } - set - { - SetProperty(ref m_comments, value); - } - } - [JsonIgnore] - ObservableCollection m_comments = new ObservableCollection(); - - /// - /// The visibility of the scroll bar depending on if the any comments are loaded. - /// - [JsonIgnore] - public ScrollBarVisibility VerticalScrollBarVisibility - { - get - { - return m_verticalScrollBarVisibility; - } - set - { - SetProperty(ref m_verticalScrollBarVisibility, value); - } - } - [JsonIgnore] - ScrollBarVisibility m_verticalScrollBarVisibility = ScrollBarVisibility.Hidden; - /// /// The visibility of "Loading Comments", depending on if the comments have loaded yet. @@ -726,43 +653,6 @@ public Visibility FlipViewMenuButton [JsonIgnore] Visibility m_flipViewMenuButton = Visibility.Collapsed; - /// - /// The visibility of the post's header as sticky to the top of the flip view. - /// Note!!! The default should be visible so the FlipViewStickyHeaderMargin trick works! - /// - [JsonIgnore] - public Visibility FlipViewStickyHeaderVis - { - get - { - return m_flipViewStickyHeaderVis; - } - set - { - SetProperty(ref m_flipViewStickyHeaderVis, value); - } - } - [JsonIgnore] - Visibility m_flipViewStickyHeaderVis = Visibility.Visible; - - /// - /// Fun trick, this is used to make the flipview sticky header render off screen so it is ready when we want - /// to show it. We use -3000 to make sure it is way off screen. - /// - public Thickness FlipViewStickyHeaderMargin - { - get - { - return m_flipViewStickyHeaderMargin; - } - set - { - SetProperty(ref m_flipViewStickyHeaderMargin, value); - } - } - [JsonIgnore] - Thickness m_flipViewStickyHeaderMargin = new Thickness(0,-3000,0,0); - /// /// The visibility of the button to show all comments on a post. /// This should be visible when only some comments are visible. @@ -830,25 +720,6 @@ public Visibility FlipviewHeaderVisibility [JsonIgnore] Visibility m_flipviewHeaderVisibility = Visibility.Visible; - /// - /// The current angle of the header toggle button - /// - [JsonIgnore] - public int HeaderCollpaseToggleAngle - { - get - { - return m_headerCollpaseToggleAngle; - } - set - { - SetProperty(ref m_headerCollpaseToggleAngle, value); - } - } - [JsonIgnore] - int m_headerCollpaseToggleAngle = 180; - - /// /// Indicates how many comments we are showing /// diff --git a/BaconBackend/Managers/UiSettingManager.cs b/BaconBackend/Managers/UiSettingManager.cs index 19ead7f..837009c 100644 --- a/BaconBackend/Managers/UiSettingManager.cs +++ b/BaconBackend/Managers/UiSettingManager.cs @@ -511,6 +511,34 @@ public bool FlipView_ShowCommentScrollTip } private bool? m_flipView_ShowCommentScrollTip = null; + /// + /// If the user wants us to minimize the story header. + /// + public bool FlipView_MinimizeStoryHeader + { + get + { + if (!m_flipView_MinimizeStoryHeader.HasValue) + { + if (m_baconMan.SettingsMan.RoamingSettings.ContainsKey("UiSettingManager.FlipView_MinimizeStoryHeader")) + { + m_flipView_MinimizeStoryHeader = m_baconMan.SettingsMan.ReadFromRoamingSettings("UiSettingManager.FlipView_MinimizeStoryHeader"); + } + else + { + m_flipView_MinimizeStoryHeader = true; + } + } + return m_flipView_MinimizeStoryHeader.Value; + } + set + { + m_flipView_MinimizeStoryHeader = value; + m_baconMan.SettingsMan.WriteToRoamingSettings("UiSettingManager.FlipView_MinimizeStoryHeader", m_flipView_MinimizeStoryHeader.Value); + } + } + private bool? m_flipView_MinimizeStoryHeader = null; + #endregion #region Developer diff --git a/Baconit/Baconit.csproj b/Baconit/Baconit.csproj index 5c2b8d3..2436842 100644 --- a/Baconit/Baconit.csproj +++ b/Baconit/Baconit.csproj @@ -148,6 +148,7 @@ CommentsLoadingFooter.xaml + GlobalContentPresenter.xaml @@ -191,7 +192,12 @@ PanelManager.xaml - + + + + + FlipViewPostPanel.xaml + MessageInbox.xaml @@ -222,7 +228,7 @@ SubredditViewSettings.xaml - + FlipViewPanel.xaml @@ -418,10 +424,14 @@ MSBuild:Compile Designer - + MSBuild:Compile Designer + + Designer + MSBuild:Compile + MSBuild:Compile Designer diff --git a/Baconit/ContentPanels/Panels/WebPageContentPanel.xaml.cs b/Baconit/ContentPanels/Panels/WebPageContentPanel.xaml.cs index 7030414..95f1161 100644 --- a/Baconit/ContentPanels/Panels/WebPageContentPanel.xaml.cs +++ b/Baconit/ContentPanels/Panels/WebPageContentPanel.xaml.cs @@ -199,7 +199,7 @@ private void NavigationCompleted(WebView sender, WebViewNavigationCompletedEvent } private void NavigationFailed(object sender, WebViewNavigationFailedEventArgs e) { - m_base.FireOnError(true, "This web page having trouble loading"); + m_base.FireOnError(true, "This web page is broken"); } private void NavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args) diff --git a/Baconit/HelperControls/EndDetectingListView.cs b/Baconit/HelperControls/EndDetectingListView.cs index 009cee2..c290a4e 100644 --- a/Baconit/HelperControls/EndDetectingListView.cs +++ b/Baconit/HelperControls/EndDetectingListView.cs @@ -11,6 +11,16 @@ namespace Baconit.HelperControls { + /// + /// Indicates which way the list is scrolling. + /// + public enum ScrollDirection + { + WiggleRoomUp, // Used to indicate when we are going up but might not want to react. + Up, + Down + } + /// /// The args class for the OnListEndDetected event. /// @@ -18,11 +28,14 @@ public class OnListEndDetected : EventArgs { public double ListScrollPercent; public double ListScrollTotalDistance; + public ScrollDirection ScrollDirection; } public class EndDetectingListView : ListView { ScrollBar m_listeningScrollBar = null; + double m_lastValue = 0; + double m_lastDirectionChangeValue = 0; public EndDetectingListView() { @@ -66,6 +79,9 @@ public bool SuppressEndOfListEvent /// private void EndDetectingListView_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) { + // Stop listening to the event. We only want to do this once. + Loaded -= EndDetectingListView_Loaded; + // Get the scroll bars List scrollBars = new List(); UiControlHelpers.RecursivelyFindElement(this, ref scrollBars); @@ -100,9 +116,24 @@ private void ScrollBar_ValueChanged(object sender, RangeBaseValueChangedEventArg { // Calculate the percent and fire when it meets the threshold double scrollPercentage = m_listeningScrollBar.Value / m_listeningScrollBar.Maximum; - if (!m_supressEndOfListEvent && scrollPercentage > m_endOfListDetectionThreshold) + ScrollDirection direction = m_lastValue > m_listeningScrollBar.Value ? ScrollDirection.Up : ScrollDirection.Down; + m_lastValue = m_listeningScrollBar.Value; + + // We need to account for some play in the numbers when it comes to this, don't report a + // direction of up until we move up 50px from the last down position. This prevents us from + // jump back and forth when on a small point. + if(direction == ScrollDirection.Up && (m_lastDirectionChangeValue - m_lastValue) < 50) + { + direction = ScrollDirection.WiggleRoomUp; + } + else if(direction == ScrollDirection.Down) + { + m_lastDirectionChangeValue = m_listeningScrollBar.Value; + } + + if ((!m_supressEndOfListEvent && scrollPercentage > m_endOfListDetectionThreshold) || m_listeningScrollBar.Value == 0) { - m_onListEndDetectedEvent.Raise(this, new OnListEndDetected() { ListScrollPercent = scrollPercentage, ListScrollTotalDistance = m_listeningScrollBar.Value }); + m_onListEndDetectedEvent.Raise(this, new OnListEndDetected() { ListScrollPercent = scrollPercentage, ListScrollTotalDistance = m_listeningScrollBar.Value, ScrollDirection = direction }); } } } diff --git a/Baconit/HelperControls/ExtendedVisualStateManger.cs b/Baconit/HelperControls/ExtendedVisualStateManger.cs new file mode 100644 index 0000000..6e9846c --- /dev/null +++ b/Baconit/HelperControls/ExtendedVisualStateManger.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Baconit.HelperControls +{ + public class ExtendedVisualStateManager : VisualStateManager + { + protected override bool GoToStateCore(Control control, FrameworkElement stateGroupsRoot, string stateName, VisualStateGroup group, VisualState state, bool useTransitions) + { + if ((group == null) || (state == null)) + { + return false; + } + + if (control == null) + { + control = new ContentControl(); + } + + return base.GoToStateCore(control, stateGroupsRoot, stateName, group, state, useTransitions); + } + + public static bool GoToElementState(FrameworkElement root, string stateName, bool useTransitions) + { + ExtendedVisualStateManager customVisualStateManager = VisualStateManager.GetCustomVisualStateManager(root) as ExtendedVisualStateManager; + + return ((customVisualStateManager != null) && customVisualStateManager.GoToStateInternal(root, stateName, useTransitions)); + } + + private bool GoToStateInternal(FrameworkElement stateGroupsRoot, string stateName, bool useTransitions) + { + VisualStateGroup group; + VisualState state; + + return (TryGetState(stateGroupsRoot, stateName, out group, out state) && this.GoToStateCore(null, stateGroupsRoot, stateName, group, state, useTransitions)); + } + + private static bool TryGetState(FrameworkElement element, string stateName, out VisualStateGroup group, out VisualState state) + { + group = null; + state = null; + + foreach (VisualStateGroup group2 in VisualStateManager.GetVisualStateGroups(element)) + { + foreach (VisualState state2 in group2.States) + { + if (state2.Name == stateName) + { + group = group2; + state = state2; + return true; + } + } + } + + return false; + } + } +} diff --git a/Baconit/MainPage.xaml.cs b/Baconit/MainPage.xaml.cs index ae0b438..1116820 100644 --- a/Baconit/MainPage.xaml.cs +++ b/Baconit/MainPage.xaml.cs @@ -8,6 +8,7 @@ using Baconit.HelperControls; using Baconit.Interfaces; using Baconit.Panels; +using Baconit.Panels.FlipView; using System; using System.Collections.Generic; using System.Collections.ObjectModel; diff --git a/Baconit/Panels/FlipView/FlipViewPanel.xaml b/Baconit/Panels/FlipView/FlipViewPanel.xaml new file mode 100644 index 0000000..122036a --- /dev/null +++ b/Baconit/Panels/FlipView/FlipViewPanel.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Baconit/Panels/FlipView/FlipViewPanel.xaml.cs b/Baconit/Panels/FlipView/FlipViewPanel.xaml.cs new file mode 100644 index 0000000..f792e14 --- /dev/null +++ b/Baconit/Panels/FlipView/FlipViewPanel.xaml.cs @@ -0,0 +1,948 @@ +using BaconBackend.Collectors; +using BaconBackend.DataObjects; +using BaconBackend.Helpers; +using BaconBackend.Managers; +using Baconit.ContentPanels; +using Baconit.HelperControls; +using Baconit.Interfaces; +using Microsoft.ApplicationInsights.DataContracts; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading; +using System.Threading.Tasks; +using Windows.ApplicationModel.DataTransfer; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI; +using Windows.UI.Core; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Animation; +using Windows.UI.Xaml.Navigation; + +namespace Baconit.Panels.FlipView +{ + public sealed partial class FlipViewPanel : UserControl, IPanel + { + // + // Private Vars + // + + /// + /// The subreddit this flip view is representing. + /// + Subreddit m_subreddit; + + /// + /// The current sort for this flip view instance + /// + SortTypes m_currentSort; + + /// + /// The current sort time for this flip view instance + /// + SortTimeTypes m_currentSortTime; + + /// + /// The collector backing this flip view + /// + PostCollector m_collector; + + /// + /// A reference to the main panel host. + /// + IPanelHost m_host; + + /// + /// The list of posts that back the flip view + /// + ObservableCollection m_postsLists = new ObservableCollection(); + + /// + /// This list holds posts that we defer loaded if we have any. + /// + List m_deferredPostList = new List(); + + /// + /// Indicates that there is a target post we are trying to get to. + /// + string m_targetPost = ""; + + /// + /// Indicates that there is a target comment we are trying to get to. + /// + string m_targetComment = ""; + + /// + /// Holds a reference to the loading overlay if there is one. + /// + LoadingOverlay m_loadingOverlay = null; + + /// + /// Used to defer the first comments loading so we give the UI time to load before we start + /// the intense work of loading comments. + /// + bool m_isFirstPostLoad = true; + + /// + /// Holds a unique id for this flipview. + /// + string m_uniqueId = String.Empty; + + public FlipViewPanel() + { + this.InitializeComponent(); + + // Create a unique id for this + m_uniqueId = DateTime.Now.Ticks.ToString(); + } + + /// + /// Fired when the panel is being created. + /// + /// A reference to the host. + /// Arguments for the panel + public void PanelSetup(IPanelHost host, Dictionary arguments) + { + // Capture the host + m_host = host; + + // Check for the subreddit arg + if (!arguments.ContainsKey(PanelManager.NAV_ARGS_SUBREDDIT_NAME)) + { + throw new Exception("No subreddit was given!"); + } + string subredditName = (string)arguments[PanelManager.NAV_ARGS_SUBREDDIT_NAME]; + + // Kick off a background task to do the work + Task.Run(async () => + { + // Try to get the subreddit from the local cache. + Subreddit subreddit = App.BaconMan.SubredditMan.GetSubredditByDisplayName(subredditName); + + // It is very rare that we can't get it from the cache because something + // else usually request it from the web and then it will be cached. + if (subreddit == null) + { + // Since this can take some time, show the loading overlay + ShowFullScreenLoading(); + + // Try to get the subreddit from the web + subreddit = await App.BaconMan.SubredditMan.GetSubredditFromWebByDisplayName((string)arguments[PanelManager.NAV_ARGS_SUBREDDIT_NAME]); + } + + // Check again. + if (subreddit == null) + { + // Hmmmm. We can't load the subreddit. Show a message and go back + App.BaconMan.MessageMan.ShowMessageSimple("Hmmm, That's Not Right", "We can't load this subreddit right now, check your Internet connection."); + + // We need to wait some time until the transition animation is done or we can't go back. + // If we call GoBack while we are still navigating it will be ignored. + await Task.Delay(1000); + + // Now try to go back. + await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => + { + m_host.GoBack(); + }); + + // Get out of here. + return; + } + + // Capture the subreddit + m_subreddit = subreddit; + + // Get the current sort + m_currentSort = arguments.ContainsKey(PanelManager.NAV_ARGS_SUBREDDIT_SORT) ? (SortTypes)arguments[PanelManager.NAV_ARGS_SUBREDDIT_SORT] : SortTypes.Hot; + + // Get the current sort time + m_currentSortTime = arguments.ContainsKey(PanelManager.NAV_ARGS_SUBREDDIT_SORT_TIME) ? (SortTimeTypes)arguments[PanelManager.NAV_ARGS_SUBREDDIT_SORT_TIME] : SortTimeTypes.Week; + + // Try to get the target post id + if (arguments.ContainsKey(PanelManager.NAV_ARGS_POST_ID)) + { + m_targetPost = (string)arguments[PanelManager.NAV_ARGS_POST_ID]; + } + + // Try to get the force post, this will make us show only one post for the subreddit, + // which is the post given. + string forcePostId = null; + if (arguments.ContainsKey(PanelManager.NAV_ARGS_FORCE_POST_ID)) + { + forcePostId = (string)arguments[PanelManager.NAV_ARGS_FORCE_POST_ID]; + + // If the UI isn't already shown show the loading UI. Most of the time this post wont' be cached + // so it can take some time to load. + ShowFullScreenLoading(); + } + + // See if we are targeting a comment + if (arguments.ContainsKey(PanelManager.NAV_ARGS_FORCE_COMMENT_ID)) + { + m_targetComment = (string)arguments[PanelManager.NAV_ARGS_FORCE_COMMENT_ID]; + } + + // Get the collector and register for updates. + m_collector = PostCollector.GetCollector(m_subreddit, App.BaconMan, m_currentSort, m_currentSortTime, forcePostId); + m_collector.OnCollectionUpdated += Collector_OnCollectionUpdated; + + // Kick off an update of the subreddits if needed. + m_collector.Update(); + + // Set any posts that exist right now + UpdatePosts(0, m_collector.GetCurrentPosts()); + }); + } + + /// + /// Fired when the panel is being navigated to. + /// + public void OnNavigatingTo() + { + // Set the task bar color + m_host.SetStatusBar(Color.FromArgb(255, 25, 25, 25)); + + // If we have a current panel, set it visible. + if(ui_flipView.SelectedItem != null) + { + ((FlipViewPostItem)ui_flipView.SelectedItem).IsVisible = true; + ((FlipViewPostItem)ui_flipView.SelectedItem).LoadComments = true; + } + } + + /// + /// Fired when the panel is being navigated from. + /// + public async void OnNavigatingFrom() + { + // Deffer the action so we don't mess up any animations. + await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => + { + // Tell all of the posts they are not visible. + lock (m_postsLists) + { + foreach (FlipViewPostItem item in m_postsLists) + { + item.IsVisible = false; + } + } + }); + } + + /// + /// Fired when the panel should clear all memory. + /// + public async void OnCleanupPanel() + { + // If we have a collector unregister for updates. + if(m_collector != null) + { + m_collector.OnCollectionUpdated -= Collector_OnCollectionUpdated; + } + + // Kick to a background thread, remove all of our content. + await Task.Run(() => + { + // Remove all of our content + ContentPanelMaster.Current.RemoveAllAllowedContent(m_uniqueId); + }); + + // Deffer the action so we don't mess up any animations. + await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => + { + // Clear out all of the posts, and comments. + lock (m_postsLists) + { + foreach (FlipViewPostItem item in m_postsLists) + { + item.IsVisible = false; + item.LoadComments = false; + } + m_postsLists.Clear(); + } + }); + } + + /// + /// Fired when the panel should try to reduce memory if possible. This will only be called + /// while the panel isn't visible. + /// + public void OnReduceMemory() + { + // Kick to a background thread. + Task.Run(() => + { + // When we are asked to reduce memory unload all of our panels. + // If the user comes back they will be reloaded as they view them. + ContentPanelMaster.Current.UnloadContentForGroup(m_uniqueId); + }); + } + + /// + /// Fired when the panel is already in the stack, but a new navigate has been made to it. + /// Instead of creating a new panel, this same panel is used and given the navigation arguments. + /// + /// The argumetns passed when navigate was called + public async void OnPanelPulledToTop(Dictionary arguments) + { + // Do this logic here. + OnNavigatingTo(); + + if(!arguments.ContainsKey(PanelManager.NAV_ARGS_POST_ID)) + { + return; + } + + // Set the target post. + m_targetPost = (string)arguments[PanelManager.NAV_ARGS_POST_ID]; + + // Kick off to the UI thread + await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + // Lock the post list + lock(m_postsLists) + { + // Make sure the string is still valid + if (String.IsNullOrWhiteSpace(m_targetPost)) + { + return; + } + + // Set up the objects for the UI + for (int i = 0; i < m_postsLists.Count; i++) + { + // Check if this post is it + if (m_postsLists[i].Context.Post.Id == m_targetPost) + { + // It is important we set the target post to empty string first! + m_targetPost = string.Empty; + + // Found it! Only set the index it we aren't already there. + if (ui_flipView.SelectedIndex != i) + { + ui_flipView.SelectedIndex = i; + } + } + } + } + }); + } + + /// + /// Fired when the collection list has been updated. + /// + /// + /// + private void Collector_OnCollectionUpdated(object sender , OnCollectionUpdatedArgs args) + { + // Update the posts + UpdatePosts(args.StartingPosition, args.ChangedItems); + } + + /// + /// Update the posts in flip view. Staring at the index given and going until the list is empty. + /// + /// + /// + private async void UpdatePosts(int startingPos, List newPosts) + { + await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => + { + Visibility flipViewMenuVis = m_host.CurrentScreenMode() == ScreenMode.Single ? Visibility.Visible : Visibility.Collapsed; + + bool deferLoadPosts = false; + // Grab the list lock + lock (m_postsLists) + { + // If we are currently in a deferred scenario then we need to handle updates + // differently since the list won't match the list that is expected + if(m_deferredPostList.Count != 0) + { + if (m_postsLists.Count > 0 && newPosts.Count > 0 && + m_postsLists[0].Context.Post.Id.Equals(newPosts[0].Id)) + { + // The current post is updated, so update it. + // We can't replace the time because flip view will freak out + // so just update whatever UI we need to update. + m_postsLists[0].Context.Post.Likes = newPosts[0].Likes; + m_postsLists[0].Context.Post.SubTextLine1 = newPosts[0].SubTextLine1; + m_postsLists[0].Context.Post.SubTextLine2PartOne = newPosts[0].SubTextLine2PartOne; + m_postsLists[0].Context.Post.SubTextLine2PartTwo = newPosts[0].SubTextLine2PartTwo; + m_postsLists[0].Context.Post.Domain = newPosts[0].Domain; + m_postsLists[0].Context.Post.Score = newPosts[0].Score; + } + + // We have done all we want to do, leave now. + return; + } + + // If the list is currently empty we want to only load the first element and defer the rest of the + // elements. If the target post is -1 we load the first element, if not we load it only. + deferLoadPosts = m_postsLists.Count == 0; + string deferTargetPost = m_targetPost; + m_targetPost = null; + if (deferLoadPosts) + { + // If we are doing a defer make sure we have a target + if (String.IsNullOrWhiteSpace(deferTargetPost) && newPosts.Count > 0) + { + deferTargetPost = newPosts[0].Id; + } + } + + // Now setup the post update + int insertIndex = startingPos; + + // Set up the objects for the UI + foreach (Post post in newPosts) + { + // Check if we are adding or inserting. + bool isReplace = insertIndex < m_postsLists.Count; + + if (isReplace) + { + if (m_postsLists[insertIndex].Context.Post.Id.Equals(post.Id)) + { + // We can't replace the time because flip view will freak out + // so just update whatever UI we need to update. + m_postsLists[insertIndex].Context.Post.Likes = post.Likes; + m_postsLists[insertIndex].Context.Post.SubTextLine1 = post.SubTextLine1; + m_postsLists[insertIndex].Context.Post.SubTextLine2PartOne = post.SubTextLine2PartOne; + m_postsLists[insertIndex].Context.Post.SubTextLine2PartTwo = post.SubTextLine2PartTwo; + m_postsLists[insertIndex].Context.Post.Domain = post.Domain; + m_postsLists[insertIndex].Context.Post.Score = post.Score; + } + else + { + // Replace the entire post if it brand new + m_postsLists[insertIndex].Context.Post = post; + } + } + else + { + // If we are deferring posts only add the target + if (deferLoadPosts) + { + if (post.Id.Equals(deferTargetPost)) + { + // Try catch is a work around for bug https://github.com/QuinnDamerell/Baconit/issues/53 + try + { + m_postsLists.Add(new FlipViewPostItem(m_host, m_collector, post)); + } + catch (Exception e) + { + App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "mPostListAddFailedSpot1", e); + App.BaconMan.MessageMan.DebugDia("Adding to m_postList failed! " + (post == null ? "post was null!" : "post IS NOT NULL"), e); + } + } + + // Add it to the deferred list, also add the deferred post so we know + // where it is in the list. + m_deferredPostList.Add(post); + } + else + { + // Otherwise, just add it. + // Try catch is a work around for bug https://github.com/QuinnDamerell/Baconit/issues/53 + try + { + m_postsLists.Add(new FlipViewPostItem(m_host, m_collector, post)); + } + catch(Exception e) + { + App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "mPostListAddFailedSpot2", e); + App.BaconMan.MessageMan.DebugDia("Adding to m_postList failed! " + (post == null ? "post was null!" : "post IS NOT NULL"), e); + } + } + } + + // Set the menu button + post.FlipViewMenuButton = flipViewMenuVis; + + // Add one to the insert index + insertIndex++; + } + + // If the item source hasn't been set yet do it now. + if (ui_flipView.ItemsSource == null) + { + ui_flipView.ItemsSource = m_postsLists; + } + } + + // Hide the loading overlay if it is visible + HideFullScreenLoading(); + }); + } + + /// + /// If we have deferred post this will add them + /// + public async void DoDeferredPostUpdate() + { + // Check if we have work to do. + lock(m_postsLists) + { + if(m_deferredPostList.Count == 0) + { + return; + } + } + + // Otherwise, sleep for a little to let the UI settle down. + await Task.Delay(1000); + + // Now kick off a UI thread on low to do the update. + await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, async () => + { + lock(m_postsLists) + { + // Ensure we sill have work do to. + if (m_deferredPostList.Count == 0) + { + return; + } + + bool addBefore = true; + string deferredPostId = m_postsLists.Count > 0 ? m_postsLists[0].Context.Post.Id : String.Empty; + List insertList = new List(); + + foreach(Post post in m_deferredPostList) + { + // If this is the post don't do anything but indicate we should add after + if(post.Id.Equals(deferredPostId)) + { + addBefore = false; + } + else + { + // If we are adding before add it before + if(addBefore) + { + // #todo BUG! This is a fun work around. If we insert posts here about 5% of the time the app will + // crash due to a bug in the platform. Since the exception is in the system we don't get a chance to handle it + // we just die. + // So, add these to another list and add them after. + insertList.Add(post); + } + else + { + // If not add it to the end. + m_postsLists.Add(new FlipViewPostItem(m_host, m_collector, post)); + } + } + } + + // Now ad the inserts, but insert them from the element closest to the visible post to away. + // We want to do this so flipview doesn't keep changing which panels are virtualized as we add panels. + // as possible. + foreach (Post post in insertList.Reverse()) + { + m_postsLists.Insert(0, new FlipViewPostItem(m_host, m_collector, post)); + } + + // Clear the deferrals + m_deferredPostList.Clear(); + } + + // And preload the content for the next post, but again defer this since it is a background task. + await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => + { + UpdatePanelContent(); + }); + }); + } + + #region Flippping Logic + + /// + /// Fired when the flip panel selection changes. + /// + /// + /// + private async void FlipView_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if(ui_flipView.SelectedIndex == -1) + { + return; + } + + // Mark the item read. + m_collector.MarkPostRead(((FlipViewPostItem)ui_flipView.SelectedItem).Context.Post, ui_flipView.SelectedIndex); + + // Hide the comment box if open + HideCommentBoxIfOpen(); + + // If the index is 0 we are most likely doing a first load, so we want to + // set the panel content instantly so we get the loading UI as fast a possible. + if (ui_flipView.SelectedIndex == 0) + { + // Update the posts + UpdatePanelContent(); + } + else + { + // Kick off the panel content update to the UI thread with idle pri to give the UI time to setup. + await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => + { + // Update the posts + UpdatePanelContent(); + }); + } + + // Now if we have deferred post add them + DoDeferredPostUpdate(); + } + + /// + /// Updates the content in all of the panels in flipview. + /// + private async void UpdatePanelContent() + { + // Create a list we need to set to the UI. + List> setToUiList = new List>(); + List clearList = new List(); + bool extendCollection = false; + + // Get the min and max number of posts to load. + int minContentLoad = ui_flipView.SelectedIndex; + int maxContentLoad = ui_flipView.SelectedIndex; + if (App.BaconMan.UiSettingsMan.FlipView_PreloadFutureContent) + { + maxContentLoad++; + } + + // Lock the list + lock (m_postsLists) + { + for (int i = 0; i < m_postsLists.Count; i++) + { + FlipViewPostItem item = m_postsLists[i]; + if (i >= minContentLoad && i <= maxContentLoad) + { + // Add the post to the list of posts to set. We have to do this outside of the lock + // because we might delay while doing it. + setToUiList.Add(new Tuple(item, ui_flipView.SelectedIndex == i)); + } + else + { + // Add the post to the list of posts to clear. We have to do this outside of the lock + // because we might delay while doing it. + clearList.Add(item); + } + } + + // Check if we should load more posts. Note we want to check how many post the + // collector has because this gets called when the m_postList is being built, thus + // the count will be wrong. + if(m_postsLists.Count > 5 && m_collector.GetCurrentPosts().Count < maxContentLoad + 4) + { + extendCollection = true; + } + } + + // Extend if we should + if(extendCollection) + { + m_collector.ExtendCollection(25); + } + + // Now that we are out of lock set the items we want to set. + foreach(Tuple tuple in setToUiList) + { + // We found an item to show or prelaod, do it. + await SetPostContent(tuple.Item1, tuple.Item2); + + // If this is the first post to load delay for a while to give the UI time to setup. + if (m_isFirstPostLoad) + { + m_isFirstPostLoad = false; + await Task.Delay(1000); + } + + // After the delay tell the post to prefetch the comments if we are visible. + if (tuple.Item2) + { + tuple.Item1.LoadComments = true; + } + } + + // Now set them all to not be visible and clear + // the comments. + foreach (FlipViewPostItem item in clearList) + { + item.IsVisible = false; + item.LoadComments = false; + } + + // Kick off a background thread to clear out what we don't want. + await Task.Run(() => + { + // Get all of our content. + List allContent = ContentPanelMaster.Current.GetAllAllowedContentForGroup(m_uniqueId); + + // Find the ids that should be cleared and clear them. + List removeId = new List(); + foreach (string contentId in allContent) + { + bool found = false; + foreach (Tuple tuple in setToUiList) + { + if (tuple.Item1.Context.Post.Id.Equals(contentId)) + { + found = true; + break; + } + } + + if (!found) + { + // If we didn't find it clear it. + ContentPanelMaster.Current.RemoveAllowedContent(contentId); + } + } + }); + } + + #endregion + + #region Full Screen Loading + + /// + /// Shows a loading overlay if there isn't one already + /// + private async void ShowFullScreenLoading() + { + await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.High, () => + { + // Make sure we don't have one already, if so get out of here. + lock (this) + { + if (m_loadingOverlay != null) + { + return; + } + m_loadingOverlay = new LoadingOverlay(); + } + + m_loadingOverlay.OnHideComplete += LoadingOverlay_OnHideComplete; + Grid.SetRowSpan(m_loadingOverlay, 3); + ui_contentRoot.Children.Add(m_loadingOverlay); + m_loadingOverlay.Show(); + }); + } + + /// + /// Hides the exiting loading overlay + /// + private void HideFullScreenLoading() + { + LoadingOverlay overlay = null; + lock(this) + { + if (m_loadingOverlay == null) + { + return; + } + overlay = m_loadingOverlay; + } + + overlay.Hide(); + } + + /// + /// Fired when the overlay is hidden. + /// + /// + /// + private void LoadingOverlay_OnHideComplete(object sender, EventArgs e) + { + ui_contentRoot.Children.Remove(m_loadingOverlay); + lock(this) + { + m_loadingOverlay = null; + } + } + + #endregion + + #region Comment Box + + /// + /// Hides the box if it is open. + /// + private void HideCommentBoxIfOpen() + { + if (ui_commentBox != null) + { + ui_commentBox.HideBox(); + } + } + + /// + /// Fired when a panel wasn't us to open the comment box. + /// + /// + /// + private void FlipViewPostPanel_OnOpenCommentBox(object sender, OnOpenCommentBox e) + { + // Show the box with this data and the argument as the context. + ShowCommentBox(e.RedditId, e.EditText, e); + } + + /// + /// Shows the comment box + /// + private void ShowCommentBox(string redditId, string editText, object context) + { + // Important! Call find name so the deferred loaded element is created! + FindName("ui_commentBox"); + ui_commentBox.Visibility = Visibility.Visible; + ui_commentBox.ShowBox(redditId, editText, context); + SetCommentBoxHeight(this.ActualHeight); + } + + /// + /// Fired when the comment box is done opening. + /// + /// + /// + private void CommentBox_OnBoxOpened(object sender, CommentBoxOnOpenedArgs e) + { + OnOpenCommentBox openBoxContext = (OnOpenCommentBox)e.Context; + if(openBoxContext != null) + { + // Replace the context + e.Context = openBoxContext.Context; + openBoxContext.CommentBoxOpened(sender, e); + } + } + + /// + /// Fired when content has been submitted in the comment box. + /// + /// + /// + private void CommentBox_OnCommentSubmitted(object sender, OnCommentSubmittedArgs e) + { + OnOpenCommentBox openBoxContext = (OnOpenCommentBox)e.Context; + if (openBoxContext != null) + { + // Replace the context + e.Context = openBoxContext.Context; + bool wasActionSuccessful = openBoxContext.CommentBoxSubmitted(sender, e); + + // Hide the box if good + if (wasActionSuccessful) + { + ui_commentBox.HideBox(true); + } + else + { + ui_commentBox.HideLoadingOverlay(); + } + } + } + + /// + /// Sets the comment box's max height + /// + /// + private void SetCommentBoxHeight(double height) + { + // We have to set the max height because the grid row is set to auto. + // With auto the box will keep expanding as large as possible because + // it isn't bound by the grid. + if (ui_commentBox != null) + { + ui_commentBox.MaxHeight = height; + } + } + + /// + /// Fired when the control changes size, we need up update the height of our comment box. + /// + /// + /// + private void ContentRoot_SizeChanged(object sender, SizeChangedEventArgs e) + { + SetCommentBoxHeight(e.NewSize.Height - 1); + } + + #endregion + + /// + /// Sets the post content. For now all we give the flip view control is the URL + /// and it must figure out the rest on it's own. + /// + /// + private async Task SetPostContent(FlipViewPostItem item, bool isVisiblePost) + { + // Set that the post is visible if it is + item.IsVisible = isVisiblePost; + + // Only load the content if we are doing it with out action. (most of the time) + if (App.BaconMan.UiSettingsMan.FlipView_LoadPostContentWithoutAction) + { + await Task.Run(() => + { + ContentPanelMaster.Current.AddAllowedContent(ContentPanelSource.CreateFromPost(item.Context.Post), m_uniqueId, !isVisiblePost); + }); + } + } + + /// + /// Fired when the user has tapped the panel requesting the content to load. + /// + /// + /// + private async void FlipViewPostPanel_OnContentLoadRequest(object sender, OnContentLoadRequestArgs e) + { + // Find the post + Post post = null; + lock (m_postsLists) + { + foreach (FlipViewPostItem item in m_postsLists) + { + if (item.Context.Post.Id.Equals(e.SourceId)) + { + post = item.Context.Post; + break; + } + } + } + + // Send off a command to load it. + if (post != null) + { + await Task.Run(() => + { + ContentPanelMaster.Current.AddAllowedContent(ContentPanelSource.CreateFromPost(post), m_uniqueId, false); + }); + } + + ShowCommentBox("t3_tefafs", null, new object()); + } + + /// + /// Gets a unique id for this flipview. + /// + /// + private string GetUniqueId() + { + return m_subreddit.Id + m_currentSort + m_currentSortTime; + } + } +} diff --git a/Baconit/Panels/FlipViewPostCommentManager.cs b/Baconit/Panels/FlipView/FlipViewPostCommentManager.cs similarity index 88% rename from Baconit/Panels/FlipViewPostCommentManager.cs rename to Baconit/Panels/FlipView/FlipViewPostCommentManager.cs index 13d5cb9..ba6ae5a 100644 --- a/Baconit/Panels/FlipViewPostCommentManager.cs +++ b/Baconit/Panels/FlipView/FlipViewPostCommentManager.cs @@ -5,6 +5,7 @@ using Baconit.Interfaces; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -18,6 +19,23 @@ public class FlipViewPostCommentManager { DeferredCollector m_commentCollector; + /// + /// The current post for the comment manager + /// + Post m_post; + + /// + /// The current comments. + /// + public ObservableCollection Comments + { + get + { + return m_comments; + } + } + ObservableCollection m_comments = new ObservableCollection(); + /// /// Holds a full list of the comments, even collapsed comments /// @@ -38,14 +56,6 @@ public class FlipViewPostCommentManager /// bool m_showThreadSubset = false; - /// - /// The current post for the comment manager - /// - public Post Post - { - get { return m_post; } - } - Post m_post; public FlipViewPostCommentManager(ref Post post, string targetComment, bool showThreadSubset) { @@ -64,7 +74,7 @@ public FlipViewPostCommentManager(ref Post post, string targetComment, bool show } // This post might have comments if opened already in flip view. Clear them out if it does. - m_post.Comments.Clear(); + Comments.Clear(); if (!m_post.HaveCommentDefaultsBeenSet) { @@ -212,6 +222,15 @@ public void Refresh() }); } + /// + /// Returns if we are only showing a subset of the comments. + /// + /// + public bool IsOnlyShowingSubset() + { + return m_commentCollector.GetState() == DeferredLoadState.Subset; + } + /// /// Called when we need more posts because we are scrolling down. /// @@ -227,7 +246,7 @@ public async void RequestMorePosts() await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { // Only show the loading if the current number of comments is 0 - if(m_post.Comments.Count == 0) + if(Comments.Count == 0) { m_post.ShowCommentLoadingMessage = Visibility.Visible; } @@ -238,7 +257,7 @@ await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatch public void PrepareForDeletion() { // Clear out, we are going to be deleted - m_post.Comments.Clear(); + Comments.Clear(); if(m_fullCommentList != null) { @@ -263,7 +282,7 @@ private async void CommentCollector_OnCollectorStateChange(object sender, OnColl await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { // Check if we have any comments - if(e.NewPostCount == 0 && m_post.Comments.Count == 0) + if(e.NewPostCount == 0 && Comments.Count == 0) { m_post.ShowCommentLoadingMessage = Visibility.Visible; m_post.ShowCommentsErrorMessage = "No Comments"; @@ -296,7 +315,7 @@ await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatch int insertIndex = e.StartingPosition; // Lock the list - lock (m_post.Comments) + lock (Comments) { lock(m_fullCommentList) { @@ -310,29 +329,29 @@ await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatch for (int i = 0; i < e.ChangedItems.Count; i++) { Comment newComment = e.ChangedItems[i]; - Comment currentComment = i >= m_post.Comments.Count ? null : m_post.Comments[i]; + Comment currentComment = i >= Comments.Count ? null : Comments[i]; if (currentComment == null) { - m_post.Comments.Add(newComment); + Comments.Add(newComment); } else { if (newComment.Id.Equals(currentComment.Id)) { // Update the comment - m_post.Comments[i].Author = newComment.Author; - m_post.Comments[i].Score = newComment.Score; - m_post.Comments[i].TimeString = newComment.TimeString; - m_post.Comments[i].CollapsedCommentCount = newComment.CollapsedCommentCount; - m_post.Comments[i].Body = newComment.Body; - m_post.Comments[i].Likes = newComment.Likes; - m_post.Comments[i].ShowFullComment = true; + Comments[i].Author = newComment.Author; + Comments[i].Score = newComment.Score; + Comments[i].TimeString = newComment.TimeString; + Comments[i].CollapsedCommentCount = newComment.CollapsedCommentCount; + Comments[i].Body = newComment.Body; + Comments[i].Likes = newComment.Likes; + Comments[i].ShowFullComment = true; } else { // Replace it - m_post.Comments[i] = newComment; + Comments[i] = newComment; } } @@ -341,9 +360,9 @@ await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatch } // Trim off anything that shouldn't be here anymore - while (m_post.Comments.Count > e.ChangedItems.Count) + while (Comments.Count > e.ChangedItems.Count) { - m_post.Comments.RemoveAt(m_post.Comments.Count - 1); + Comments.RemoveAt(Comments.Count - 1); } } else @@ -398,7 +417,7 @@ await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatch m_fullCommentList.Add(comment); // If we are adding it to the end of the main list it is safe to add it to the end of the UI list. - m_post.Comments.Add(comment); + Comments.Add(comment); } insertIndex++; } @@ -409,20 +428,20 @@ await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatch // If the key is empty string we are inserting into the head if (String.IsNullOrWhiteSpace(insertPair.Key)) { - m_post.Comments.Insert(0, insertPair.Value); + Comments.Insert(0, insertPair.Value); } else { // Try to find the parent comment. - for (int i = 0; i < m_post.Comments.Count; i++) + for (int i = 0; i < Comments.Count; i++) { - Comment comment = m_post.Comments[i]; + Comment comment = Comments[i]; if (comment.Id.Equals(insertPair.Key)) { // We found the parent, it is not collapsed we should insert this comment after it. if (comment.ShowFullComment) { - m_post.Comments.Insert(i + 1, insertPair.Value); + Comments.Insert(i + 1, insertPair.Value); } // We are done, break out of this parent search. @@ -441,26 +460,26 @@ await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatch // Try to find the comment we will replace; Note if is very important that we start at the current replace point // because this comment might have been added before this count already due to a perviouse replace. In that case // we don't want to accidentally find that one instead of this one. - for (int i = replaceCount; i < m_post.Comments.Count; i++) + for (int i = replaceCount; i < Comments.Count; i++) { - Comment comment = m_post.Comments[i]; + Comment comment = Comments[i]; if (comment.Id.Equals(replacePair.Key)) { // If the id is the same we are updating. If we replace the comment the UI will freak out, // so just update the UI values if (comment.Id.Equals(replacePair.Value.Id)) { - m_post.Comments[i].Author = replacePair.Value.Author; - m_post.Comments[i].Score = replacePair.Value.Score; - m_post.Comments[i].TimeString = replacePair.Value.TimeString; - m_post.Comments[i].CollapsedCommentCount = replacePair.Value.CollapsedCommentCount; - m_post.Comments[i].Body = replacePair.Value.Body; - m_post.Comments[i].Likes = replacePair.Value.Likes; + Comments[i].Author = replacePair.Value.Author; + Comments[i].Score = replacePair.Value.Score; + Comments[i].TimeString = replacePair.Value.TimeString; + Comments[i].CollapsedCommentCount = replacePair.Value.CollapsedCommentCount; + Comments[i].Body = replacePair.Value.Body; + Comments[i].Likes = replacePair.Value.Likes; } else { // Replace the comment with this one - m_post.Comments[i] = replacePair.Value; + Comments[i] = replacePair.Value; } // We are done, break out of the search for the match. @@ -597,19 +616,19 @@ public void CollapseCommentsFromComment(Comment comment) int killedComment = 0; // Lock the list - lock (m_post.Comments) + lock (Comments) { // Go through all of the comments - for (int i = 0; i < m_post.Comments.Count; i++) + for (int i = 0; i < Comments.Count; i++) { // If found level is set we have already seen the comment if (foundLevel != -1) { // If this comment is higher than the root collapse kill it - if (m_post.Comments[i].CommentDepth > foundLevel) + if (Comments[i].CommentDepth > foundLevel) { // Remove it - m_post.Comments.RemoveAt(i); + Comments.RemoveAt(i); i--; // Update kill count @@ -627,7 +646,7 @@ public void CollapseCommentsFromComment(Comment comment) } // If we are here we haven't found the comment yet. - else if (m_post.Comments[i].Id.Equals(comment.Id)) + else if (Comments[i].Id.Equals(comment.Id)) { // We found it! Note the level foundLevel = comment.CommentDepth; @@ -643,20 +662,20 @@ public void CollapseCommentsFromComment(Comment comment) public void ExpandCommentsFromComment(Comment comment) { // Lock the list - lock (m_post.Comments) + lock (Comments) { lock (m_fullCommentList) { // First, we need to find where in the UI we need to add comments int inserationPoint = -1; int expandRootLevel = 0; - for (int i = 0; i < m_post.Comments.Count; i++) + for (int i = 0; i < Comments.Count; i++) { - if (m_post.Comments[i].Id.Equals(comment.Id)) + if (Comments[i].Id.Equals(comment.Id)) { // We found it! inserationPoint = i; - expandRootLevel = m_post.Comments[i].CommentDepth; + expandRootLevel = Comments[i].CommentDepth; break; } } @@ -688,7 +707,7 @@ public void ExpandCommentsFromComment(Comment comment) } // Insert this comment into the UI list - m_post.Comments.Insert(inserationPoint, m_fullCommentList[i]); + Comments.Insert(inserationPoint, m_fullCommentList[i]); inserationPoint++; } diff --git a/Baconit/Panels/FlipView/FlipViewPostContext.cs b/Baconit/Panels/FlipView/FlipViewPostContext.cs new file mode 100644 index 0000000..062012b --- /dev/null +++ b/Baconit/Panels/FlipView/FlipViewPostContext.cs @@ -0,0 +1,69 @@ +using BaconBackend.Collectors; +using BaconBackend.DataObjects; +using Baconit.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Baconit.Panels.FlipView +{ + /// + /// Used by flip view to pass context to post items. + /// + public class FlipViewPostContext : BindableBase + { + public FlipViewPostContext(IPanelHost host, PostCollector collector, Post post) + { + Post = post; + Collector = collector; + Host = host; + } + + public Post Post { get; set; } + + public PostCollector Collector { get; set; } + + public IPanelHost Host { get; set; } + + + #region UI Vars + + /// + /// The number of pixels the UI needs to display the post's header. + /// + public int HeaderSize + { + get + { + return m_headerSize; + } + set + { + SetProperty(ref m_headerSize, value); + } + } + int m_headerSize = 500; + + /// + /// Controls if the post menu icon is visible. + /// + public Visibility PostMenuIconVisibility + { + get + { + return m_postMenuIcon; + } + set + { + SetProperty(ref m_postMenuIcon, value); + } + } + Visibility m_postMenuIcon = Visibility.Collapsed; + + #endregion + } +} diff --git a/Baconit/Panels/FlipView/FlipViewPostItem.cs b/Baconit/Panels/FlipView/FlipViewPostItem.cs new file mode 100644 index 0000000..b15dde3 --- /dev/null +++ b/Baconit/Panels/FlipView/FlipViewPostItem.cs @@ -0,0 +1,72 @@ +using BaconBackend.Collectors; +using BaconBackend.DataObjects; +using Baconit.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Baconit.Panels.FlipView +{ + /// + /// This class is what is bound to the flip view for each item. + /// It holds the properties that represent a post. + /// + class FlipViewPostItem : BindableBase + { + public FlipViewPostItem(IPanelHost host, PostCollector collector, Post post) + { + Context = new FlipViewPostContext(host, collector, post); + IsVisible = false; + } + + /// + /// The context for the post. + /// + public FlipViewPostContext Context + { + get + { + return m_context; + } + set + { + SetProperty(ref m_context, value); + } + } + FlipViewPostContext m_context; + + /// + /// If the post is visible. + /// + public bool IsVisible + { + get + { + return m_isVisible; + } + set + { + SetProperty(ref m_isVisible, value); + } + } + bool m_isVisible = false; + + /// + /// If the post should load comments + /// + public bool LoadComments + { + get + { + return m_loadComments; + } + set + { + SetProperty(ref m_loadComments, value); + } + } + bool m_loadComments = false; + } +} diff --git a/Baconit/Panels/FlipViewPanel.xaml b/Baconit/Panels/FlipView/FlipViewPostPanel.xaml similarity index 72% rename from Baconit/Panels/FlipViewPanel.xaml rename to Baconit/Panels/FlipView/FlipViewPostPanel.xaml index 3550a0f..f71269b 100644 --- a/Baconit/Panels/FlipViewPanel.xaml +++ b/Baconit/Panels/FlipView/FlipViewPostPanel.xaml @@ -1,11 +1,11 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -22,7 +95,9 @@ - + @@ -32,9 +107,13 @@ + TextWrapping="Wrap"> + + + + + + + + - + - + - - + + - + @@ -60,6 +143,9 @@ + + + @@ -73,13 +159,13 @@ @@ -96,7 +182,7 @@ + Visibility="{x:Bind Context.PostMenuIconVisibility, Mode=OneWay}" /> - - + + + Visibility="{x:Bind Context.Post.ShowSaveImageMenu}" /> - + + Visibility="{x:Bind Context.Post.DeletePostVisibility}" /> + Visibility="{x:Bind Context.Post.EditPostVisibility}" /> @@ -144,8 +230,8 @@ Margin="0,0,0,2" VerticalAlignment="Bottom" Background="Transparent" - IsIndeterminate="{x:Bind FlipViewShowLoadingMoreComments, Mode=OneWay}" - Visibility="{x:Bind FlipViewShowLoadingMoreCommentsVis, Mode=OneWay}" /> + IsIndeterminate="{x:Bind Context.Post.FlipViewShowLoadingMoreComments, Mode=OneWay}" + Visibility="{x:Bind Context.Post.FlipViewShowLoadingMoreCommentsVis, Mode=OneWay}" /> + Text="{x:Bind Context.Post.NumComments, Mode=OneWay}" /> @@ -186,7 +272,7 @@ Text="Sort:" /> + Text="{x:Bind Context.Post.CommentCurrentSortTypeString, Mode=OneWay}" /> + Text="{x:Bind Context.Post.CurrentCommentShowingCount, Mode=OneWay}" /> - + @@ -271,7 +357,7 @@ Height="36" Background="{ThemeResource SystemControlBackgroundAccentBrush}" Tapped="ViewEntireThread_Tapped" - Visibility="{x:Bind FlipViewShowEntireThreadMessage, Mode=OneWay}"> + Visibility="{x:Bind Context.Post.FlipViewShowEntireThreadMessage, Mode=OneWay}"> @@ -279,25 +365,24 @@ - - + - + - + SourceId="{x:Bind Context.Post.Id}" /> - + + ShowErrorText="{x:Bind Context.Post.ShowCommentsErrorMessage, Mode=OneWay}" + ShowLoading="{x:Bind Context.Post.ShowCommentLoadingMessage, Mode=OneWay}" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - + diff --git a/Baconit/Panels/FlipView/FlipViewPostPanel.xaml.cs b/Baconit/Panels/FlipView/FlipViewPostPanel.xaml.cs new file mode 100644 index 0000000..15d722e --- /dev/null +++ b/Baconit/Panels/FlipView/FlipViewPostPanel.xaml.cs @@ -0,0 +1,1537 @@ +using BaconBackend.Collectors; +using BaconBackend.DataObjects; +using BaconBackend.Helpers; +using Baconit.ContentPanels; +using Baconit.HelperControls; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading.Tasks; +using Windows.ApplicationModel.DataTransfer; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Media.Animation; +using Windows.UI.Xaml.Navigation; + +// The User Control item template is documented at http://go.microsoft.com/fwlink/?LinkId=234236 + +namespace Baconit.Panels.FlipView +{ + /// + /// Arguments used to request the comment box opens. + /// + public class OnOpenCommentBox : EventArgs + { + public string RedditId; + public string EditText; + public object Context; + public Action CommentBoxOpened; + public Func CommentBoxSubmitted; + } + + public sealed partial class FlipViewPostPanel : UserControl + { + const int c_hiddenCommentHeaderHeight = 36; + const int c_hiddenShowAllCommentsHeight = 36; + + /// + /// Indicates if we have screen mode changed setup. + /// + bool m_isScrrenModeChangedSetup = false; + + /// + /// Holds the current comment manager. + /// + FlipViewPostCommentManager m_commentManager = null; + + /// + /// Indicates if we have a header size change that we haven't updated yet. + /// + bool m_hasDeferredHeaderSizeUpdate = false; + + /// + /// The last known scroll position. + /// + int m_lastKnownScrollOffset = 0; + + /// + /// Holds the protip pop up if one exists. + /// + TipPopUp m_commentTipPopUp = null; + + /// + /// Indicates if the header is showing or not. + /// + bool m_isFullscreen = false; + + /// + /// Indicates if the user has overwritten full screen. + /// + bool? m_fullScreenOverwrite = null; + + /// + /// Indicates if the fullnesses overwrite is from the user. + /// + bool? m_isfullScreenOverwriteUser = true; + + /// + /// A grid to hold on to the sticky header. + /// + Grid m_stickyHeader; + + /// + /// A grid to hold on to the normal header + /// + Grid m_storyHeader; + + /// + /// Fired when the user taps the content load request message. + /// + public event EventHandler OnContentLoadRequest + { + add { m_onContentLoadRequest.Add(value); } + remove { m_onContentLoadRequest.Remove(value); } + } + SmartWeakEvent> m_onContentLoadRequest = new SmartWeakEvent>(); + + /// + /// Fired when the host UI should show a message box. + /// + public event EventHandler OnOpenCommentBox + { + add { m_onOpenCommentBox.Add(value); } + remove { m_onOpenCommentBox.Remove(value); } + } + SmartWeakEvent> m_onOpenCommentBox = new SmartWeakEvent>(); + + + public FlipViewPostPanel() + { + this.InitializeComponent(); + } + + #region IsVisible Logic + + /// + /// This it how we get the isvisible from the xmal binding. + /// + public bool IsVisible + { + get { return (bool)GetValue(IsVisibleProperty); } + set { SetValue(IsVisibleProperty, value); } + } + + public static readonly DependencyProperty IsVisibleProperty = + DependencyProperty.Register( + "IsVisible", + typeof(bool), + typeof(FlipViewPostPanel), + new PropertyMetadata(false, new PropertyChangedCallback(OnIsVisibleChangedStatic))); + + private static void OnIsVisibleChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var instance = (FlipViewPostPanel)d; + if (instance != null) + { + instance.OnIsVisibleChanged((bool)e.NewValue); + } + } + + /// + /// Fired when the OnVisible property changes. + /// + /// + private void OnIsVisibleChanged(bool newVis) + { + } + + #endregion + + #region LoadComments Logic + + /// + /// This it how we get the LoadComments from the xmal binding. + /// + public bool LoadComments + { + get { return (bool)GetValue(LoadCommentsProperty); } + set { SetValue(LoadCommentsProperty, value); } + } + + public static readonly DependencyProperty LoadCommentsProperty = + DependencyProperty.Register( + "LoadComments", + typeof(bool), + typeof(FlipViewPostPanel), + new PropertyMetadata(false, new PropertyChangedCallback(OnLoadCommentsChangedStatic))); + + private static void OnLoadCommentsChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var instance = (FlipViewPostPanel)d; + if (instance != null) + { + instance.OnLoadCommentsChanged((bool)e.NewValue); + } + } + + /// + /// Fired when the LoadComments property changes. + /// + /// + private void OnLoadCommentsChanged(bool loadComments) + { + if (loadComments) + { + PreFetchPostComments(); + } + else + { + ClearCommentManger(); + } + } + + #endregion + + #region Context Logic + + /// + /// This it how we get the context from the xmal binding. + /// + public FlipViewPostContext PanelContext + { + get { return (FlipViewPostContext)GetValue(PanelContextProperty); } + set { SetValue(PanelContextProperty, value); } + } + + public static readonly DependencyProperty PanelContextProperty = + DependencyProperty.Register( + "PanelContext", + typeof(FlipViewPostContext), + typeof(FlipViewPostPanel), + new PropertyMetadata(null, new PropertyChangedCallback(OnContextChangedStatic))); + + private static void OnContextChangedStatic(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var instance = (FlipViewPostPanel)d; + if (instance != null) + { + instance.OnContextChanged((FlipViewPostContext)e.NewValue); + } + } + + /// + /// Fired when the Context property changes. + /// + /// + private void OnContextChanged(FlipViewPostContext newContext) + { + // Setup screen mode changing if not already. + if (!m_isScrrenModeChangedSetup) + { + m_isScrrenModeChangedSetup = true; + PanelContext.Host.OnScreenModeChanged += OnScreenModeChanged; + } + + // When our context changes make sure our comments are reset + ClearCommentManger(); + + // If we should be prefetching comments do so now. + if(LoadComments) + { + PreFetchPostComments(); + } + + // Setup the UI for the new context + SetupListViewForNewContext(); + SetupHeaderForNewContext(); + SetupScreenModeForNewContext(newContext.Host.CurrentScreenMode()); + ShowCommentScrollTipIfNeeded(); + SetupFullScreenForNewContext(); + } + + /// + /// Ensures that we have a good state for this post. + /// + /// + private FlipViewPostContext GetContext() + { + if (PanelContext != null) + { + // Make sure we are good + if (PanelContext.Collector != null && PanelContext.Host != null && PanelContext.Post != null) + { + return PanelContext; + } + } + return null; + } + + #endregion + + #region Post Click Actions + + /// + /// Fired when a up vote arrow is tapped + /// + /// + /// + private void UpVote_Tapped(object sender, EventArgs e) + { + FlipViewPostContext context = GetContext(); + if(context != null) + { + context.Collector.ChangePostVote(context.Post, PostVoteAction.UpVote); + } + } + + /// + /// Fired when a down vote arrow is tapped + /// + /// + /// + private void DownVote_Tapped(object sender, EventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + context.Collector.ChangePostVote(context.Post, PostVoteAction.DownVote); + } + } + + private async void OpenBrowser_Tapped(object sender, EventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + string url = context.Post.Url; + if (String.IsNullOrWhiteSpace(url)) + { + url = context.Post.Permalink; + } + if (!String.IsNullOrWhiteSpace(url)) + { + await Windows.System.Launcher.LaunchUriAsync(new Uri(url, UriKind.Absolute)); + App.BaconMan.TelemetryMan.ReportEvent(this, "OpenInBrowser"); + } + } + } + + private void More_Tapped(object sender, EventArgs e) + { + // Show the more menu + FrameworkElement element = sender as FrameworkElement; + if (element != null) + { + FlyoutBase.ShowAttachedFlyout(element); + } + App.BaconMan.TelemetryMan.ReportEvent(this, "MoreTapped"); + } + + private void SavePost_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + context.Collector.SaveOrHidePost(context.Post, !context.Post.IsSaved, null); + App.BaconMan.TelemetryMan.ReportEvent(this, "SavePostTapped"); + } + } + + private void HidePost_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + context.Collector.SaveOrHidePost(context.Post, null, !context.Post.IsHidden); + App.BaconMan.TelemetryMan.ReportEvent(this, "HidePostTapped"); + } + } + + private void CopyLink_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + DataPackage data = new DataPackage(); + if (String.IsNullOrWhiteSpace(context.Post.Url)) + { + data.SetText("http://www.reddit.com" + context.Post.Permalink); + } + else + { + data.SetText(context.Post.Url); + } + Clipboard.SetContent(data); + App.BaconMan.TelemetryMan.ReportEvent(this, "CopyLinkTapped"); + } + } + + private void CopyPermalink_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + DataPackage data = new DataPackage(); + data.SetText("http://www.reddit.com" + context.Post.Permalink); + Clipboard.SetContent(data); + App.BaconMan.TelemetryMan.ReportEvent(this, "CopyLinkTapped"); + } + } + + private void SaveImage_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + App.BaconMan.ImageMan.SaveImageLocally(context.Post.Url); + App.BaconMan.TelemetryMan.ReportEvent(this, "CopyLinkTapped"); + } + } + + // I threw up a little while I wrote this. + Post m_sharePost = null; + private void SharePost_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + if (!String.IsNullOrWhiteSpace(context.Post.Url)) + { + m_sharePost = context.Post; + // Setup the share contract so we can share data + DataTransferManager dataTransferManager = DataTransferManager.GetForCurrentView(); + dataTransferManager.DataRequested += DataTransferManager_DataRequested; + DataTransferManager.ShowShareUI(); + App.BaconMan.TelemetryMan.ReportEvent(this, "SharePostTapped"); + } + } + } + + private void DataTransferManager_DataRequested(DataTransferManager sender, DataRequestedEventArgs args) + { + if (m_sharePost != null) + { + args.Request.Data.Properties.ApplicationName = "Baconit"; + args.Request.Data.Properties.ContentSourceWebLink = new Uri(m_sharePost.Url, UriKind.Absolute); + args.Request.Data.Properties.Title = "A Reddit Post Shared From Baconit"; + args.Request.Data.Properties.Description = m_sharePost.Title; + args.Request.Data.SetText($"\r\n\r\n{m_sharePost.Title}\r\n\r\n{m_sharePost.Url}"); + m_sharePost = null; + App.BaconMan.TelemetryMan.ReportEvent(this, "PostShared"); + } + else + { + args.Request.FailWithDisplayText("Baconit doesn't have anything to share!"); + App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "FailedToShareFilpViewPostNoSharePost"); + } + } + + /// + /// Called when we should open the global menu + /// + /// + /// + private void MenuPost_Tapped(object sender, EventArgs e) + { + // Show the global menu + FlipViewPostContext context = GetContext(); + if (context != null) + { + context.Host.ToggleMenu(true); + } + } + + /// + /// Fired when the user taps delete post. + /// + /// + /// + private async void DeletePost_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + // Confirm + bool? doIt = await App.BaconMan.MessageMan.ShowYesNoMessage("Delete Post", "Are you sure you want to?"); + + if (doIt.HasValue && doIt.Value) + { + // Delete it. + context.Collector.DeletePost(context.Post); + } + } + } + + /// + /// Fired when the user taps edit post. + /// + /// + /// + private void EditPost_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + ShowCommentBox("t3_" + context.Post.Id, context.Post.Selftext, context.Post); + } + } + + /// + /// Called when the user wants to comment on the post + /// + /// + /// + private void PostCommentOn_OnIconTapped(object sender, EventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + ShowCommentBox("t3_" + context.Post.Id, null, context.Post); + } + } + + /// + /// Fired when the user taps the go to subreddit button + /// + /// + /// + private void GoToSubreddit_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + // Navigate to the subreddit. + Dictionary args = new Dictionary(); + args.Add(PanelManager.NAV_ARGS_SUBREDDIT_NAME, context.Post.Subreddit); + context.Host.Navigate(typeof(SubredditPanel), context.Post.Subreddit + SortTypes.Hot + SortTimeTypes.Week, args); + App.BaconMan.TelemetryMan.ReportEvent(this, "GoToSubredditFlipView"); + } + } + + /// + /// Fired when the user taps the go to user button + /// + /// + /// + private void GoToUser_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context != null) + { + // Navigate to the user. + Dictionary args = new Dictionary(); + args.Add(PanelManager.NAV_ARGS_USER_NAME, context.Post.Author); + context.Host.Navigate(typeof(UserProfile), context.Post.Author, args); + App.BaconMan.TelemetryMan.ReportEvent(this, "GoToUserFlipView"); + } + } + + #endregion + + #region List Scrolling Logic + + private void SetupListViewForNewContext() + { + // Hide the scroll bar. + ScrollViewer.SetVerticalScrollBarVisibility(ui_listView, ScrollBarVisibility.Hidden); + m_lastKnownScrollOffset = 0; + } + + /// + /// Creates a comment manager for the post and prefetches the comments + /// + /// + private void PreFetchPostComments(bool forcePreFetch = false, bool showThreadSubset = true) + { + // Make sure we aren't already ready. + if(m_commentManager != null) + { + return; + } + + FlipViewPostContext context = GetContext(); + if(context == null) + { + return; + } + + // Create a comment manager for the post + Post refPost = context.Post; + m_commentManager = new FlipViewPostCommentManager(ref refPost, "", false);//m_targetComment, showThreadSubset); + + // Set the comment list to the list view + ui_listView.ItemsSource = m_commentManager.Comments; + + // If the user wanted, kick off pre load of comments. + if (forcePreFetch || App.BaconMan.UiSettingsMan.FlipView_PreloadComments) + { + m_commentManager.PreFetchComments(); + } + } + + /// + /// Will remove any comment managers that may exist for a post, and clears the comments. + /// + /// + private void ClearCommentManger() + { + if(m_commentManager != null) + { + m_commentManager.PrepareForDeletion(); + m_commentManager = null; + } + } + + /// + /// Returns the comment manger if we have one. + /// + /// + private FlipViewPostCommentManager GetCommentManger() + { + return m_commentManager; + } + + /// + /// Fired when a user tap the header to view an entire thread instead of just the + /// context of a message + /// + /// + /// + private void ViewEntireThread_Tapped(object sender, TappedRoutedEventArgs e) + { + // Get the post and the manager + Post post = ((Post)((FrameworkElement)sender).DataContext); + + // First remove the current comment manager + ClearCommentManger(); + + // Now make a new one without using the subset + PreFetchPostComments(true, false); + + // Update the header sizes to fix the UI + SetHeaderSize(); + } + + /// + /// Fired when one of the lists are scrolling to the bottom. + /// + /// + /// + private void List_OnListEndDetectedEvent(object sender, OnListEndDetected e) + { + // NOTE!!! + // This is a very hot code path, so anything done here should be really quick! + + FlipViewPostContext context = GetContext(); + if(context == null) + { + return; + } + + // Make sure the margin is normal. + if (ui_stickyHeader.Margin.Top != 0) + { + // If the margin is not 0 we are playing our render tick. If the sticky header + // is always set to collapsed it actually never loads or render until we set it to + // visible while we are scrolling which makes it 'pop' in with a delay. To avoid this + // it defaults visible but way off the screen, this makes it load and render so it is ready + // when we set it visible. Here is is safe to restore it to the normal state. + ui_stickyHeader.Visibility = Visibility.Collapsed; + ui_stickyHeader.Margin = new Thickness(0, 0, 0, 0); + } + + // Show or hide the scroll bar depending if we have gotten to comments yet or not. + ScrollViewer.SetVerticalScrollBarVisibility(ui_listView, e.ListScrollTotalDistance > 60 ? ScrollBarVisibility.Auto : ScrollBarVisibility.Hidden); + + // Find the header size for this post + int currentScrollAera = GetCurrentScrollArea(); + + // This will return -1 if we can't get this yet, so just get out of here. + if (currentScrollAera == -1) + { + return; + } + + // Use the header size and the control size to figure out if we should show the static header. + bool showHeader = e.ListScrollTotalDistance > currentScrollAera - ui_stickyHeader.ActualHeight; + ui_stickyHeader.Visibility = showHeader ? Visibility.Visible : Visibility.Collapsed; + + // Get the distance for the animation header to move. We need to account for the header + // size here because when we move from full screen to not it will toggle. + int headerAniamtionDistance = currentScrollAera; + if(m_isFullscreen) + { + Grid headerGrid = (Grid)m_storyHeader.FindName("ui_storyHeaderBlock"); + headerAniamtionDistance -= (int)headerGrid.ActualHeight; + } + + // If we are far enough to also hide the header consider hiding it. + if (e.ListScrollTotalDistance > headerAniamtionDistance) + { + if (App.BaconMan.UiSettingsMan.FlipView_MinimizeStoryHeader) + { + if (e.ScrollDirection == ScrollDirection.Down) + { + ToggleFullscreen(true); + } + else if(e.ScrollDirection == ScrollDirection.Up) + { + ToggleFullscreen(false); + } + } + } + else + { + // If we are the top force it. + ToggleFullscreen(false, e.ListScrollTotalDistance < 10); + } + + + // If we have a differed header update and we are near the top (not showing the header) + // do the update now. For a full story see the comment on m_hasDeferredHeaderSizeUpdate + if (m_hasDeferredHeaderSizeUpdate && !showHeader) + { + SetHeaderSize(); + } + + //// Update the last known scroll pos + m_lastKnownScrollOffset = (int)e.ListScrollTotalDistance; + + //// Hide the tip box if needed. + HideCommentScrollTipIfNeeded(); + + // If we have a manager request more posts. + if (m_commentManager != null) + { + if (m_commentManager.IsOnlyShowingSubset()) + { + m_commentManager.RequestMorePosts(); + } + } + } + + bool headerVis = true; + + /// + /// Fired when a item is selected, just clear it. + /// + /// + /// + private void EndDetectingListView_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + EndDetectingListView listview = sender as EndDetectingListView; + listview.SelectedIndex = -1; + } + + /// + /// Scrolls to the top of the list view for a post. + /// + /// + private void ScrollCommentsToTop() + { + ui_listView.ScrollIntoView(null); + } + + #endregion + + #region Comment Click Listeners + + /// + /// Fired when the refresh button is pressed. + /// + /// + /// + private void CommentRefresh_Click(object sender, RoutedEventArgs e) + { + FlipViewPostCommentManager manager = GetCommentManger(); + if (manager != null) + { + m_commentManager.Refresh(); + } + } + + private void CommentUp_Tapped(object sender, TappedRoutedEventArgs e) + { + // We don't need to animate here, the vote will do it for us + Comment comment = (sender as FrameworkElement).DataContext as Comment; + FlipViewPostCommentManager manager = GetCommentManger(); + if (manager != null) + { + manager.UpVote_Tapped(comment); + } + } + + private void CommentDown_Tapped(object sender, TappedRoutedEventArgs e) + { + // We don't need to animate here, the vote will do it for us + Comment comment = (sender as FrameworkElement).DataContext as Comment; + FlipViewPostCommentManager manager = GetCommentManger(); + if (manager != null) + { + manager.DownVote_Tapped(comment); + } + } + + private void CommentButton3_Tapped(object sender, TappedRoutedEventArgs e) + { + // Animate the text + AnimateText((FrameworkElement)sender); + + // Get the comment + Comment comment = (sender as FrameworkElement).DataContext as Comment; + + if (comment.IsDeleted) + { + App.BaconMan.MessageMan.ShowMessageSimple("LET IT GO!", "You can't edit a deleted comment!"); + return; + } + + if (comment.IsCommentOwnedByUser) + { + // Edit + ShowCommentBox("t1_" + comment.Id, comment.Body, comment); + } + else + { + // Reply + ShowCommentBox("t1_" + comment.Id, null, comment); + } + } + + private async void CommentButton4_Tapped(object sender, TappedRoutedEventArgs e) + { + // Animate the text + AnimateText((FrameworkElement)sender); + + // Get the comment + Comment comment = (sender as FrameworkElement).DataContext as Comment; + + if (comment.IsCommentOwnedByUser) + { + // Delete the comment + bool? response = await App.BaconMan.MessageMan.ShowYesNoMessage("Delete Comment", "Are you sure?"); + + if (response.HasValue && response.Value) + { + // Find the manager + FlipViewPostCommentManager manager = GetCommentManger(); + if (manager != null) + { + manager.CommentDeleteRequest(comment); + } + } + } + else + { + FlipViewPostContext context = GetContext(); + if(context == null) + { + return; + } + + // Navigate to the user + Dictionary args = new Dictionary(); + args.Add(PanelManager.NAV_ARGS_USER_NAME, comment.Author); + context.Host.Navigate(typeof(UserProfile), comment.Author, args); + App.BaconMan.TelemetryMan.ReportEvent(this, "GoToUserFromComment"); + } + } + + private void CommentMore_Tapped(object sender, TappedRoutedEventArgs e) + { + // Animate the text + AnimateText((FrameworkElement)sender); + + // Show the more menu + FrameworkElement element = sender as FrameworkElement; + if (element != null) + { + FlyoutBase.ShowAttachedFlyout(element); + } + + App.BaconMan.TelemetryMan.ReportEvent(this, "CommentMoreTapped"); + } + + private void CommentSave_Click(object sender, RoutedEventArgs e) + { + Comment comment = (sender as FrameworkElement).DataContext as Comment; + FlipViewPostCommentManager manager = GetCommentManger(); + if (manager != null) + { + manager.Save_Tapped(comment); + } + App.BaconMan.TelemetryMan.ReportEvent(this, "CommentSaveTapped"); + } + + private void CommentShare_Click(object sender, RoutedEventArgs e) + { + Comment comment = (sender as FrameworkElement).DataContext as Comment; + FlipViewPostCommentManager manager = GetCommentManger(); + if (manager != null) + { + manager.Share_Tapped(comment); + } + App.BaconMan.TelemetryMan.ReportEvent(this, "CommentShareTapped"); + } + + private void CommentPermalink_Click(object sender, RoutedEventArgs e) + { + Comment comment = (sender as FrameworkElement).DataContext as Comment; + FlipViewPostCommentManager manager = GetCommentManger(); + if (manager != null) + { + manager.CopyPermalink_Tapped(comment); + } + App.BaconMan.TelemetryMan.ReportEvent(this, "CommentPermalinkTapped"); + } + + private void CommentCollapse_Tapped(object sender, TappedRoutedEventArgs e) + { + // Animate the text + AnimateText((FrameworkElement)sender); + + Comment comment = (sender as FrameworkElement).DataContext as Comment; + FlipViewPostCommentManager manager = GetCommentManger(); + if (manager != null) + { + manager.Collpase_Tapped(comment); + } + } + + private void CollapsedComment_Tapped(object sender, TappedRoutedEventArgs e) + { + Comment comment = (sender as FrameworkElement).DataContext as Comment; + FlipViewPostCommentManager manager = GetCommentManger(); + if (manager != null) + { + manager.Expand_Tapped(comment); + } + } + + /// + /// Does the text animation on the text box in the Grid. + /// Note!! We do this crazy animation stuff so the UI feel responsive. + /// It would be ideal to use the SimpleButtonText control but it is too expensive for the virtual list. + /// + /// + private void AnimateText(FrameworkElement textBlockContainer) + { + // Make sure it has children + if (VisualTreeHelper.GetChildrenCount(textBlockContainer) != 1) + { + return; + } + + // Try to get the text block + TextBlock textBlock = (TextBlock)VisualTreeHelper.GetChild(textBlockContainer, 0); + + // Return if failed. + if (textBlock == null) + { + return; + } + + // Make a storyboard + Storyboard storyboard = new Storyboard(); + ColorAnimation colorAnimation = new ColorAnimation(); + storyboard.Children.Add(colorAnimation); + + // Set them up. + Storyboard.SetTarget(storyboard, textBlock); + Storyboard.SetTargetProperty(storyboard, "(TextBlock.Foreground).(SolidColorBrush.Color)"); + storyboard.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 400)); + colorAnimation.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 400)); + storyboard.BeginTime = new TimeSpan(0, 0, 0, 0, 100); + + // Set the colors + colorAnimation.To = Color.FromArgb(255, 153, 153, 153); + colorAnimation.From = ((SolidColorBrush)Application.Current.Resources["SystemControlBackgroundAccentBrush"]).Color; + + // Set the text to be the accent color + textBlock.Foreground = ((SolidColorBrush)Application.Current.Resources["SystemControlBackgroundAccentBrush"]); + + // And animate it back out. + storyboard.Begin(); + } + + /// + /// Fired when a user taps a link in a comment + /// + /// + /// + private void MarkdownTextBlock_OnMarkdownLinkTapped(object sender, UniversalMarkdown.OnMarkdownLinkTappedArgs e) + { + App.BaconMan.ShowGlobalContent(e.Link); + } + + #endregion + + #region Full Screen Logic + + /// + /// Gets full screen ready for a new context + /// + private void SetupFullScreenForNewContext() + { + m_fullScreenOverwrite = null; + m_isfullScreenOverwriteUser = null; + ToggleFullscreen(false, true); + } + + /// + /// Fired when the control wants to go full screen. + /// + /// + /// + private void ContentPanelHost_OnToggleFullscreen(object sender, OnToggleFullScreenEventArgs e) + { + // Set the overwrite + if(e.GoFullScreen) + { + // Scroll the header into view + ScrollCommentsToTop(); + + // Set the overwrite + m_fullScreenOverwrite = true; + m_isfullScreenOverwriteUser = false; + } + else + { + // Disable the overwrite + m_isfullScreenOverwriteUser = null; + m_fullScreenOverwrite = null; + } + + // Toggle full screen. + ToggleFullscreen(e.GoFullScreen); + } + + /// + /// Fired when the post header toggle is clicked + /// + /// + /// + private void PostHeaderToggle_Click(object sender, RoutedEventArgs e) + { + // Set the user overwrite, only overwrite the hide. + if(!m_isFullscreen) + { + m_fullScreenOverwrite = true; + m_isfullScreenOverwriteUser = true; + } + else + { + m_fullScreenOverwrite = null; + m_isfullScreenOverwriteUser = null; + } + + ToggleFullscreen(!m_isFullscreen); + } + + /// + /// Given a post toggles the header + /// + /// + private void ToggleFullscreen(bool goFullscreen, bool force = false) + { + // If we are already there don't do anything. + if(m_isFullscreen == goFullscreen) + { + return; + } + + // Make sure the user hasn't overwritten this. + if(m_fullScreenOverwrite.HasValue) + { + // If we are being force and the overwrite isn't from the user skip this. + if(!force || (m_isfullScreenOverwriteUser.HasValue && !m_isfullScreenOverwriteUser.Value)) + { + if (m_fullScreenOverwrite.Value != goFullscreen) + { + return; + } + } + } + m_isFullscreen = goFullscreen; + + // Get our elements for the sticky header + Storyboard storyboard = (Storyboard)m_stickyHeader.FindName("ui_storyCollapseHeader"); + DoubleAnimation animBody = (DoubleAnimation)m_stickyHeader.FindName("ui_animHeaderBodyTranslate"); + DoubleAnimation animTitle = (DoubleAnimation)m_stickyHeader.FindName("ui_animHeaderTitleTranslate"); + DoubleAnimation animSubtext = (DoubleAnimation)m_stickyHeader.FindName("ui_animHeaderSubtextTranslate"); + DoubleAnimation animIcons = (DoubleAnimation)m_stickyHeader.FindName("ui_animHeaderIconsTranslate"); + DoubleAnimation animFullscreenButton = (DoubleAnimation)m_stickyHeader.FindName("ui_animHeaderFullscreenButtonRotate"); + Grid stickyGrid = (Grid)m_stickyHeader.FindName("ui_storyHeaderBlock"); + + // Stop any past animations. + if (storyboard.GetCurrentState() != ClockState.Stopped) + { + storyboard.Stop(); + } + + // Setup the animations. + double animTo = goFullscreen ? -stickyGrid.ActualHeight : 0; + double animFrom = goFullscreen ? 0 : -stickyGrid.ActualHeight; + animBody.To = animTo; + animBody.From = animFrom; + animTitle.To = animTo; + animTitle.From = animFrom; + animSubtext.To = animTo; + animSubtext.From = animFrom; + animIcons.To = animTo; + animIcons.From = animFrom; + animFullscreenButton.To = goFullscreen ? 0 : 180; + animFullscreenButton.From = goFullscreen ? 180 : 0; + + // For the normal header + Storyboard storyNormal = (Storyboard)m_storyHeader.FindName("ui_storyCollapseHeaderHeight"); + Grid headerGrid = (Grid)m_storyHeader.FindName("ui_storyHeaderBlock"); + DoubleAnimation animNormal = (DoubleAnimation)m_storyHeader.FindName("ui_animHeaderHeightCollapse"); + DoubleAnimation animNormalFullscreenButton = (DoubleAnimation)m_storyHeader.FindName("ui_animHeaderHeightButtonRotate"); + + // Stop any past animations. + if (storyNormal.GetCurrentState() != ClockState.Stopped) + { + storyNormal.Stop(); + } + + // Set the normal animations. + animNormal.To = goFullscreen ? 0 : headerGrid.ActualHeight; + animNormal.From = goFullscreen ? headerGrid.ActualHeight : 0; + animNormalFullscreenButton.To = goFullscreen ? 0 : 180; + animNormalFullscreenButton.From = goFullscreen ? 180 : 0; + + // Play the animations. + storyboard.Begin(); + storyNormal.Begin(); + } + + /// + /// Fired when a header loads. + /// + /// + /// + private void StoryHeader_Loaded(object sender, RoutedEventArgs e) + { + // The normal header will always load first. + if(m_storyHeader == null) + { + m_storyHeader = (Grid)sender; + } + else + { + m_stickyHeader = (Grid)sender; + } + } + + #endregion + + #region Comment Sort + + /// + /// Fired when either comment sort is tapped + /// + /// + /// + private void OpenMenuFlyout_Tapped(object sender, TappedRoutedEventArgs e) + { + FrameworkElement element = sender as FrameworkElement; + if (element != null) + { + FlyoutBase.ShowAttachedFlyout(element); + } + App.BaconMan.TelemetryMan.ReportEvent(this, "CommentSortTapped"); + } + + /// + /// Fired when a user taps a new sort type for comments. + /// + /// + /// + private void CommentSortMenu_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if(context == null) + { + return; + } + + // Update sort type + MenuFlyoutItem item = sender as MenuFlyoutItem; + context.Post.CommentSortType = GetCommentSortFromString(item.Text); + + // Get the collector and update the sort + FlipViewPostCommentManager commentManager = GetCommentManger(); + commentManager.ChangeCommentSort(); + } + + private CommentSortTypes GetCommentSortFromString(string typeString) + { + typeString = typeString.ToLower(); + switch (typeString) + { + case "best": + default: + return CommentSortTypes.Best; + case "controversial": + return CommentSortTypes.Controversial; + case "new": + return CommentSortTypes.New; + case "old": + return CommentSortTypes.Old; + case "q&a": + return CommentSortTypes.QA; + case "top": + return CommentSortTypes.Top; + } + } + + private void CommentShowingCountMenu_Click(object sender, RoutedEventArgs e) + { + FlipViewPostContext context = GetContext(); + if (context == null) + { + return; + } + + // Parse the new comment count + MenuFlyoutItem item = sender as MenuFlyoutItem; + context.Post.CurrentCommentShowingCount = int.Parse(item.Text); + + // Get the collector and update the sort + FlipViewPostCommentManager commentManager = GetCommentManger(); + commentManager.UpdateShowingCommentCount(context.Post.CurrentCommentShowingCount); + } + + #endregion + + #region Screen Mode + + /// + /// Fired when the screen mode changes. + /// + /// + /// + private void OnScreenModeChanged(object sender, OnScreenModeChangedArgs e) + { + SetupScreenModeForNewContext(e.NewScreenMode); + } + + /// + /// Sets the menu icon correctly. + /// + /// + private void SetupScreenModeForNewContext(ScreenMode newMode) + { + FlipViewPostContext context = GetContext(); + if(context == null) + { + return; + } + context.PostMenuIconVisibility = newMode == ScreenMode.Single ? Visibility.Visible : Visibility.Collapsed; + } + + #endregion + + #region Header Logic + + /// + /// Sets up the header for the new context + /// + public void SetupHeaderForNewContext() + { + // If we have not done our trick yet, don't collapse it. + if (ui_stickyHeader.Margin.Top != 0) + { + ui_stickyHeader.Visibility = Visibility.Visible; + } + else + { + ui_stickyHeader.Visibility = Visibility.Collapsed; + } + + // Set the header size for this. + SetHeaderSize(); + } + + /// + /// Updates the header sizes for the flip post so they fit perfectly on the screen. + /// + private void SetHeaderSize() + { + // Get the screen size, account for the comment box if it is open. + int currentScrollAera = GetCurrentScrollArea(); + + // This will return -1 if we can't get this number yet. + if (currentScrollAera == -1) + { + return; + } + + FlipViewPostContext context = GetContext(); + context.HeaderSize = currentScrollAera; + } + + /// + /// Returns the current space that's available for the flipview scroller + /// + /// + private int GetCurrentScrollArea() + { + // Get the control size + int screenSize = (int)ui_contentRoot.ActualHeight; + + // Make sure we are ready. + if (ui_contentRoot.ActualHeight == 0) + { + // If not return -1. + return -1; + } + + FlipViewPostContext context = GetContext(); + if(context == null) + { + return -1; + } + + // If we are showing the show all comments header add the height of that so it won't be on the screen by default + if (context.Post.FlipViewShowEntireThreadMessage == Visibility.Visible) + { + screenSize += c_hiddenShowAllCommentsHeight; + } + + // If we are not hiding the large header also add the height of the comment bar so it will be off screen by default + // or if we are full screen from the flip control. + if (context.Post.FlipviewHeaderVisibility == Visibility.Visible) + { + screenSize += c_hiddenCommentHeaderHeight; + } + + // If we are full screen account for the header being gone. + if(m_isFullscreen) + { + Grid headerGrid = (Grid)m_storyHeader.FindName("ui_storyHeaderBlock"); + screenSize += (int)headerGrid.ActualHeight; + } + + return screenSize; + } + + /// + /// Fired when the content root changes sizes + /// + /// + /// + private void ContentRoot_SizeChanged(object sender, SizeChangedEventArgs e) + { + // We do this so we jump the UI ever time the comment box is open. + // see the description on m_hasDeferredHeaderSizeUpdate for a full story. + // This isn't 100% correct, but we are looking for comment box changes. + // So if the width doesn't change assume it is the comment box. + if (e.PreviousSize.Width == e.NewSize.Width && m_lastKnownScrollOffset > 30) + { + m_hasDeferredHeaderSizeUpdate = true; + } + else + { + // Fire a header size change to fix them up. + SetHeaderSize(); + } + } + + #endregion + + #region Comment Box + + /// + /// Shows the comment box + /// + private void ShowCommentBox(string redditId, string editText, object context) + { + OnOpenCommentBox args = new OnOpenCommentBox() + { + Context = context, + RedditId = redditId, + EditText = editText, + CommentBoxOpened = CommentBox_OnBoxOpened, + CommentBoxSubmitted = CommentBox_OnCommentSubmitted + }; + m_onOpenCommentBox.Raise(this, args); + } + + /// + /// Fired when the comment box is done opening. + /// + /// + /// + public void CommentBox_OnBoxOpened(object sender, CommentBoxOnOpenedArgs e) + { + // We want to scroll the comment we are working off of into view. + if (e.RedditId.StartsWith("t1_")) + { + Comment comment = (Comment)e.Context; + ui_listView.ScrollIntoView(comment); + } + } + + /// + /// Fired when the comment in the comment box was submitted + /// + /// + /// + private bool CommentBox_OnCommentSubmitted(object sender, OnCommentSubmittedArgs e) + { + bool wasActionSuccessful = false; + + FlipViewPostContext context = GetContext(); + if(context == null) + { + return wasActionSuccessful; + } + + if (e.RedditId.StartsWith("t3_")) + { + Post post = (Post)e.Context; + + if (post != null) + { + if (e.IsEdit) + { + // We edited selftext + wasActionSuccessful = context.Collector.EditSelfPost(post, e.Response); + + if (wasActionSuccessful) + { + // If we are successful to update the UI we will remove the post + // and reallow it. + ContentPanelMaster.Current.RemoveAllowedContent(post.Id); + + // Now ask the host to reallow it. + m_onContentLoadRequest.Raise(this, new OnContentLoadRequestArgs() { SourceId = post.Id }); + } + } + else + { + // We added a new comment + FlipViewPostCommentManager manager = GetCommentManger(); + if (manager != null) + { + wasActionSuccessful = manager.CommentAddedOrEdited("t3_" + post.Id, e); + } + else + { + App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "CommentSubmitManagerObjNull"); + } + } + } + else + { + App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "CommentSubmitPostObjNull"); + } + } + else if (e.RedditId.StartsWith("t1_")) + { + Comment comment = (Comment)e.Context; + if (comment != null) + { + // Comment added or edited. + FlipViewPostCommentManager manager = GetCommentManger(); + if (manager != null) + { + wasActionSuccessful = manager.CommentAddedOrEdited("t1_" + comment.Id, e); + } + else + { + App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "CommentSubmitManagerObjNull"); + } + } + else + { + App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "CommentSubmitCommentObjNull"); + } + } + + return wasActionSuccessful; + } + + #endregion + + #region Pro Tip Logic + + /// + /// Shows the comment scroll tip if needed + /// + public void ShowCommentScrollTipIfNeeded() + { + if (!App.BaconMan.UiSettingsMan.FlipView_ShowCommentScrollTip) + { + return; + } + + // Never show it again. + App.BaconMan.UiSettingsMan.FlipView_ShowCommentScrollTip = false; + + // Create the tip UI, add it to the UI and show it. + m_commentTipPopUp = new TipPopUp(); + m_commentTipPopUp.Margin = new Thickness(0, 0, 0, 90); + m_commentTipPopUp.VerticalAlignment = VerticalAlignment.Bottom; + m_commentTipPopUp.TipHideComplete += CommentTipPopUp_TipHideComplete; + ui_contentRoot.Children.Add(m_commentTipPopUp); + m_commentTipPopUp.ShowTip(); + } + + /// + /// Hides the comment scroll tip if needed. + /// + public void HideCommentScrollTipIfNeeded() + { + // If we don't have one return. + if (m_commentTipPopUp == null) + { + return; + } + + // Tell it to hide and set it to null + m_commentTipPopUp.HideTip(); + m_commentTipPopUp = null; + } + + /// + /// Fired when the hide is complete + /// + /// + /// + private void CommentTipPopUp_TipHideComplete(object sender, EventArgs e) + { + TipPopUp popUp = (TipPopUp)sender; + + // Unregister the event + popUp.TipHideComplete += CommentTipPopUp_TipHideComplete; + + // Remove the tip from the UI + ui_contentRoot.Children.Remove(popUp); + } + + #endregion + + /// + /// Fired when the user has tapped the panel requesting the content to load. + /// + /// + /// + private void ContentPanelHost_OnContentLoadRequest(object sender, OnContentLoadRequestArgs e) + { + // Forward the call along. + m_onContentLoadRequest.Raise(this, e); + } + } +} diff --git a/Baconit/Panels/FlipViewPanel.xaml.cs b/Baconit/Panels/FlipViewPanel.xaml.cs deleted file mode 100644 index 9d66dc1..0000000 --- a/Baconit/Panels/FlipViewPanel.xaml.cs +++ /dev/null @@ -1,2100 +0,0 @@ -using BaconBackend.Collectors; -using BaconBackend.DataObjects; -using BaconBackend.Helpers; -using BaconBackend.Managers; -using Baconit.ContentPanels; -using Baconit.HelperControls; -using Baconit.Interfaces; -using Microsoft.ApplicationInsights.DataContracts; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using System.Threading; -using System.Threading.Tasks; -using Windows.ApplicationModel.DataTransfer; -using Windows.Foundation; -using Windows.Foundation.Collections; -using Windows.UI; -using Windows.UI.Core; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Data; -using Windows.UI.Xaml.Input; -using Windows.UI.Xaml.Media; -using Windows.UI.Xaml.Media.Animation; -using Windows.UI.Xaml.Navigation; - -namespace Baconit.Panels -{ - public sealed partial class FlipViewPanel : UserControl, IPanel - { - const int c_hiddenCommentHeaderHeight = 36; - const int c_hiddenShowAllCommentsHeight = 36; - - // - // Private Vars - // - - /// - /// The subreddit this flip view is representing. - /// - Subreddit m_subreddit; - - /// - /// The current sort for this flip view instance - /// - SortTypes m_currentSort; - - /// - /// The current sort time for this flip view instance - /// - SortTimeTypes m_currentSortTime; - - /// - /// The collector backing this flip view - /// - PostCollector m_collector; - - /// - /// A reference to the main panel host. - /// - IPanelHost m_host; - - /// - /// The list of posts that back the flip view - /// - ObservableCollection m_postsLists = new ObservableCollection(); - - /// - /// This list holds posts that we defer loaded if we have any. - /// - List m_deferredPostList = new List(); - - /// - /// Holds the protip pop up if one exists. - /// - TipPopUp m_commentTipPopUp = null; - - /// - /// Indicates that there is a target post we are trying to get to. - /// - string m_targetPost = ""; - - /// - /// Indicates that there is a target comment we are trying to get to. - /// - string m_targetComment = ""; - - /// - /// Holds the current flip view comment manager - /// - List m_commentManagers = new List(); - - /// - /// Holds the current story headers, we need to to figure out how large they are. - /// - List m_flipViewStoryHeaders = new List(); - - /// - /// Used by the comment box to differ the header update until the scrolling gets close to the top. - /// Without this every time the comment box opens or closes the UI jumps because the header gets - /// moved. - /// - bool m_hasDeferredHeaderSizeUpdate = false; - - /// - /// Keeps track of the last known scroll position for the UI on the screen. - /// - int m_lastKnownScrollOffset = 0; - - /// - /// Holds a reference to the loading overlay if there is one. - /// - LoadingOverlay m_loadingOverlay = null; - - /// - /// Used to defer the first comments loading so we give the UI time to load before we start - /// the intense work of loading comments. - /// - bool m_isFirstPostLoad = true; - - /// - /// Indicates if we are full screen from the flipveiw control or not. - /// - bool m_isFullScreen = false; - - /// - /// Holds a unique id for this flipview. - /// - string m_uniqueId = String.Empty; - - /// - /// Holds a reference to the current lists. - /// - List> m_currentListViews = new List>(); - - - public FlipViewPanel() - { - this.InitializeComponent(); - - // Create a unique id for this - m_uniqueId = DateTime.Now.Ticks.ToString(); - - // Setup a listener for size changes - this.SizeChanged += FlipViewPanel_SizeChanged; - } - - - /// - /// Fired when the panel is being created. - /// - /// A reference to the host. - /// Arguments for the panel - public void PanelSetup(IPanelHost host, Dictionary arguments) - { - // Capture the host - m_host = host; - - // Check for the subreddit arg - if (!arguments.ContainsKey(PanelManager.NAV_ARGS_SUBREDDIT_NAME)) - { - throw new Exception("No subreddit was given!"); - } - string subredditName = (string)arguments[PanelManager.NAV_ARGS_SUBREDDIT_NAME]; - - // Kick off a background task to do the work - Task.Run(async () => - { - // Try to get the subreddit from the local cache. - Subreddit subreddit = App.BaconMan.SubredditMan.GetSubredditByDisplayName(subredditName); - - // It is very rare that we can't get it from the cache because something - // else usually request it from the web and then it will be cached. - if (subreddit == null) - { - // Since this can take some time, show the loading overlay - ShowFullScreenLoading(); - - // Try to get the subreddit from the web - subreddit = await App.BaconMan.SubredditMan.GetSubredditFromWebByDisplayName((string)arguments[PanelManager.NAV_ARGS_SUBREDDIT_NAME]); - } - - // Check again. - if (subreddit == null) - { - // Hmmmm. We can't load the subreddit. Show a message and go back - App.BaconMan.MessageMan.ShowMessageSimple("Hmmm, That's Not Right", "We can't load this subreddit right now, check your Internet connection."); - - // We need to wait some time until the transition animation is done or we can't go back. - // If we call GoBack while we are still navigating it will be ignored. - await Task.Delay(1000); - - // Now try to go back. - await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => - { - m_host.GoBack(); - }); - - // Get out of here. - return; - } - - // Capture the subreddit - m_subreddit = subreddit; - - // Get the current sort - m_currentSort = arguments.ContainsKey(PanelManager.NAV_ARGS_SUBREDDIT_SORT) ? (SortTypes)arguments[PanelManager.NAV_ARGS_SUBREDDIT_SORT] : SortTypes.Hot; - - // Get the current sort time - m_currentSortTime = arguments.ContainsKey(PanelManager.NAV_ARGS_SUBREDDIT_SORT_TIME) ? (SortTimeTypes)arguments[PanelManager.NAV_ARGS_SUBREDDIT_SORT_TIME] : SortTimeTypes.Week; - - // Try to get the target post id - if (arguments.ContainsKey(PanelManager.NAV_ARGS_POST_ID)) - { - m_targetPost = (string)arguments[PanelManager.NAV_ARGS_POST_ID]; - } - - // Try to get the force post, this will make us show only one post for the subreddit, - // which is the post given. - string forcePostId = null; - if (arguments.ContainsKey(PanelManager.NAV_ARGS_FORCE_POST_ID)) - { - forcePostId = (string)arguments[PanelManager.NAV_ARGS_FORCE_POST_ID]; - - // If the UI isn't already shown show the loading UI. Most of the time this post wont' be cached - // so it can take some time to load. - ShowFullScreenLoading(); - } - - // See if we are targeting a comment - if (arguments.ContainsKey(PanelManager.NAV_ARGS_FORCE_COMMENT_ID)) - { - m_targetComment = (string)arguments[PanelManager.NAV_ARGS_FORCE_COMMENT_ID]; - } - - // Get the collector and register for updates. - m_collector = PostCollector.GetCollector(m_subreddit, App.BaconMan, m_currentSort, m_currentSortTime, forcePostId); - m_collector.OnCollectionUpdated += Collector_OnCollectionUpdated; - - // Kick off an update of the subreddits if needed. - m_collector.Update(); - - // Set any posts that exist right now - UpdatePosts(0, m_collector.GetCurrentPosts()); - - // Set the command bar - m_host.OnScreenModeChanged += OnScreenModeChanged; - }); - } - - public void OnNavigatingTo() - { - // Set the task bar color - m_host.SetStatusBar(Color.FromArgb(255, 25, 25, 25)); - - // If we have a current panel, set it visible. - if(ui_flipView.SelectedItem != null) - { - ((Post)ui_flipView.SelectedItem).IsPostVisible = true; - } - } - - /// - /// Fired when the panel is being navigated from. - /// - public async void OnNavigatingFrom() - { - // Deffer the action so we don't mess up any animations. - await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => - { - // Tell all of the posts they are not visible. - lock (m_postsLists) - { - foreach (Post post in m_postsLists) - { - post.IsPostVisible = false; - } - } - }); - } - - public async void OnCleanupPanel() - { - // If we have a collector unregister for updates. - if(m_collector != null) - { - m_collector.OnCollectionUpdated -= Collector_OnCollectionUpdated; - } - - // Kick to a background thread, remove all of our content. - await Task.Run(() => - { - // Remove all of our content - ContentPanelMaster.Current.RemoveAllAllowedContent(m_uniqueId); - }); - - // Deffer the action so we don't mess up any animations. - await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => - { - // Clear out all of the posts, and comments. - lock (m_postsLists) - { - foreach (Post post in m_postsLists) - { - Post refPost = post; - post.IsPostVisible = false; - ClearPostComments(ref refPost); - } - m_postsLists.Clear(); - } - }); - } - - /// - /// Fired when the panel should try to reduce memory if possible. This will only be called - /// while the panel isn't visible. - /// - public void OnReduceMemory() - { - // Kick to a background thread. - Task.Run(() => - { - // When we are asked to reduce memory unload all of our panels. - // If the user comes back they will be reloaded as they view them. - ContentPanelMaster.Current.UnloadContentForGroup(m_uniqueId); - }); - } - - /// - /// Fired when the panel is already in the stack, but a new navigate has been made to it. - /// Instead of creating a new panel, this same panel is used and given the navigation arguments. - /// - /// The argumetns passed when navigate was called - public async void OnPanelPulledToTop(Dictionary arguments) - { - // Do this logic here. - OnNavigatingTo(); - - if(!arguments.ContainsKey(PanelManager.NAV_ARGS_POST_ID)) - { - return; - } - - // Set the target post. - m_targetPost = (string)arguments[PanelManager.NAV_ARGS_POST_ID]; - - // Kick off to the UI thread - await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => - { - // Lock the post list - lock(m_postsLists) - { - // Make sure the string is still valid - if (String.IsNullOrWhiteSpace(m_targetPost)) - { - return; - } - - // Set up the objects for the UI - for (int i = 0; i < m_postsLists.Count; i++) - { - // Check if this post is it - if (m_postsLists[i].Id == m_targetPost) - { - // It is important we set the target post to empty string first! - m_targetPost = string.Empty; - - // Found it! Only set the index it we aren't already there. - if (ui_flipView.SelectedIndex != i) - { - ui_flipView.SelectedIndex = i; - } - } - } - } - }); - } - - /// - /// Fired when the screen mode changes. - /// - /// - /// - private void OnScreenModeChanged(object sender, OnScreenModeChangedArgs e) - { - lock (m_postsLists) - { - Visibility flipViewMenuVis = m_host.CurrentScreenMode() == ScreenMode.Single ? Visibility.Visible : Visibility.Collapsed; - foreach (Post post in m_postsLists) - { - post.FlipViewMenuButton = flipViewMenuVis; - } - } - } - - /// - /// Fired when the collection list has been updated. - /// - /// - /// - private void Collector_OnCollectionUpdated(object sender , OnCollectionUpdatedArgs args) - { - // Update the posts - UpdatePosts(args.StartingPosition, args.ChangedItems); - } - - /// - /// Update the posts in flip view. Staring at the index given and going until the list is empty. - /// - /// - /// - private async void UpdatePosts(int startingPos, List newPosts) - { - await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => - { - Visibility flipViewMenuVis = m_host.CurrentScreenMode() == ScreenMode.Single ? Visibility.Visible : Visibility.Collapsed; - - bool deferLoadPosts = false; - // Grab the list lock - lock (m_postsLists) - { - // If we are currently in a deferred scenario then we need to handle updates - // differently since the list won't match the list that is expected - if(m_deferredPostList.Count != 0) - { - if (m_postsLists.Count > 0 && newPosts.Count > 0 && - m_postsLists[0].Id.Equals(newPosts[0].Id)) - { - // The current post is updated, so update it. - // We can't replace the time because flip view will freak out - // so just update whatever UI we need to update. - m_postsLists[0].Likes = newPosts[0].Likes; - m_postsLists[0].SubTextLine1 = newPosts[0].SubTextLine1; - m_postsLists[0].SubTextLine2PartOne = newPosts[0].SubTextLine2PartOne; - m_postsLists[0].SubTextLine2PartTwo = newPosts[0].SubTextLine2PartTwo; - m_postsLists[0].Domain = newPosts[0].Domain; - m_postsLists[0].Score = newPosts[0].Score; - } - - // We have done all we want to do, leave now. - return; - } - - // If the list is currently empty we want to only load the first element and defer the rest of the - // elements. If the target post is -1 we load the first element, if not we load it only. - deferLoadPosts = m_postsLists.Count == 0; - string deferTargetPost = m_targetPost; - m_targetPost = null; - if (deferLoadPosts) - { - // If we are doing a defer make sure we have a target - if (String.IsNullOrWhiteSpace(deferTargetPost) && newPosts.Count > 0) - { - deferTargetPost = newPosts[0].Id; - } - } - - // Now setup the post update - int insertIndex = startingPos; - - // Set up the objects for the UI - foreach (Post post in newPosts) - { - // Check if we are adding or inserting. - bool isReplace = insertIndex < m_postsLists.Count; - - if (isReplace) - { - if (m_postsLists[insertIndex].Id.Equals(post.Id)) - { - // We can't replace the time because flip view will freak out - // so just update whatever UI we need to update. - m_postsLists[insertIndex].Likes = post.Likes; - m_postsLists[insertIndex].SubTextLine1 = post.SubTextLine1; - m_postsLists[insertIndex].SubTextLine2PartOne = post.SubTextLine2PartOne; - m_postsLists[insertIndex].SubTextLine2PartTwo = post.SubTextLine2PartTwo; - m_postsLists[insertIndex].Domain = post.Domain; - m_postsLists[insertIndex].Score = post.Score; - } - else - { - // Replace the entire post if it brand new - m_postsLists[insertIndex] = post; - } - } - else - { - // If we are deferring posts only add the target - if (deferLoadPosts) - { - if (post.Id.Equals(deferTargetPost)) - { - // Try catch is a work around for bug https://github.com/QuinnDamerell/Baconit/issues/53 - try - { - m_postsLists.Add(post); - } - catch (Exception e) - { - App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "mPostListAddFailedSpot1", e); - App.BaconMan.MessageMan.DebugDia("Adding to m_postList failed! " + (post == null ? "post was null!" : "post IS NOT NULL"), e); - } - } - - // Add it to the deferred list, also add the deferred post so we know - // where it is in the list. - m_deferredPostList.Add(post); - } - else - { - // Otherwise, just add it. - // Try catch is a work around for bug https://github.com/QuinnDamerell/Baconit/issues/53 - try - { - m_postsLists.Add(post); - } - catch(Exception e) - { - App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "mPostListAddFailedSpot2", e); - App.BaconMan.MessageMan.DebugDia("Adding to m_postList failed! " + (post == null ? "post was null!" : "post IS NOT NULL"), e); - } - } - } - - // Set the menu button - post.FlipViewMenuButton = flipViewMenuVis; - - // Add one to the insert index - insertIndex++; - } - - // If the item source hasn't been set yet do it now. - if (ui_flipView.ItemsSource == null) - { - ui_flipView.ItemsSource = m_postsLists; - } - - // This is a good place to show the comment tip if we have to since this - // indicates this is a first load. - if (deferLoadPosts) - { - ShowCommentScrollTipIfNeeded(); - } - } - - SetHeaderSizes(); - - // Hide the loading overlay if it is visible - HideFullScreenLoading(); - }); - } - - /// - /// If we have deferred post this will add them - /// - public async void DoDeferredPostUpdate() - { - // Check if we have work to do. - lock(m_postsLists) - { - if(m_deferredPostList.Count == 0) - { - return; - } - } - - // Otherwise, sleep for a little to let the UI settle down. - await Task.Delay(1000); - - // Now kick off a UI thread on low to do the update. - await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, async () => - { - lock(m_postsLists) - { - // Ensure we sill have work do to. - if (m_deferredPostList.Count == 0) - { - return; - } - - bool addBefore = true; - string deferredPostId = m_postsLists.Count > 0 ? m_postsLists[0].Id : String.Empty; - List insertList = new List(); - - foreach(Post post in m_deferredPostList) - { - // If this is the post don't do anything but indicate we should add after - if(post.Id.Equals(deferredPostId)) - { - addBefore = false; - } - else - { - // If we are adding before add it before - if(addBefore) - { - // #todo BUG! This is a fun work around. If we insert posts here about 5% of the time the app will - // crash due to a bug in the platform. Since the exception is in the system we don't get a chance to handle it - // we just die. - // So, add these to another list and add them after. - insertList.Add(post); - } - else - { - // If not add it to the end. - m_postsLists.Add(post); - } - } - } - - // Now ad the inserts, but insert them from the element closest to the visible post to away. - // We want to do this so flipview doesn't keep changing which panels are virtualized as we add panels. - // as possible. - foreach (Post post in insertList.Reverse()) - { - m_postsLists.Insert(0, post); - } - - // Clear the deferrals - m_deferredPostList.Clear(); - } - - // Add update the header sizes again - SetHeaderSizes(); - - // And preload the content for the next post, but again defer this since it is a background task. - await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => - { - UpdatePanelContent(); - }); - }); - } - - #region Post Click Actions - - private void MenuButton_Click(object sender, RoutedEventArgs e) - { - m_host.ToggleMenu(true); - } - - /// - /// Fired when a up vote arrow is tapped - /// - /// - /// - private void UpVote_Tapped(object sender, EventArgs e) - { - Post post = (sender as FrameworkElement).DataContext as Post; - m_collector.ChangePostVote(post, PostVoteAction.UpVote); - } - - /// - /// Fired when a down vote arrow is tapped - /// - /// - /// - private void DownVote_Tapped(object sender, EventArgs e) - { - Post post = (sender as FrameworkElement).DataContext as Post; - m_collector.ChangePostVote(post, PostVoteAction.DownVote); - } - - private async void OpenBrowser_Tapped(object sender, EventArgs e) - { - Post post = (sender as FrameworkElement).DataContext as Post; - string url = post.Url; - if (String.IsNullOrWhiteSpace(url)) - { - url = post.Permalink; - } - - if (!String.IsNullOrWhiteSpace(url)) - { - await Windows.System.Launcher.LaunchUriAsync(new Uri(url, UriKind.Absolute)); - App.BaconMan.TelemetryMan.ReportEvent(this, "OpenInBrowser"); - } - } - - private void More_Tapped(object sender, EventArgs e) - { - // Show the more menu - FrameworkElement element = sender as FrameworkElement; - if (element != null) - { - FlyoutBase.ShowAttachedFlyout(element); - } - - App.BaconMan.TelemetryMan.ReportEvent(this, "MoreTapped"); - } - - private void SavePost_Click(object sender, RoutedEventArgs e) - { - Post post = (sender as FrameworkElement).DataContext as Post; - m_collector.SaveOrHidePost(post, !post.IsSaved, null); - App.BaconMan.TelemetryMan.ReportEvent(this, "SavePostTapped"); - } - - private void HidePost_Click(object sender, RoutedEventArgs e) - { - Post post = (sender as FrameworkElement).DataContext as Post; - m_collector.SaveOrHidePost(post, null, !post.IsHidden); - App.BaconMan.TelemetryMan.ReportEvent(this, "HidePostTapped"); - } - - private void CopyLink_Click(object sender, RoutedEventArgs e) - { - // Get the post and copy the url into the clipboard - Post post = (sender as FrameworkElement).DataContext as Post; - DataPackage data = new DataPackage(); - if(String.IsNullOrWhiteSpace(post.Url)) - { - data.SetText("http://www.reddit.com" + post.Permalink); - } - else - { - data.SetText(post.Url); - } - Clipboard.SetContent(data); - App.BaconMan.TelemetryMan.ReportEvent(this, "CopyLinkTapped"); - } - - private void CopyPermalink_Click(object sender, RoutedEventArgs e) - { - // Get the post and copy the url into the clipboard - Post post = (sender as FrameworkElement).DataContext as Post; - DataPackage data = new DataPackage(); - data.SetText("http://www.reddit.com" + post.Permalink); - Clipboard.SetContent(data); - App.BaconMan.TelemetryMan.ReportEvent(this, "CopyLinkTapped"); - } - - private void SaveImage_Click(object sender, RoutedEventArgs e) - { - Post post = (sender as FrameworkElement).DataContext as Post; - App.BaconMan.ImageMan.SaveImageLocally(post.Url); - App.BaconMan.TelemetryMan.ReportEvent(this, "CopyLinkTapped"); - } - - // I threw up a little while I wrote this. - Post m_sharePost = null; - private void SharePost_Click(object sender, RoutedEventArgs e) - { - Post post = (sender as FrameworkElement).DataContext as Post; - // #todo handle things other than URLs - if (!String.IsNullOrWhiteSpace(post.Url)) - { - m_sharePost = post; - // Setup the share contract so we can share data - DataTransferManager dataTransferManager = DataTransferManager.GetForCurrentView(); - dataTransferManager.DataRequested += DataTransferManager_DataRequested; - DataTransferManager.ShowShareUI(); - App.BaconMan.TelemetryMan.ReportEvent(this, "SharePostTapped"); - } - } - - private void DataTransferManager_DataRequested(DataTransferManager sender, DataRequestedEventArgs args) - { - if(m_sharePost != null) - { - args.Request.Data.Properties.ApplicationName = "Baconit"; - args.Request.Data.Properties.ContentSourceWebLink = new Uri(m_sharePost.Url, UriKind.Absolute); - args.Request.Data.Properties.Title = "A Reddit Post Shared From Baconit"; - args.Request.Data.Properties.Description = m_sharePost.Title; - args.Request.Data.SetText($" \r\n\r\n{m_sharePost.Title}\r\n\r\n{m_sharePost.Url}"); - m_sharePost = null; - App.BaconMan.TelemetryMan.ReportEvent(this, "PostShared"); - } - else - { - args.Request.FailWithDisplayText("Baconit doesn't have anything to share!"); - App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "FailedToShareFilpViewPostNoSharePost"); - } - } - - /// - /// Called when we should open the global menu - /// - /// - /// - private void MenuPost_Tapped(object sender, EventArgs e) - { - // Show the global menu - m_host.ToggleMenu(true); - } - - /// - /// Fired when the user taps delete post. - /// - /// - /// - private async void DeletePost_Click(object sender, RoutedEventArgs e) - { - // Get the post. - Post post = (Post)((FrameworkElement)sender).DataContext; - - // Confirm - bool? doIt = await App.BaconMan.MessageMan.ShowYesNoMessage("Delete Post", "Are you sure you want to?"); - - if(doIt.HasValue && doIt.Value) - { - // Delete it. - m_collector.DeletePost(post); - } - } - - /// - /// Fired when the user taps edit post. - /// - /// - /// - private void EditPost_Click(object sender, RoutedEventArgs e) - { - Post post = (Post)((FrameworkElement)sender).DataContext; - ShowCommentBox("t3_" + post.Id, post.Selftext, post); - } - - /// - /// Called when the user wants to comment on the post - /// - /// - /// - private void PostCommentOn_OnIconTapped(object sender, EventArgs e) - { - Post post = (Post)((FrameworkElement)sender).DataContext; - ShowCommentBox("t3_" + post.Id, null, post); - } - - /// - /// Called when we should show more for the post - /// - /// - /// - - private void MorePost_Tapped(object sender, EventArgs e) - { - Post post = (sender as FrameworkElement).DataContext as Post; - } - - /// - /// Fired when the user taps the go to subreddit button - /// - /// - /// - private void GoToSubreddit_Click(object sender, RoutedEventArgs e) - { - // Navigate to the subreddit. - Post post = (sender as FrameworkElement).DataContext as Post; - Dictionary args = new Dictionary(); - args.Add(PanelManager.NAV_ARGS_SUBREDDIT_NAME, post.Subreddit); - m_host.Navigate(typeof(SubredditPanel), post.Subreddit + SortTypes.Hot + SortTimeTypes.Week, args); - App.BaconMan.TelemetryMan.ReportEvent(this, "GoToSubredditFlipView"); - } - - /// - /// Fired when the user taps the go to user button - /// - /// - /// - private void GoToUser_Click(object sender, RoutedEventArgs e) - { - // Navigate to the user. - Post post = (sender as FrameworkElement).DataContext as Post; - Dictionary args = new Dictionary(); - args.Add(PanelManager.NAV_ARGS_USER_NAME, post.Author); - m_host.Navigate(typeof(UserProfile), post.Author, args); - App.BaconMan.TelemetryMan.ReportEvent(this, "GoToUserFlipView"); - } - - #endregion - - #region Flippping Logic - - /// - /// Fired when the flip panel selection changes. - /// - /// - /// - private async void FlipView_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - if(ui_flipView.SelectedIndex == -1) - { - return; - } - - // Mark the item read. - m_collector.MarkPostRead((Post)ui_flipView.SelectedItem, ui_flipView.SelectedIndex); - - // Hide the comment box if shown - if (ui_commentBox != null) - { - ui_commentBox.HideBox(); - } - - // Reset the scroll pos - m_lastKnownScrollOffset = 0; - - // If the index is 0 we are most likely doing a first load, so we want to - // set the panel content instantly so we get the loading UI as fast a possible. - if (ui_flipView.SelectedIndex == 0) - { - // Update the posts - UpdatePanelContent(); - } - else - { - // Kick off the panel content update to the UI thread with idle pri to give the UI time to setup. - await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => - { - // Update the posts - UpdatePanelContent(); - }); - } - - // Now if we have deferred post add them - DoDeferredPostUpdate(); - } - - /// - /// Updates the content in all of the panels in flipview. - /// - private async void UpdatePanelContent() - { - // Create a list we need to set to the UI. - List> setToUiList = new List>(); - List clearList = new List(); - bool extendCollection = false; - - // Get the min and max number of posts to load. - int minContentLoad = ui_flipView.SelectedIndex; - int maxContentLoad = ui_flipView.SelectedIndex; - if (App.BaconMan.UiSettingsMan.FlipView_PreloadFutureContent) - { - maxContentLoad++; - } - - // Lock the list - lock (m_postsLists) - { - for (int i = 0; i < m_postsLists.Count; i++) - { - Post post = m_postsLists[i]; - if (i >= minContentLoad && i <= maxContentLoad) - { - // Add the post to the list of posts to set. We have to do this outside of the lock - // because we might delay while doing it. - setToUiList.Add(new Tuple(post, ui_flipView.SelectedIndex == i)); - } - else - { - // Add the post to the list of posts to clear. We have to do this outside of the lock - // because we might delay while doing it. - clearList.Add(post); - } - } - - // Check if we should load more posts. Note we want to check how many post the - // collector has because this gets called when the m_postList is being built, thus - // the count will be wrong. - if(m_postsLists.Count > 5 && m_collector.GetCurrentPosts().Count < maxContentLoad + 4) - { - extendCollection = true; - } - } - - // Extend if we should - if(extendCollection) - { - m_collector.ExtendCollection(25); - } - - // Now that we are out of lock set the items we want to set. - foreach(Tuple tuple in setToUiList) - { - // We found an item to show or prelaod, do it. - await SetPostContent(tuple.Item1, tuple.Item2); - - // If this is the first post to load delay for a while to give the UI time to setup. - // This will delay setting the next post as well as prefetching the comments for this - // current post. - if (m_isFirstPostLoad) - { - m_isFirstPostLoad = false; - await Task.Delay(1000); - } - - // If this is the current item also preload the comments now. - if (tuple.Item2) - { - Post postToSet = tuple.Item1; - PreFetchPostComments(ref postToSet); - } - } - - // Now set them all to not be visible and clear - // the comments. - foreach(Post post in clearList) - { - post.IsPostVisible = false; - Post refPost = post; - ClearPostComments(ref refPost); - } - - // Update the header sizes - SetHeaderSizes(); - - // Kick off a background thread to clear out what we don't want. - await Task.Run(() => - { - // Get all of our content. - List allContent = ContentPanelMaster.Current.GetAllAllowedContentForGroup(m_uniqueId); - - // Find the ids that should be cleared and clear them. - List removeId = new List(); - foreach (string contentId in allContent) - { - bool found = false; - foreach (Tuple tuple in setToUiList) - { - if (tuple.Item1.Id.Equals(contentId)) - { - found = true; - break; - } - } - - if (!found) - { - // If we didn't find it clear it. - ContentPanelMaster.Current.RemoveAllowedContent(contentId); - } - } - }); - } - - #endregion - - #region CommentScrollingLogic - - /// - /// Creates a comment manager for the post and prefetches the comments - /// - /// - private void PreFetchPostComments(ref Post post, bool forcePreFetch = false, bool showThreadSubset = true) - { - // Ensure we don't already have the comments for this post - lock (m_commentManagers) - { - for (int i = 0; i < m_commentManagers.Count; i++) - { - if (m_commentManagers[i].Post.Id.Equals(post.Id)) - { - return; - } - } - } - - // Create a comment manager for the post - FlipViewPostCommentManager manager = new FlipViewPostCommentManager(ref post, m_targetComment, showThreadSubset); - - // If the user wanted, kick off pre load of comments. - if (forcePreFetch || App.BaconMan.UiSettingsMan.FlipView_PreloadComments) - { - manager.PreFetchComments(); - } - - // Add the manager to the current list - lock(m_commentManagers) - { - m_commentManagers.Add(manager); - } - } - - /// - /// Will remove any comment managers that may exist for a post, and clears the comments. - /// - /// - private void ClearPostComments(ref Post post) - { - // Look for the comment manager, clean it up and remove it. - lock(m_commentManagers) - { - for(int i = 0; i < m_commentManagers.Count; i++) - { - if(m_commentManagers[i].Post.Id.Equals(post.Id)) - { - m_commentManagers[i].PrepareForDeletion(); - m_commentManagers.RemoveAt(i); - break; - } - } - } - } - - /// - /// Fired when a user tap the header to view an entire thread instead of just the - /// context of a message - /// - /// - /// - private void ViewEntireThread_Tapped(object sender, TappedRoutedEventArgs e) - { - // Get the post and the manager - Post post = ((Post)((FrameworkElement)sender).DataContext); - - // First remove the current comment manager - ClearPostComments(ref post); - - // Now make a new one without using the subset - PreFetchPostComments(ref post, true, false); - - // Update the header sizes to fix the UI - SetHeaderSizes(); - } - - /// - /// Fired when one of the lists are scrolling to the bottom. - /// - /// - /// - private void List_OnListEndDetectedEvent(object sender, OnListEndDetected e) - { - // NOTE!!! - // This is a very hot code path, so anything done here should be really quick! - - // Get the post - Post post = ((Post)((FrameworkElement)sender).DataContext); - - // Make sure the margin is normal. - if(post.FlipViewStickyHeaderMargin.Top != 0) - { - // If the margin is not 0 we are playing our render tick. If the sticky header - // is always set to collapsed it actually never loads or render until we set it to - // visible while we are scrolling which makes it 'pop' in with a delay. To avoid this - // it defaults visible but way off the screen, this makes it load and render so it is ready - // when we set it visible. Here is is safe to restore it to the normal state. - post.FlipViewStickyHeaderVis = Visibility.Collapsed; - post.FlipViewStickyHeaderMargin = new Thickness(0, 0, 0, 0); - } - - // Show or hide the scroll bar depending if we have gotten to comments yet or not. - post.VerticalScrollBarVisibility = e.ListScrollTotalDistance > 60 ? ScrollBarVisibility.Auto : ScrollBarVisibility.Hidden; - - // Find the header size for this post - int currentScrollAera = GetCurrentScrollArea(post); - - // This will return -1 if we can't get this yet, so just get out of here. - if(currentScrollAera == -1) - { - return; - } - - // Get the height of the current post header - double currentPostHeaderSize = 0; - lock (m_flipViewStoryHeaders) - { - foreach (Grid flipHeader in m_flipViewStoryHeaders) - { - Post headerPost = (Post)(flipHeader.DataContext); - if (headerPost != null && headerPost.Id.Equals(post.Id)) - { - currentPostHeaderSize = flipHeader.ActualHeight; - break; - } - } - } - - // Use the header size and the control size to figure out if we should show the static header. - bool showHeader = e.ListScrollTotalDistance > currentScrollAera - currentPostHeaderSize; - post.FlipViewStickyHeaderVis = showHeader ? Visibility.Visible : Visibility.Collapsed; - - // If we have a differed header update and we are near the top (not showing the header) - // do the update now. For a full story see the comment on m_hasDeferredHeaderSizeUpdate - if (m_hasDeferredHeaderSizeUpdate && !showHeader) - { - SetHeaderSizes(); - } - - // Update the last known scroll pos - m_lastKnownScrollOffset = (int)e.ListScrollTotalDistance; - - // Hide the tip box if needed. - HideCommentScrollTipIfNeeded(); - - // Do the rest of this work on a background thread - Task.Run(() => - { - FlipViewPostCommentManager manager = null; - - // Get the comment manager for this post - lock (m_commentManagers) - { - foreach (FlipViewPostCommentManager search in m_commentManagers) - { - if (search.Post.Id.Equals(post.Id)) - { - // When found, request more posts - manager = search; - break; - } - } - } - - // Make the call if we found one - if(manager != null) - { - manager.RequestMorePosts(); - } - }); - } - - /// - /// Fired when a story header loads. We need to keep track of them - /// so we can get their sizes. - /// - /// - /// - private void FlipViewStoryHeader_Loaded(object sender, RoutedEventArgs e) - { - // Add this to our list - lock(m_flipViewStoryHeaders) - { - m_flipViewStoryHeaders.Add(sender as Grid); - } - } - - /// - /// Updates the header sizes for the flip post so they fit perfectly on the screen. - /// - private void SetHeaderSizes() - { - // Get the screen size, account for the comment box if it is open. - int currentScrollAera = GetCurrentScrollArea(); - - // This will return -1 if we can't get this number yet. - if(currentScrollAera == -1) - { - return; - } - - // Lock the post list - lock (m_postsLists) - { - // Set up the objects for the UI - foreach (Post post in m_postsLists) - { - if(post.HeaderSize != currentScrollAera) - { - post.HeaderSize = currentScrollAera; - } - } - } - } - - /// - /// Returns the current space that's available for the flipview scroller - /// - /// - private int GetCurrentScrollArea(Post post = null) - { - // If the post is null get the current post - if(post == null && ui_flipView.SelectedIndex != -1) - { - post = m_postsLists[ui_flipView.SelectedIndex]; - } - - // Get the control size - int screenSize = (int)ui_contentRoot.ActualHeight; - - // Make sure we are ready. - if(ui_contentRoot.ActualHeight == 0) - { - // If not return -1. - return -1; - } - - // If the comment box is open remove the height of it. - if (ui_commentBox != null && ui_commentBox.IsOpen) - { - screenSize -= (int)ui_commentBox.ActualHeight; - } - - // If post is null back out here. - if (post == null) - { - return screenSize; - } - - // If we are showing the show all comments header add the height of that so it won't be on the screen by default - if(post.FlipViewShowEntireThreadMessage == Visibility.Visible) - { - screenSize += c_hiddenShowAllCommentsHeight; - } - - // If we are not hiding the large header also add the height of the comment bar so it will be off screen by default - // or if we are full screen from the flip control. - if(post.FlipviewHeaderVisibility == Visibility.Visible || m_isFullScreen) - { - screenSize += c_hiddenCommentHeaderHeight; - } - - return screenSize; - } - - private void EndDetectingListView_SelectionChanged(object sender, SelectionChangedEventArgs e) - { - EndDetectingListView listview = sender as EndDetectingListView; - listview.SelectedIndex = -1; - } - - /// - /// When a new list loads add our listeners - /// - /// - /// - private void EndDetectingListView_Loaded(object sender, RoutedEventArgs e) - { - EndDetectingListView list = sender as EndDetectingListView; - list.OnListEndDetectedEvent += List_OnListEndDetectedEvent; - - // Set the threshold to 0 so we always get notifications - list.EndOfListDetectionThrehold = 0.0; - - // Add this list to our current lists. - m_currentListViews.Add(new WeakReference(list)); - } - - - /// - /// Scrolls to the top of the list view for a post. - /// - /// - private void ScrollCommentsToTop(string postId) - { - lock(m_currentListViews) - { - // Loop through the possible lists. - foreach (WeakReference weakList in m_currentListViews) - { - EndDetectingListView currentList = null; - weakList.TryGetTarget(out currentList); - if (currentList != null) - { - // Check the post - Post post = (Post)currentList.DataContext; - if (post.Id.Equals(postId)) - { - currentList.ScrollIntoView(null); - break; - } - } - } - } - } - - #endregion - - #region Comment Click Listeners - - /// - /// Fired when the refresh button is pressed. - /// - /// - /// - private void CommentRefresh_Click(object sender, RoutedEventArgs e) - { - Post post = (sender as FrameworkElement).DataContext as Post; - FlipViewPostCommentManager manager = FindCommentManager(post.Id); - if (manager != null) - { - manager.Refresh(); - } - } - - private void CommentUp_Tapped(object sender, TappedRoutedEventArgs e) - { - // We don't need to animate here, the vote will do it for us - Comment comment = (sender as FrameworkElement).DataContext as Comment; - FlipViewPostCommentManager manager = FindCommentManager(comment.LinkId); - if (manager != null) - { - manager.UpVote_Tapped(comment); - } - } - - private void CommentDown_Tapped(object sender, TappedRoutedEventArgs e) - { - // We don't need to animate here, the vote will do it for us - Comment comment = (sender as FrameworkElement).DataContext as Comment; - FlipViewPostCommentManager manager = FindCommentManager(comment.LinkId); - if (manager != null) - { - manager.DownVote_Tapped(comment); - } - } - - private void CommentButton3_Tapped(object sender, TappedRoutedEventArgs e) - { - // Animate the text - AnimateText((FrameworkElement)sender); - - // Get the comment - Comment comment = (sender as FrameworkElement).DataContext as Comment; - - if(comment.IsDeleted) - { - App.BaconMan.MessageMan.ShowMessageSimple("LET IT GO!", "You can't edit a deleted comment!"); - return; - } - - if (comment.IsCommentOwnedByUser) - { - // Edit - ShowCommentBox("t1_" + comment.Id, comment.Body, comment); - } - else - { - // Reply - ShowCommentBox("t1_" + comment.Id, null, comment); - } - } - - private async void CommentButton4_Tapped(object sender, TappedRoutedEventArgs e) - { - // Animate the text - AnimateText((FrameworkElement)sender); - - // Get the comment - Comment comment = (sender as FrameworkElement).DataContext as Comment; - - if(comment.IsCommentOwnedByUser) - { - // Delete the comment - bool? response = await App.BaconMan.MessageMan.ShowYesNoMessage("Delete Comment", "Are you sure?"); - - if(response.HasValue && response.Value) - { - // Find the manager - FlipViewPostCommentManager manager = FindCommentManager(comment.LinkId); - if (manager != null) - { - manager.CommentDeleteRequest(comment); - } - } - } - else - { - // Navigate to the user - Dictionary args = new Dictionary(); - args.Add(PanelManager.NAV_ARGS_USER_NAME, comment.Author); - m_host.Navigate(typeof(UserProfile), comment.Author, args); - App.BaconMan.TelemetryMan.ReportEvent(this, "GoToUserFromComment"); - } - } - - private void CommentMore_Tapped(object sender, TappedRoutedEventArgs e) - { - // Animate the text - AnimateText((FrameworkElement)sender); - - // Show the more menu - FrameworkElement element = sender as FrameworkElement; - if (element != null) - { - FlyoutBase.ShowAttachedFlyout(element); - } - - App.BaconMan.TelemetryMan.ReportEvent(this, "CommentMoreTapped"); - } - - private void CommentSave_Click(object sender, RoutedEventArgs e) - { - Comment comment = (sender as FrameworkElement).DataContext as Comment; - FlipViewPostCommentManager manager = FindCommentManager(comment.LinkId); - if (manager != null) - { - manager.Save_Tapped(comment); - } - App.BaconMan.TelemetryMan.ReportEvent(this, "CommentSaveTapped"); - } - - private void CommentShare_Click(object sender, RoutedEventArgs e) - { - Comment comment = (sender as FrameworkElement).DataContext as Comment; - FlipViewPostCommentManager manager = FindCommentManager(comment.LinkId); - if (manager != null) - { - manager.Share_Tapped(comment); - } - App.BaconMan.TelemetryMan.ReportEvent(this, "CommentShareTapped"); - } - - private void CommentPermalink_Click(object sender, RoutedEventArgs e) - { - Comment comment = (sender as FrameworkElement).DataContext as Comment; - - FlipViewPostCommentManager manager = FindCommentManager(comment.LinkId); - if (manager != null) - { - manager.CopyPermalink_Tapped(comment); - } - App.BaconMan.TelemetryMan.ReportEvent(this, "CommentPermalinkTapped"); - } - - private void CommentCollapse_Tapped(object sender, TappedRoutedEventArgs e) - { - // Animate the text - AnimateText((FrameworkElement)sender); - - Comment comment = (sender as FrameworkElement).DataContext as Comment; - FlipViewPostCommentManager manager = FindCommentManager(comment.LinkId); - if (manager != null) - { - manager.Collpase_Tapped(comment); - } - } - - private void CollapsedComment_Tapped(object sender, TappedRoutedEventArgs e) - { - Comment comment = (sender as FrameworkElement).DataContext as Comment; - FlipViewPostCommentManager manager = FindCommentManager(comment.LinkId); - if (manager != null) - { - manager.Expand_Tapped(comment); - } - } - - /// - /// Does the text animation on the text box in the Grid. - /// Note!! We do this crazy animation stuff so the UI feel responsive. - /// It would be ideal to use the SimpleButtonText control but it is too expensive for the virtual list. - /// - /// - private void AnimateText(FrameworkElement textBlockContainer) - { - // Make sure it has children - if(VisualTreeHelper.GetChildrenCount(textBlockContainer) != 1) - { - return; - } - - // Try to get the text block - TextBlock textBlock = (TextBlock)VisualTreeHelper.GetChild(textBlockContainer, 0); - - // Return if failed. - if(textBlock == null) - { - return; - } - - // Make a storyboard - Storyboard storyboard = new Storyboard(); - ColorAnimation colorAnimation = new ColorAnimation(); - storyboard.Children.Add(colorAnimation); - - // Set them up. - Storyboard.SetTarget(storyboard, textBlock); - Storyboard.SetTargetProperty(storyboard, "(TextBlock.Foreground).(SolidColorBrush.Color)"); - storyboard.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 400)); - colorAnimation.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 400)); - storyboard.BeginTime = new TimeSpan(0, 0, 0, 0, 100); - - // Set the colors - colorAnimation.To = Color.FromArgb(255, 153, 153, 153); - colorAnimation.From = ((SolidColorBrush)Application.Current.Resources["SystemControlBackgroundAccentBrush"]).Color; - - // Set the text to be the accent color - textBlock.Foreground = ((SolidColorBrush)Application.Current.Resources["SystemControlBackgroundAccentBrush"]); - - // And animate it back out. - storyboard.Begin(); - } - - /// - /// Finds the comment manager for a given post. If one isn't found it returns null. - /// - /// - /// - private FlipViewPostCommentManager FindCommentManager(string postId) - { - string searchId = postId; - if (postId.StartsWith("t3_")) - { - searchId = postId.Substring(3); - } - - lock (m_commentManagers) - { - foreach (FlipViewPostCommentManager searchManager in m_commentManagers) - { - if (searchManager.Post.Id.Equals(searchId)) - { - return searchManager; - } - } - } - return null; - } - - /// - /// Fired when a user taps a link in a comment - /// - /// - /// - private void MarkdownTextBlock_OnMarkdownLinkTapped(object sender, UniversalMarkdown.OnMarkdownLinkTappedArgs e) - { - App.BaconMan.ShowGlobalContent(e.Link); - } - - #endregion - - #region Pro Tip Logic - - /// - /// Shows the comment scroll tip if needed - /// - public void ShowCommentScrollTipIfNeeded() - { - if(!App.BaconMan.UiSettingsMan.FlipView_ShowCommentScrollTip) - { - return; - } - - // Never show it again. - App.BaconMan.UiSettingsMan.FlipView_ShowCommentScrollTip = false; - - // Create the tip UI, add it to the UI and show it. - m_commentTipPopUp = new TipPopUp(); - m_commentTipPopUp.Margin = new Thickness(0, 0, 0, 90); - m_commentTipPopUp.VerticalAlignment = VerticalAlignment.Bottom; - m_commentTipPopUp.TipHideComplete += CommentTipPopUp_TipHideComplete; - ui_contentRoot.Children.Add(m_commentTipPopUp); - m_commentTipPopUp.ShowTip(); - } - - /// - /// Hides the comment scroll tip if needed. - /// - public void HideCommentScrollTipIfNeeded() - { - // If we don't have one return. - if(m_commentTipPopUp == null) - { - return; - } - - // Tell it to hide and set it to null - m_commentTipPopUp.HideTip(); - m_commentTipPopUp = null; - } - - /// - /// Fired when the hide is complete - /// - /// - /// - private void CommentTipPopUp_TipHideComplete(object sender, EventArgs e) - { - TipPopUp popUp = (TipPopUp)sender; - - // Unregister the event - popUp.TipHideComplete += CommentTipPopUp_TipHideComplete; - - // Remove the tip from the UI - ui_contentRoot.Children.Remove(popUp); - } - - #endregion - - #region Full Screen Loading - - /// - /// Shows a loading overlay if there isn't one already - /// - private async void ShowFullScreenLoading() - { - await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.High, () => - { - // Make sure we don't have one already, if so get out of here. - lock (this) - { - if (m_loadingOverlay != null) - { - return; - } - m_loadingOverlay = new LoadingOverlay(); - } - - m_loadingOverlay.OnHideComplete += LoadingOverlay_OnHideComplete; - Grid.SetRowSpan(m_loadingOverlay, 3); - ui_contentRoot.Children.Add(m_loadingOverlay); - m_loadingOverlay.Show(); - }); - } - - /// - /// Hides the exiting loading overlay - /// - private void HideFullScreenLoading() - { - LoadingOverlay overlay = null; - lock(this) - { - if (m_loadingOverlay == null) - { - return; - } - overlay = m_loadingOverlay; - } - - overlay.Hide(); - } - - /// - /// Fired when the overlay is hidden. - /// - /// - /// - private void LoadingOverlay_OnHideComplete(object sender, EventArgs e) - { - ui_contentRoot.Children.Remove(m_loadingOverlay); - lock(this) - { - m_loadingOverlay = null; - } - } - - #endregion - - #region Full Screen Logic - - /// - /// Fired when the control wants to go full screen. - /// - /// - /// - private void ContentPanelHost_OnToggleFullscreen(object sender, OnToggleFullScreenEventArgs e) - { - // Get the post - Post post = ((Post)((FrameworkElement)sender).DataContext); - - // Set if we are full screen or not. - m_isFullScreen = e.GoFullScreen; - - // Hide or show the header - if (e.GoFullScreen == (post.FlipviewHeaderVisibility == Visibility.Visible)) - { - ToggleHeader(post); - } - - // Scroll the header into view - ScrollCommentsToTop(post.Id); - } - - /// - /// Fired when the user has tapped the panel requesting the content to load. - /// - /// - /// - private async void ContentPanelHost_OnContentLoadRequest(object sender, OnContentLoadRequestArgs e) - { - // Find the post - Post post = null; - lock(m_postsLists) - { - foreach(Post search in m_postsLists) - { - if(search.Id.Equals(e.SourceId)) - { - post = search; - break; - } - } - } - - // Send off a command to load it. - if(post != null) - { - await Task.Run(() => - { - ContentPanelMaster.Current.AddAllowedContent(ContentPanelSource.CreateFromPost(post), m_uniqueId, false); - }); - } - } - - /// - /// Fired when the post header toggle is clicked - /// - /// - /// - private void PostHeaderToggle_Click(object sender, RoutedEventArgs e) - { - Post post = (Post)((FrameworkElement)sender).DataContext; - ToggleHeader(post); - } - - /// - /// Given a post toggles the header - /// - /// - private void ToggleHeader(Post post) - { - // #todo animate this - post.FlipviewHeaderVisibility = post.FlipviewHeaderVisibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible; - post.HeaderCollpaseToggleAngle = post.FlipviewHeaderVisibility == Visibility.Visible ? 180 : 0; - - // Update the header size - SetHeaderSizes(); - } - - #endregion - - #region Comment Sort - - /// - /// Fired when either comment sort is tapped - /// - /// - /// - private void OpenMenuFlyout_Tapped(object sender, TappedRoutedEventArgs e) - { - FrameworkElement element = sender as FrameworkElement; - if (element != null) - { - FlyoutBase.ShowAttachedFlyout(element); - } - App.BaconMan.TelemetryMan.ReportEvent(this, "CommentSortTapped"); - } - - /// - /// Fired when a user taps a new sort type for comments. - /// - /// - /// - private void CommentSortMenu_Click(object sender, RoutedEventArgs e) - { - // Get the post - Post post = (Post)((FrameworkElement)sender).DataContext; - - // Update sort type - MenuFlyoutItem item = sender as MenuFlyoutItem; - post.CommentSortType = GetCommentSortFromString(item.Text); - - // Get the collector and update the sort - FlipViewPostCommentManager commentManager = FindCommentManager(post.Id); - commentManager.ChangeCommentSort(); - } - - private CommentSortTypes GetCommentSortFromString(string typeString) - { - typeString = typeString.ToLower(); - switch(typeString) - { - case "best": - default: - return CommentSortTypes.Best; - case "controversial": - return CommentSortTypes.Controversial; - case "new": - return CommentSortTypes.New; - case "old": - return CommentSortTypes.Old; - case "q&a": - return CommentSortTypes.QA; - case "top": - return CommentSortTypes.Top; - } - } - - private void CommentShowingCountMenu_Click(object sender, RoutedEventArgs e) - { - // Get the post - Post post = (Post)((FrameworkElement)sender).DataContext; - - // Parse the new comment count - MenuFlyoutItem item = sender as MenuFlyoutItem; - post.CurrentCommentShowingCount = int.Parse(item.Text); - - // Get the collector and update the sort - FlipViewPostCommentManager commentManager = FindCommentManager(post.Id); - commentManager.UpdateShowingCommentCount(post.CurrentCommentShowingCount); - } - - #endregion - - #region Comment Box - - /// - /// Shows the comment box - /// - private void ShowCommentBox(string redditId, string editText, object context) - { - // Important! Call find name so the deferred loaded element is created! - FindName("ui_commentBox"); - ui_commentBox.Visibility = Visibility.Visible; - ui_commentBox.ShowBox(redditId, editText, context); - SetCommentBoxHeight(this.ActualHeight); - } - - /// - /// Fired when the comment box changes states. - /// - /// - /// - private void CommentBox_SizeChanged(object sender, SizeChangedEventArgs e) - { - // We do this so we jump the UI ever time the comment box is open. - // see the description on m_hasDeferredHeaderSizeUpdate for a full story. - if (e.NewSize.Height > 0 && m_lastKnownScrollOffset > 30) - { - m_hasDeferredHeaderSizeUpdate = true; - } - else - { - // Fire a header size change to fix them up. - SetHeaderSizes(); - } - } - - /// - /// Fired when the control changes sizes. If this happens we need to update - /// the max height of the comment box if we have one. - /// - /// - /// - private void FlipViewPanel_SizeChanged(object sender, SizeChangedEventArgs e) - { - SetCommentBoxHeight(e.NewSize.Height); - } - - /// - /// Sets the comment box's max height - /// - /// - private void SetCommentBoxHeight(double height) - { - // We have to set the max height because the grid row is set to auto. - // With auto the box will keep expanding as large as possible because - // it isn't bound by the grid. - if (ui_commentBox != null) - { - ui_commentBox.MaxHeight = height; - } - } - - /// - /// Fired when the comment box is done opening. - /// - /// - /// - private void CommentBox_OnBoxOpened(object sender, CommentBoxOnOpenedArgs e) - { - // We want to scroll the comment we are working off of into view. - if(e.RedditId.StartsWith("t1_")) - { - Comment comment = (Comment)e.Context; - foreach (WeakReference weakList in m_currentListViews) - { - EndDetectingListView currentList = null; - weakList.TryGetTarget(out currentList); - if(currentList != null) - { - Post post = (Post)currentList.DataContext; - if (post.Id.Equals(comment.LinkId.Substring(3))) - { - currentList.ScrollIntoView(comment); - } - } - } - } - } - - /// - /// Fired when the comment in the comment box was submitted - /// - /// - /// - private void CommentBox_OnCommentSubmitted(object sender, OnCommentSubmittedArgs e) - { - bool wasActionSuccessful = false; - - if(e.RedditId.StartsWith("t3_")) - { - Post post = (Post)e.Context; - - if(post != null) - { - if (e.IsEdit) - { - // We edited selftext - wasActionSuccessful = m_collector.EditSelfPost(post, e.Response); - - if(wasActionSuccessful) - { - // If we are successful to update the UI we will remove the post - // and reallow it. - ContentPanelMaster.Current.RemoveAllowedContent(post.Id); - ContentPanelSource soruce = ContentPanelSource.CreateFromPost(post); - ContentPanelMaster.Current.AddAllowedContent(soruce, m_uniqueId); - } - } - else - { - // We added a new comment - FlipViewPostCommentManager manager = FindCommentManager(post.Id); - if (manager != null) - { - wasActionSuccessful = manager.CommentAddedOrEdited("t3_"+post.Id, e); - } - else - { - App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "CommentSubmitManagerObjNull"); - } - } - } - else - { - App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "CommentSubmitPostObjNull"); - } - } - else if(e.RedditId.StartsWith("t1_")) - { - Comment comment = (Comment)e.Context; - if(comment != null) - { - // Comment added or edited. - FlipViewPostCommentManager manager = FindCommentManager(comment.LinkId); - if (manager != null) - { - wasActionSuccessful = manager.CommentAddedOrEdited("t1_"+comment.Id, e); - } - else - { - App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "CommentSubmitManagerObjNull"); - } - } - else - { - App.BaconMan.TelemetryMan.ReportUnExpectedEvent(this, "CommentSubmitCommentObjNull"); - } - } - - // Hide the box if good - if(wasActionSuccessful) - { - ui_commentBox.HideBox(true); - } - else - { - ui_commentBox.HideLoadingOverlay(); - } - } - - #endregion - - /// - /// Sets the post content. For now all we give the flip view control is the URL - /// and it must figure out the rest on it's own. - /// - /// - private async Task SetPostContent(Post post, bool isVisiblePost) - { - // Set that the post is visible if it is - post.IsPostVisible = isVisiblePost; - - // Only load the content if we are doing it with out action. (most of the time) - if (App.BaconMan.UiSettingsMan.FlipView_LoadPostContentWithoutAction) - { - await Task.Run(() => - { - ContentPanelMaster.Current.AddAllowedContent(ContentPanelSource.CreateFromPost(post), m_uniqueId, !isVisiblePost); - }); - } - } - - - /// - /// Fired when the content root changes sizes - /// - /// - /// - private void ContentRoot_SizeChanged(object sender, SizeChangedEventArgs e) - { - SetHeaderSizes(); - } - - /// - /// Gets a unique id for this flipview. - /// - /// - private string GetUniqueId() - { - return m_subreddit.Id + m_currentSort + m_currentSortTime; - } - } -} diff --git a/Baconit/Panels/MessageInbox.xaml.cs b/Baconit/Panels/MessageInbox.xaml.cs index 65539c6..95a7517 100644 --- a/Baconit/Panels/MessageInbox.xaml.cs +++ b/Baconit/Panels/MessageInbox.xaml.cs @@ -2,6 +2,7 @@ using BaconBackend.DataObjects; using Baconit.HelperControls; using Baconit.Interfaces; +using Baconit.Panels.FlipView; using System; using System.Collections.Generic; using System.Collections.ObjectModel; diff --git a/Baconit/Panels/Search.xaml.cs b/Baconit/Panels/Search.xaml.cs index d6d8310..8ac5902 100644 --- a/Baconit/Panels/Search.xaml.cs +++ b/Baconit/Panels/Search.xaml.cs @@ -2,6 +2,7 @@ using BaconBackend.DataObjects; using BaconBackend.Helpers; using Baconit.Interfaces; +using Baconit.Panels.FlipView; using System; using System.Collections.Generic; using System.Collections.ObjectModel; diff --git a/Baconit/Panels/SettingsPanels/FlipViewSettings.xaml b/Baconit/Panels/SettingsPanels/FlipViewSettings.xaml index 096b88d..5aaef6f 100644 --- a/Baconit/Panels/SettingsPanels/FlipViewSettings.xaml +++ b/Baconit/Panels/SettingsPanels/FlipViewSettings.xaml @@ -49,6 +49,12 @@ Margin="0,0,0,8" Toggled="PreLoadComments_Toggled" /> + + + +