Last active
February 19, 2020 19:23
-
-
Save erikolsson/cb67dd8d0522d303dda576e59cda6f46 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import Combine | |
class ChatViewController: UIViewController { | |
var cancellables = Set<AnyCancellable>() | |
let viewModel: ChatViewModel | |
init(viewModel: ChatViewModel) { | |
self.viewModel = viewModel | |
super.init(nibName: nil, bundle: nil) | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
viewModel.sections | |
.apply(to: dataSource, animatingDifferences: true, completion: { [weak tableView] in | |
tableView?.contentOffset = .zero | |
}) | |
.store(in: &cancellables) | |
viewModel.send(.markAsRead) | |
} | |
@objc func sendButtonPressed() { | |
viewModel.send(.sendMessage(textInputView.text)) | |
} | |
required init?(coder: NSCoder) { | |
fatalError() | |
} | |
} | |
extension NewChatViewController: UITextViewDelegate { | |
func textViewDidChange(_ textView: UITextView) { | |
placeholderLabel.isHidden = textView.text.count > 0 | |
sendButton.isHidden = textView.text.count == 0 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ChatViewModel: Store<ChatViewModel.State, ChatViewModel.Event> { | |
struct State { | |
let room: Room | |
var messages: [ChatMessage] = [] | |
} | |
enum Event { | |
case fetch | |
case fetchFailed(Error) | |
case didFetchMessages([ChatMessage]) | |
case subscribe | |
case socketFailed(Error) | |
case didReceiveMessage(ChatMessage) | |
case sendMessage(String) | |
case didSendMessage(ChatMessage) | |
case didFailSendingMessage(Error) | |
case markAsRead | |
case didMarkAsRead | |
} | |
init(appContext: AuthenticatedAppContext, room: Room) { | |
super.init(initialValue: State(room: room), | |
reducer: NewChatViewModel.reducer, | |
environment: appContext) | |
send(.fetch) | |
} | |
static func reducer(state: inout State, event: Event, environment: AuthenticatedAppContext) -> [Effect<Event>] { | |
switch event { | |
case .fetch: | |
return [initialFetch(appContext: environment, room: state.room)] | |
case .fetchFailed(_): | |
return [ | |
Just<Event>(.fetch).delay(for: .seconds(10), scheduler: DispatchQueue.main).eraseToEffect() | |
] | |
case .didFetchMessages(let messages): | |
state.messages = (state.messages + messages).unique().sorted(by: {$0.id > $1.id}) | |
return [Just<Event>(.subscribe).eraseToEffect()] | |
case .subscribe: | |
let maxId = state.messages.map(\.id).max() ?? 0 | |
return [subscribe(appContext: environment, room: state.room, fromId: maxId)] | |
case .socketFailed(_): | |
return [ | |
Just<Event>(.subscribe).delay(for: .seconds(5), scheduler: DispatchQueue.main).eraseToEffect() | |
] | |
case .didReceiveMessage(let message): | |
state.messages = (state.messages + [message]).unique().sorted(by: {$0.id > $1.id}) | |
return [] | |
case .sendMessage(let text): | |
return [sendMessage(appContext: environment, room: state.room, message: text)] | |
case .didSendMessage(let msg): | |
return [] | |
case .didFailSendingMessage(let err): | |
print(err) | |
return [] | |
case .markAsRead: | |
return [markAsRead(appContext: environment, room: state.room)] | |
case .didMarkAsRead: | |
return [] | |
} | |
} | |
static func subscribe(appContext: AuthenticatedAppContext, room: Room, fromId: Int) -> Effect<Event> { | |
return appContext.user | |
.tokenPublisher() | |
.replaceError(with: "") | |
.flatMap({ (token) -> Effect<Event> in | |
let authHeader = HTTPHeader(field: "X-Auth-Token", value: token) | |
let fromIdHeader = HTTPHeader(field: "from_id", value: "\(fromId)") | |
let base = URLRequestBuilder.subscribeChat(room: room) | |
.with(headers: [authHeader, fromIdHeader]) | |
let publisher = WebsocketPublisher(base: base) | |
return publisher.decode(type: ChatMessage.self, | |
success: Event.didReceiveMessage, | |
fail: Event.socketFailed) | |
.eraseToEffect() | |
}) | |
.eraseToEffect() | |
} | |
static func initialFetch(appContext: AuthenticatedAppContext, room: Room) -> Effect<Event> { | |
let builder = URLRequestBuilder.getChatMessages(room: room) | |
return appContext.authenticatedRequest(builder: builder) | |
.decode(type: [ChatMessage].self, success: Event.didFetchMessages, fail: Event.fetchFailed) | |
.eraseToEffect() | |
} | |
static func sendMessage(appContext: AuthenticatedAppContext, room: Room, message: String) -> Effect<Event> { | |
let builder = URLRequestBuilder.addChatMessage(room: room, message: message, uniqueID: UUID().uuidString) | |
return appContext.authenticatedRequest(builder: builder) | |
.decode(type: ChatMessage.self, success: Event.didSendMessage, fail: Event.didFailSendingMessage) | |
.eraseToEffect() | |
} | |
static func markAsRead(appContext: AuthenticatedAppContext, room: Room) -> Effect<Event> { | |
let builder = URLRequestBuilder.markAsRead(room: room) | |
return appContext.authenticatedRequest(builder: builder) | |
.map { _ in Event.didMarkAsRead } | |
.replaceError(with: Event.didMarkAsRead) | |
.eraseToEffect() | |
} | |
var sections: AnyPublisher<NSDiffableDataSourceSnapshot<Int, NewChatSectionItem>, Never> { | |
return $value.map(\.messages) | |
.map { (messages) -> NSDiffableDataSourceSnapshot<Int, NewChatSectionItem> in | |
var snapshot = NSDiffableDataSourceSnapshot<Int, NewChatSectionItem>() | |
snapshot.appendSections([0]) | |
var prev: ChatMessage? | |
let items = messages.map({ (msg) -> NewChatSectionItem in | |
defer { prev = msg } | |
return ChatSectionItem(message: msg, showProfile: prev?.uid != msg.uid) | |
}) | |
snapshot.appendItems(items, toSection: 0) | |
return snapshot | |
}.eraseToAnyPublisher() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment