Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wip stretch goal] Live stream discovery #53

Merged
merged 74 commits into from
Feb 7, 2017
Merged

Conversation

mbrandonw
Copy link
Contributor

Hey everyone, Justin and I wanted to get one more thing done before my time in cape town is over (sniff), and so we thought about adding a lil bit of discovery to the live streams. Currently you can only find live streams if you go straight to a project, no deep-linking or push notifications yet :/

So, we just added a lil "Kickstarter Live" entry to the discovery filters:

live-filters

Tapping that takes you to a feed of live, upcoming and recent live streams:

simulator screen shot jan 19 2017 8 53 48 pm

simulator screen shot jan 19 2017 8 53 58 pm

Tapping those cards either goes to the countdown, live stream or reply.

NB

This PR has a few things that I wanna call out:

  1. We are now hitting two api endpoints on the live api: one to get a list of events and one to get a single event. these two endpoints return subtly different json unfortunately and so i had to do a bit of gymnastics to accommodate for this. i'll call out the lines in my self-review.

  2. Due to the above, i had to change the live stream models quite a bit, which then wreaked havoc on the lenses. So, I wanted to try something new to ease the creation of lenses for us (at least until we get code gen). If we set all of the fields of a struct to public private(set) var field, we still get to act like we are immutable to the outside world, but inside this file only we can make lenses like:

extension Model {
  public enum lens {
    public static let field = Lens<Model, Int>(
      view: { $0.field },
      set: { var new = $1; new.field= $0; return field; }
    )
  }
}

not only is it less code, but it doesnt break when we add new fields. whatdya'll think?

}
}

private func sorted(liveStreamEvents: [LiveStreamEvent]) -> [LiveStreamEvent] {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

if the code in this function looks suspiciously like one i cooked up in #45, then you are right. unfortunately i cant share the code cause one operates on LiveStreamEvents and the other on Project.LiveStream :(((

the models are a bit messy due to hitting two different apis right now. we are gonna try to find a way to bridge these two worlds soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

actually never mind on this! with @justinswart's recent refactoring work to get rid of Project.LiveStream #63 i was able to share all of this code.

events.forEach { event in
self.appendRow(value: event, cellClass: LiveStreamDiscoveryCell.self, toSection: title.section)
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

^^^ this is a fun lil thing. it does the work to create titles for live/upcoming/recent with live streams grouped into them. it's heavily tested too...

Copy link
Contributor

Choose a reason for hiding this comment

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

cool! just thinking, we coullllld in the future, maybe, update Sorts to have these 3 categories.... if this feature becomes really popular, it might be nice to go directly to these sections. Could have a sort option for streams your friends subscribe too..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@theginbin ^^^ that would be awesome! a really nice lil step after the initial release.

@@ -23,11 +23,11 @@ internal final class LiveStreamContainerViewControllerTests: TestCase {
|> Project.LiveStream.lens.startDate .~ (MockDate().timeIntervalSince1970 - 86_400)
|> Project.LiveStream.lens.isLiveNow .~ false
let liveStreamEvent = .template
|> LiveStreamEvent.lens.stream.hasReplay .~ true
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I got rid of the Stream sub-struct of LiveStreamEvent in order to flatten the models a bit.

@@ -377,32 +379,3 @@ public final class LiveStreamCountdownViewController: UIViewController {
self.eventDetailsViewModel.inputs.subscribeButtonTapped()
}
}

// Returns a fancy monospaced font for the countdown.
private func countdownFont(label: UILabel) -> UIFont {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we use this twice now so i extracted it to a helper on UIFont, can just do baseFont.countdownMonospaced

nav.viewControllers = [vc]
DispatchQueue.main.async {
self.present(nav, animated: true, completion: nil)
}
Copy link
Contributor Author

@mbrandonw mbrandonw Jan 20, 2017

Choose a reason for hiding this comment

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

we were seeing some weird flickering of the screen when not using DispatchQueue.main.async :/ it seems to be entirely due to initializing the nav controller and then setting its controllers, as opposed to using the UINavigationController(rootViewController:) initializer.

///
/// - returns: A signal producer.
public func countdownProducer(to date: Date)
-> SignalProducer<(day: String, hour: String, minute: String, second: String), NoError> {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is a fun function! we use it in the countdown controller and the countdown of the live stream discovery cells.

lots of tests here: https://github.com/kickstarter/ios-oss/pull/53/files#diff-c7a229a8e1612db2ccad1308b69894a5

@@ -297,6 +297,8 @@ private func stringsForTitle(params: DiscoveryParams) -> (filter: String, subcat

if params.staffPicks == true {
filterText = Strings.Projects_We_Love()
} else if params.hasLiveStreams == .some(true) {
filterText = "Kickstarter Live"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

do we need to localize this? or is this a branding thing? /cc @ktman @rreymundi

Copy link

Choose a reason for hiding this comment

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

Hey @awakinglim mind weighing in here? Curious what you think from brand, UX, localization perspective? This will appear in the discovery overlay and let you access cards for upcoming, live, and recent streams.

var discoveryPagesViewHidden: Signal<Bool, NoError> { get }

/// Emits a boolean that determines if the live stream discovery view is hidden.
var liveStreamDiscoveryViewHidden: Signal<Bool, NoError> { get }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

so the livestream discovery controller is an embedded controller in DiscoveryViewController, which means the discovery pages are its siblings. this vm will now take care of hiding and showing them appropriately.

let currentParams = Signal.merge(
self.viewWillAppearProperty.signal.take(first: 1).mapConst(DiscoveryViewModel.defaultParams),
self.filterWithParamsProperty.signal.skipNil()
)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

just a small refactor while i was here. don't know why i jumped through hoops for that weird producer thing when it can be done so much more easily...


_ = self.hoursSubtitleLabel
|> UILabel.lens.text %~ { _ in localizedString(key: "days", defaultValue: "hours") }
|> UILabel.lens.text %~ { _ in Strings.hours() }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we had a typo here! we were using days for hours! 😱

Copy link
Contributor

Choose a reason for hiding this comment

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

😳 good catch!

Copy link
Contributor

@justinswart justinswart left a comment

Choose a reason for hiding this comment

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

Just a couple observations!

Great work! This is going to be great 👍

@@ -49,6 +49,7 @@
"Continue_to_payment" = "Continue to payment";
"Continue_to_update_pledge" = "Continue to update pledge";
"Couldnt_add_attachment" = "Couldn't add attachment";
"Couldnt_open_live_stream_Try_again_later" = "Couldn‘t open live stream. Try again later.";
Copy link
Contributor

Choose a reason for hiding this comment

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

Is that a backtick in Couldn't? Doesn't look like a normal apostrophe.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's a "smart" apostrophe http://smartquotesforsmartpeople.com

that site is a bit much :/

@testable import Kickstarter_Framework
@testable import Library
@testable import LiveStream
@testable import KsApi
Copy link
Contributor

Choose a reason for hiding this comment

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

Could alphabetize

parent.view.setNeedsLayout()
vc.view.setNeedsLayout()
parent.view.setNeedsUpdateConstraints()
vc.view.setNeedsUpdateConstraints()
Copy link
Contributor

Choose a reason for hiding this comment

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

What were all the layout passes needed for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i cant remember! lemme try without and see what happens

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not needed at all! removing!

func testCountdownProducer() {
let future: TimeInterval = TimeInterval(1*60*60*24) + TimeInterval(16*60*60) + TimeInterval(34*60) + 2
let futureDate = MockDate().addingTimeInterval(future).date
let countdown = countdownProducer(to: futureDate)
Copy link
Contributor

Choose a reason for hiding this comment

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

So DRY 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

brave new world for me!

@testable import KsApi
@testable import Library
@testable import LiveStream
import Prelude
Copy link
Contributor

Choose a reason for hiding this comment

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

Alphabetize

@testable import KsApi
@testable import Library
@testable import LiveStream
import Prelude
Copy link
Contributor

Choose a reason for hiding this comment

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

☝️

@@ -91,7 +101,7 @@ LiveStreamContainerViewModelInputs, LiveStreamContainerViewModelOutputs {
)
.map(first)

let initialEvent = configData.map(second)
let initialEvent = configData.map { _, event, _, _ in event }
Copy link
Contributor

Choose a reason for hiding this comment

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

We should add a fourth sometime 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we really should!

}
.map { (day: $0.day ?? 0, hour: $0.hour ?? 0, minute: $0.minute ?? 0, second: $0.second ?? 0) }
.take(first: 1)
.switchMap { countdownProducer(to: $0.startDate) }
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@@ -64,6 +64,40 @@ public struct LiveStreamEvent: Equatable {
public var definitelyHasReplay: Bool {
return self.hasReplay && self.replayUrl != nil
}

public static func canonicalLiveStreamEventComparator(now: Date) -> Prelude.Comparator<LiveStreamEvent> {
Copy link
Contributor

Choose a reason for hiding this comment

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

🚀

@justinswart justinswart self-requested a review February 7, 2017 20:14
Copy link
Contributor

@justinswart justinswart left a comment

Choose a reason for hiding this comment

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

👍

@mbrandonw mbrandonw merged commit 6837f3f into master Feb 7, 2017
@mbrandonw mbrandonw deleted the live-stream-disc branch February 7, 2017 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants