Skip to content

Commit

Permalink
Refactor event options + add context menu actions
Browse files Browse the repository at this point in the history
  • Loading branch information
pakerwreah committed Feb 20, 2023
1 parent 8fa9d00 commit b369575
Show file tree
Hide file tree
Showing 20 changed files with 643 additions and 387 deletions.
96 changes: 70 additions & 26 deletions Calendr.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions Calendr/Events/ContextMenu/ContextMenu.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// ContextMenu.swift
// Calendr
//
// Created by Paker on 18/02/23.
//

import AppKit
import RxSwift

protocol ContextMenuAction {
var icon: NSImage? { get }
var title: String { get }
}

extension ContextMenuAction {
var icon: NSImage? { nil }
}

enum ContextMenuViewModelItem<Action: ContextMenuAction> {
case separator
case action(Action)
}

extension ContextMenuViewModelItem: Equatable where Action: Equatable { }

protocol ContextMenuViewModel {
associatedtype Action: ContextMenuAction
typealias ActionItem = ContextMenuViewModelItem<Action>

var items: [ActionItem] { get }
var actionCallback: Observable<Void> { get }

func triggerAction(_ action: Action)
}

private class ContextMenuItem<Action: ContextMenuAction>: NSMenuItem {

private let value: Action
private let trigger: (Action) -> Void

init(action: Action, trigger: @escaping (Action) -> Void) {
self.value = action
self.trigger = trigger
super.init(title: action.title, action: #selector(selected), keyEquivalent: "")
self.target = self
self.image = action.icon
}

@objc private func selected() {
trigger(value)
}

required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

class ContextMenu<ViewModel: ContextMenuViewModel>: NSMenu {

init(viewModel: ViewModel) {

super.init(title: "")

for item in viewModel.items {
switch item {
case .separator:
addItem(.separator())
case .action(let action):
addItem(ContextMenuItem(action: action, trigger: viewModel.triggerAction))
}
}
}

required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

29 changes: 29 additions & 0 deletions Calendr/Events/ContextMenu/ContextMenuFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// ContextMenuFactory.swift
// Calendr
//
// Created by Paker on 18/02/23.
//

import Foundation

enum ContextMenuFactory {

static func makeViewModel(
event: EventModel,
dateProvider: DateProviding,
calendarService: CalendarServiceProviding
) -> (any ContextMenuViewModel)? {

switch event.type {
case .event(let status) where status != .unknown:
return EventOptionsViewModel(event: event, calendarService: calendarService)

case .reminder:
return ReminderOptionsViewModel(event: event, dateProvider: dateProvider, calendarService: calendarService)

default:
return nil
}
}
}
98 changes: 98 additions & 0 deletions Calendr/Events/ContextMenu/EventOptionsViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// EventOptionsViewModel.swift
// Calendr
//
// Created by Paker on 18/02/23.
//

import AppKit
import RxSwift

enum EventAction {
case accept
case maybe
case decline
}

class EventOptionsViewModel: ContextMenuViewModel {
typealias Action = EventAction

private let actionCallbackObserver: AnyObserver<Void>
let actionCallback: Observable<Void>

private(set) var items: [ActionItem] = []

private let event: EventModel
private let calendarService: CalendarServiceProviding

private let disposeBag = DisposeBag()

init(
event: EventModel,
calendarService: CalendarServiceProviding
) {
self.event = event
self.calendarService = calendarService

(actionCallback, actionCallbackObserver) = PublishSubject.pipe()

if event.status != .accepted {
items.append(.action(.accept))
}
if event.status != .maybe {
items.append(.action(.maybe))
}
if event.status != .declined {
items.append(.action(.decline))
}
}

func triggerAction(_ action: Action) {

calendarService.changeEventStatus(id: event.id, date: event.start, to: action.status)
.subscribe(
onNext: actionCallbackObserver.onNext,
onError: actionCallbackObserver.onError
)
.disposed(by: disposeBag)
}
}

private extension EventAction {

var status: EventStatus {
switch self {
case .accept:
return .accepted
case .maybe:
return .maybe
case .decline:
return .declined
}
}
}

extension EventAction: ContextMenuAction {

var icon: NSImage? {
switch self {
case .accept:
return Icons.EventStatus.accepted.with(color: .systemGreen)
case .maybe:
return Icons.EventStatus.maybe.with(color: .systemOrange)
case .decline:
return Icons.EventStatus.declined.with(color: .systemRed)
}
}

var title: String {
switch self {
case .accept:
return Strings.EventStatus.Action.accept
case .maybe:
return Strings.EventStatus.Action.maybe
case .decline:
return Strings.EventStatus.Action.decline
}
}
}
94 changes: 94 additions & 0 deletions Calendr/Events/ContextMenu/ReminderOptionsViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// ReminderOptionsViewModel.swift
// Calendr
//
// Created by Paker on 18/02/23.
//

import Foundation
import RxSwift

enum ReminderAction: Equatable {
case complete
case remind(DateComponents)
}

class ReminderOptionsViewModel: ContextMenuViewModel {
typealias Action = ReminderAction

private let actionCallbackObserver: AnyObserver<Void>
let actionCallback: Observable<Void>

let items: [ActionItem]

private let event: EventModel
private let dateProvider: DateProviding
private let calendarService: CalendarServiceProviding

private let disposeBag = DisposeBag()

init(
event: EventModel,
dateProvider: DateProviding,
calendarService: CalendarServiceProviding
) {
self.event = event
self.dateProvider = dateProvider
self.calendarService = calendarService

(actionCallback, actionCallbackObserver) = PublishSubject.pipe()

items = [
.action(.complete),
.separator,
.action(.remind(.init(minute: 5))),
.action(.remind(.init(minute: 15))),
.action(.remind(.init(minute: 30))),
.action(.remind(.init(hour: 1))),
.action(.remind(.init(day: 1)))
]
}

private func triggerAction( _ action: Action) -> Observable<Void> {

switch action {
case .complete:
return calendarService.completeReminder(id: event.id)

case .remind(let dateComponents):
let date = dateProvider.calendar.date(byAdding: dateComponents, to: dateProvider.now)!
return calendarService.rescheduleReminder(id: event.id, to: date)
}
}

func triggerAction(_ action: Action) {

triggerAction(action)
.subscribe(
onNext: actionCallbackObserver.onNext,
onError: actionCallbackObserver.onError
)
.disposed(by: disposeBag)
}
}

private enum Constants {

static let formatter = {
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named
return formatter
}()
}

extension ReminderAction: ContextMenuAction {

var title: String {
switch self {
case .complete:
return Strings.Reminder.Options.complete
case .remind(let dateComponents):
return Strings.Reminder.Options.remind(Constants.formatter.localizedString(from: dateComponents))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ class EventDetailsViewController: NSViewController, NSPopoverDelegate {

private let optionsLabel = Label()
private let optionsButton = NSButton()
private lazy var eventOptions = EventOptions(current: viewModel.status)
private lazy var reminderOptions = ReminderOptions()

private let viewModel: EventDetailsViewModel

Expand Down Expand Up @@ -345,36 +343,24 @@ class EventDetailsViewController: NSViewController, NSPopoverDelegate {
.bind { $0.material = $1 }
.disposed(by: disposeBag)

switch viewModel.type {
case .event(let status) where status != .unknown:
setUpOptionsMenuBindings(options: eventOptions, observer: viewModel.eventActionObserver)

case .reminder:
setUpOptionsMenuBindings(options: reminderOptions, observer: viewModel.reminderActionObserver)

default:
break
if let contextMenuViewModel = viewModel.makeContextMenuViewModel() {
setUpOptionsMenu(contextMenuViewModel)
}
}

private func setUpOptionsMenuBindings<T: NSMenu & ObservableConvertibleType>(
options: T,
observer: AnyObserver<T.Element>
) {
private func setUpOptionsMenu(_ viewModel: some ContextMenuViewModel) {

let menu = ContextMenu(viewModel: viewModel)

optionsButton.rx.tap.bind { [optionsButton] in
options.popUp(
menu.popUp(
positioning: nil,
at: NSPoint(x: 0, y: optionsButton.bounds.height),
in: optionsButton
)
}
.disposed(by: disposeBag)

options.asObservable()
.bind(to: observer)
.disposed(by: disposeBag)

viewModel.actionCallback
.observe(on: MainScheduler.instance)
.subscribe(
Expand Down
Loading

0 comments on commit b369575

Please sign in to comment.