Skip to content

Instantly share code, notes, and snippets.

@erikolsson
Last active February 19, 2020 19:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erikolsson/cb67dd8d0522d303dda576e59cda6f46 to your computer and use it in GitHub Desktop.
Save erikolsson/cb67dd8d0522d303dda576e59cda6f46 to your computer and use it in GitHub Desktop.
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
}
}
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