Skip to content

In-App Notifications (Notices) #1#8298

Merged
frosty merged 12 commits intodevelopfrom
feature/in-app-notifications
Dec 14, 2017
Merged

In-App Notifications (Notices) #1#8298
frosty merged 12 commits intodevelopfrom
feature/in-app-notifications

Conversation

@frosty
Copy link
Contributor

@frosty frosty commented Dec 11, 2017

This PR implements the first part of in-app notifications, to be used in the media library and plugins sections of the app. In this PR, we're adding:

  • A WordPressFlux store for Notices, and its accompanying Notice type and actions.
  • A temporary Notice Presenter, which uses UIAlertController to display notices. This will be replaced later with custom UI.
  • MediaCoordinator posts a notification when a set of uploads complete.
  • The feature is feature-flagged to local develop only.

Known Issues / Not Implemented

  • Because the Notice Presenter is temporary, there may be a couple of issues using UIAlertControllers to present notices – for example, they won't be presented if another view controller is already being presented modally.
  • The media library currently only presents a notification on success. The failure case will be handled later.
  • The action button on the alert (Write Post) currently doesn't do anything.

To test:

  • Open the Media Library and upload a couple of images.
  • Ensure that an alert is displayed when all the uploads complete.
  • Ensure that the alert is still displayed if you leave the media library during uploads.
  • Also check uploading more images after the initial set complete.
  • Modify mediaProgressCoordinatorDidFinishUpload in MediaCoordinator to dispatch multiple notices one after the other. Check that alerts are queued correctly (with the next appearing after you dismiss one).

@frosty frosty added the Media label Dec 11, 2017
@frosty frosty added this to the 9.1 milestone Dec 11, 2017
@frosty frosty requested review from SergioEstevao and koke December 11, 2017 15:05
return alert
}
}

Choose a reason for hiding this comment

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

Trailing Newline Violation: Files should have a single trailing newline. (trailing_newline)


extension WordPressAppDelegate {
@objc func configureNoticePresenter() {
let _ = NoticePresenter.shared
Copy link
Member

Choose a reason for hiding this comment

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

Could we just have the app delegate own an instance instead of having a global shared one? This looks a bit odd.

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 can (I'd prefer that too), but I don't think we can easily until the entire App Delegate is in Swift? I don't really want to make the presenter an NSObject, so the reference can't be in the obj-c App Delegate code, and the Swift code is an extension so can't add properties.

Copy link
Member

Choose a reason for hiding this comment

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

Right 😬 I don't mind temporarily making it a NSObject, but maybe you can put it inside StoreContainer for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After discussing this in Slack, I'd like to leave this shared for now as the other options have some caveats:

  • Making the presenter conform to NSObject is tricky because it has non-objective-C properties and an initialiser with a default value.
  • Adding it to the StoreContainer is okay, but it doesn't really belong there either, and I'd still need to add some code to ensure the StoreContainer is instantiated.

I'm going to add a comment to the code clarifying that it'll move in the near future, and once I add the proper UI the presenter will probably need to be instantiated with a reference to a presenting container – at which point it'll make more sense to create it in the app delegate.

}
}

private struct Queue<T>: ExpressibleByArrayLiteral {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure why the ExpressibleByArrayLiteral is there. Other than that, Queue looks good, but I'd put it in its own file with documentation and tests.

elements.append(value)
}

@discardableResult mutating func pop() -> T? {
Copy link
Member

Choose a reason for hiding this comment

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

Array has a popLast method, so you can simplify the code further by inserting at the beginning instead of appending.

struct Queue<Element> {
    private var elements = [Element]()

    mutating func push(_ element: Element) {
        elements.insert(element, at: elements.startIndex)
    }

    mutating func pop() -> Element? {
        return elements.popLast()
    }
}


enum NoticeAction: Action {
case post(Notice)
case dismiss(Notice)
Copy link
Member

Choose a reason for hiding this comment

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

Since this will always dismiss the current notice regardless of what's passed, I don't think it needs an associated value.

}


struct NoticeStoreState {
Copy link
Member

Choose a reason for hiding this comment

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

I've been thinking about some things that I find odd in the code, and I think it's just that the store is inspecting the values on the queue instead of just using push/pop. I think it might improve a bit if the store state was just the current notice to be presented (if any), and keep the rest in a queue of pending notices.

struct NoticeStoreState {
    var notice: Notice?
}


class NoticeStore: StatefulStore<NoticeStoreState> {
    private var pending = Queue<Notice>()

    // ...

    var nextNotice: Notice? {
        return state.notice
    }

    private func enqueueNotice(_ notice: Notice) {
        if state.notice == nil {
            state.notice = notice
        } else {
            pending.push(notice)
        }
    }

    private func dequeueNotice() {
        state.notice = pending.pop()
    }
}

This means the store would only dispatch state changes when the notice to present changes, and not every time a new one is enqueued, but honestly what threw me off was the @discardableResult on Queue.pop() 😁

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I like this! That makes sense, and you're right it means we only notify on changes for new notifications, not dismissing.

@frosty
Copy link
Contributor Author

frosty commented Dec 12, 2017

@koke I think I've addressed your comments, if you don't mind taking another look!

let item = queue.pop()
XCTAssertNil(item)
}

Choose a reason for hiding this comment

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

Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)

let item = queue.pop()
XCTAssertNil(item)
}

