public MeetingPageViewModel(IScreen hs, IMeetingRef mRef) { // Initial default values HostScreen = hs; Sessions = new ReactiveList <SessionUserControlViewModel>(); TalkFiles = new ReactiveList <FileUserControlViewModel>(); // And start off a background guy to populate everything. LoadMeeting(mRef); }
/// <summary> /// The meeting load failed for some reason. Offline and no cache, most likely. So we need to display something. /// </summary> /// <param name="meeting"></param> /// <returns></returns> private IObservable <IMeeting> MeetingLoadFailed(IMeetingRef meeting) { return(Observable.Return(true) .ObserveOn(RxApp.MainThreadScheduler) .Select(_ => new MessageDialog("Unable to connect to the meeting server. Either you are offline, or it doesn't exist.")) .SelectMany(d => d.ShowAsync()) .ObserveOn(RxApp.MainThreadScheduler) .Do(_ => Locator.Current.GetService <RoutingState>().NavigateBack.Execute(null)) .Where(_ => false) .Select(_ => (IMeeting)null)); }
/// <summary> /// Return meetings loaded from the internet. We will continue sending them if the meeting /// is currently running (e.g. doing constant updates). /// </summary> /// <param name="meeting"></param> /// <returns>Stream of meetings</returns> /// <remarks> /// The algorithm for this: /// 1. Always fetch the meeting at least one time. /// 2. If we are in the outer buffer region, check once an hour /// 3. If we are in the inner buffer region, then check every 5 minutes. /// /// Inner buffer region: meeting start and end times, with 30 minutes before the meeting, and 2 hours after the meeting. /// Outer buffer region: 1 day before the meeting to 1 day after the meeting. /// </remarks> private IObservable <IMeeting> MeetingLoader(IMeetingRef meeting) { // Grab a copy of the meeting online. var firstMeeting = Observable.FromAsync(() => meeting.GetMeeting()) .Catch(Observable.Empty <IMeeting>()) .Publish(); // Determine the two buffer times for this meeting, and create a "pulse" that will // fire at the required intervals during those buffered times. var laterUpdates = firstMeeting .SelectMany(m => { var bufInner = new TimePeriod(m.StartTime - TimeSpan.FromMinutes(30), m.EndTime + TimeSpan.FromHours(2)); var bufOutter = new TimePeriod(m.StartTime - TimeSpan.FromDays(1), m.EndTime + TimeSpan.FromDays(1)); IObservable <Unit> reloadPulse; if (!bufOutter.Contains(DateTime.Now)) { // This meeting is way late or way early. // Don't waste any time doing the loading. reloadPulse = Observable.Empty <Unit>(); Debug.WriteLine("Not triggering re-fetches for the meeting"); } else { // We do both inner and outer so that if someone leaves this up for a long // time it will work will. var outter = Observable.Timer(TimeSpan.FromMinutes(30)) .Where(_ => bufOutter.Contains(DateTime.Now)) .WriteLine("Getting meeting in outer buffer") .Select(_ => default(Unit)); var inner = Observable.Timer(TimeSpan.FromMinutes(5)) .Where(_ => bufInner.Contains(DateTime.Now)) .WriteLine("Getting meeting in inner buffer") .Select(_ => default(Unit)); reloadPulse = Observable.Merge(outter, inner); } // Now, use it to trigger the meetings. return(reloadPulse .SelectMany(_ => Observable.FromAsync(() => meeting.GetMeeting()).Catch(Observable.Empty <IMeeting>()))); }); var meetingSequence = Observable.Merge(firstMeeting, laterUpdates); // TODO: why do we need this firstMeeting a published observable? firstMeeting.Connect(); return(meetingSequence); }
/// <summary> /// Given a meeting, load the info. Since this is an asynchronous command, we have to schedule stuff off it. /// </summary> /// <param name="meeting"></param> private void LoadMeeting(IMeetingRef meeting) { Debug.WriteLine("Staring a new LoadMeeting."); // Fetch the guy from the local cache. StartMeetingUpdates = ReactiveCommand.Create(); var ldrCmdReady = StartMeetingUpdates .Take(1) //.SelectMany(_ => Blobs.LocalStorage.GetAndFetchLatest(meeting.AsReferenceString(), () => MeetingLoader(meeting), null, DateTime.Now + Settings.CacheAgendaTime)) .SelectMany(_ => Blobs.LocalStorage.GetAndFetchLatest(meeting.AsReferenceString(), () => meeting.GetMeeting(), null, DateTime.Now + Settings.CacheAgendaTime)) .CatchAndSwallowIfAfter(1, (Exception e) => MeetingLoadFailed(meeting)) .Publish(); ldrCmdReady .Select(m => m.Title) .ToProperty(this, x => x.MeetingTitle, out _title, "", RxApp.MainThreadScheduler); ldrCmdReady .Select(m => m.StartTime.ToString(@"M\/d\/yyyy h\:mm tt") + " - " + m.EndTime.ToString(@"h\:mm tt") + " (" + (m.EndTime - m.StartTime).ToString(@"h\:mm") + " long)") .ToProperty(this, x => x.StartTime, out _startTime, "", RxApp.MainThreadScheduler); // The talks that are in the meeting header. ldrCmdReady .Select(m => m.AttachedFiles.OrderBy(mt => mt.DisplayName).ToArray()) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(files => SetAsMeetingFiles(files)); // We want to notify the user that the meeting has no talks. We need to turn on the "no talk" thing // and also switch off the "loading" (which is normally switched off after all the talks are loaded). var meetingIsEmpty = ldrCmdReady .Select(m => m.Sessions.SelectMany(s => s.Talks).Count() == 0 && m.Sessions.Length <= 1); meetingIsEmpty .ToProperty(this, x => x.MeetingIsEmpty, out _meetingIsEmpty, false, RxApp.MainThreadScheduler); meetingIsEmpty .Where(mcnt => mcnt) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_displayDayIndex => MeetingIsReadyForDisplay = true); var ldrSessions = ldrCmdReady .Select(m => m.Sessions) .Select(s => s == null ? new ISession[0] : s); // Multi-day meetings are a bit more complex. Here we take the sessions and extract all the days associated with them. // Day's is a stream of date/times. var days = from meetingSessions in ldrSessions select(from s in meetingSessions group s.StartTime by s.StartTime.DayOfYear).Select(sgroup => sgroup.First()).Select(dt => new DateTime(dt.Year, dt.Month, dt.Day)).ToArray(); // Select the first item on the list of days. // Give it a reasonable initial value so when everything is wired up we don't get a crash. DisplayDayIndex = -1; Days = new ReactiveList <DateTime>(); days //.Select(lst => lst.Select(dt => string.Format("{0} ({1})", dt.DayOfWeek, dt.ToString("dd MMMM"))).ToArray()) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(ds => { Days.MakeListLookLike(ds); if (DisplayDayIndex >= ds.Length) { DisplayDayIndex = ds.Length - 1; } if (ds.Length > 0 && DisplayDayIndex < 0) { DisplayDayIndex = 0; } }); // When we have a set of sessions, only display the day we want to show. var selectedByUserDay = this.ObservableForProperty(x => x.DisplayDayIndex) .Select(v => v.Value) .DistinctUntilChanged() .Select(index => index >= 0 && index < Days.Count ? Days[index] : new DateTime()); var theDaysSessions = Observable.CombineLatest(ldrSessions, selectedByUserDay, (ses, day) => Tuple.Create(ses, day)) .Select(x => x.Item1.Where(s => s.StartTime.DayOfYear == x.Item2.DayOfYear)); // And prepare them for display theDaysSessions .Select(s => s.OrderBy(ss => ss.StartTime).ToArray()) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(sessions => SetAsSessions(sessions, ldrSessions)); // And mark this meeting as having been viewed by the user! var db = Locator.Current.GetService <IMRUDatabase>(); Debug.Assert(db != null); ldrCmdReady .Subscribe(m => db.MarkVisitedNow(m)); // Start everything off. ldrCmdReady.Connect(); // If they want to see it in the browser OpenMeetingInBrowser = ReactiveCommand.Create(); OpenMeetingInBrowser .Subscribe(async _ => await Launcher.LaunchUriAsync(new Uri(meeting.WebURL))); }