Choose a reason for hiding this comment

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

Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)

return Notice(title: title,
message: nil,
actionTitle: NSLocalizedString("Write Post", comment: "Button title for media notification. Opens the post editor."),
actionHandler: {})
Copy link
Member

Choose a reason for hiding this comment

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

I was going to suggest that if the "Write Post" button doesn't respond yet, maybe not show it in this first iteration?
But then maybe the model should enforce this better, and ensure you either have both an action title and handler, or none of them. This is not necessarily a blocker for this PR, but something to consider for the next iteration.

private let store: NoticeStore
private var storeReceipt: Receipt?

private var isPresenting = false
Copy link
Member

Choose a reason for hiding this comment

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

We'd have to test it, but I believe with the new NoticeStore design, the isPresenting state is redundant. Since the NoticeStoreStateState only contains the notice that should be presented, the presenter should ensure that's always the presented one.

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 think you're right – removing it now, much cleaner :)

@frosty
Copy link
Contributor Author

frosty commented Dec 13, 2017

@koke Just addressed your other issues. I took out "Write Post" for now, but I also added another initialiser to Notice to enforce having both an action title and handler.

Copy link
Contributor

@SergioEstevao SergioEstevao left a comment

Choose a reason for hiding this comment

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

The code looks super clean and the functionality is working smoothly. Just needs some extra documentation comments.

import Foundation
import WordPressFlux

struct Notice {
Copy link
Contributor

Choose a reason for hiding this comment

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

Some documentation for this struct and classes will be welcomed.

import UIKit
import WordPressFlux

class NoticePresenter: NSObject {
Copy link
Contributor

Choose a reason for hiding this comment

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

Some documentation here will be helpful also.

@frosty
Copy link
Contributor Author

frosty commented Dec 13, 2017

@SergioEstevao Thanks for the review! I added some documentation :)

Copy link
Contributor

@SergioEstevao SergioEstevao left a comment

Choose a reason for hiding this comment

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

Looking good!

@koke
Copy link
Member

koke commented Dec 14, 2017

Almost there. Code looks good and it works great. The only thing I've found is that the alert says Cancel, even though I see in the code you meant for it to say "Dismiss"

screen shot 2017-12-14 at 10 25 47

Copy link
Member

@koke koke left a comment

Choose a reason for hiding this comment

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

I don't consider the alert button a blocker, since the whole alert is temporary, so :shipit: when you consider 👏 👏 👏

@frosty
Copy link
Contributor Author

frosty commented Dec 14, 2017

Thank you both! And thanks for picking up the alert button issue, @koke – but you're right, it'll be removed very soon :)

@frosty frosty merged commit 0741b56 into develop Dec 14, 2017
@frosty frosty deleted the feature/in-app-notifications branch December 14, 2017 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants