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

[Showcase] Chat sample app template #315

Merged
merged 2 commits into from
Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
477 changes: 477 additions & 0 deletions Example/ExampleApp.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions Example/Showcase/Classes/ApplicationLayer/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}

// MARK: UISceneSession Lifecycle

func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}

func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {

}
}
8 changes: 8 additions & 0 deletions Example/Showcase/Classes/ApplicationLayer/Application.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

final class Application {

let chatService: ChatService = {
return ChatService()
}()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import UIKit

struct AppearanceConfigurator: Configurator {

func configure() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
struct ApplicationConfigurator: Configurator {

private let app: Application

init(app: Application) {
self.app = app
}

func configure() {
ChatListModule.create(app: app)
.wrapToNavigationController()
.present()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
protocol Configurator {
func configure()
}

extension Array where Element == Configurator {
func configure() {
forEach { $0.configure() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
struct MigrationConfigurator: Configurator {

let app: Application

init(app: Application) {
self.app = app
}

func configure() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
struct ThirdPartyConfigurator: Configurator {

func configure() {

}
}
26 changes: 26 additions & 0 deletions Example/Showcase/Classes/ApplicationLayer/SceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

var window: UIWindow?

private let app = Application()

private var configurators: [Configurator] {
return [
MigrationConfigurator(app: app),
ApplicationConfigurator(app: app),
AppearanceConfigurator(),
ThirdPartyConfigurator()
]
}

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }

window = UIWindow(windowScene: windowScene)
window?.makeKeyAndVisible()

configurators.configure()
}
}
39 changes: 39 additions & 0 deletions Example/Showcase/Classes/DomainLayer/ChatService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation
import Combine

typealias MessageStream = AsyncPublisher<AnyPublisher<[Message], Never>>

final class ChatService {

private let messagesSubject: CurrentValueSubject<[Message], Never> = {
return CurrentValueSubject([
Message(message: "✨gm", authorAccount: "2", timestamp: 0),
Message(message: "how r u man?", authorAccount: "1", timestamp: 0),
Message(message: "good", authorAccount: "2", timestamp: 0),
Message(message: "u?", authorAccount: "2", timestamp: 0),
Message(message: "I’m so happy I have you as my best friend, and I love Lisa so much", authorAccount: "1", timestamp: 0),
Message(message: "Why, Lisa, why, WHY?!", authorAccount: "2", timestamp: 0),
Message(message: "It’s bullshit, I did not hit her. I did nooot. Oh hi, Mark!", authorAccount: "1", timestamp: 0),
Message(message: "Johnny’s my best friend!", authorAccount: "2", timestamp: 0),
Message(message: "Anyway, how’s your sex life?", authorAccount: "1", timestamp: 0)
])
}()

func getMessages(topic: String) -> MessageStream {
return messagesSubject.eraseToAnyPublisher().values
}

func getAuthorAccount() async -> String {
return "1"
}

func sendMessage(text: String) async throws {
let authorAccount = await getAuthorAccount()
let message = Message(
message: text,
authorAccount: authorAccount,
timestamp: Int64(Date().timeIntervalSince1970)
)
messagesSubject.send(messagesSubject.value + [message])
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
final class ChatInteractor {

private let chatService: ChatService

init(chatService: ChatService) {
self.chatService = chatService
}

func getCurrentAccount() async -> String {
return await chatService.getAuthorAccount()
}

func getMessages(topic: String) -> MessageStream {
return chatService.getMessages(topic: topic)
}

func sendMessage(text: String) async throws {
try await chatService.sendMessage(text: text)
}
}
18 changes: 18 additions & 0 deletions Example/Showcase/Classes/PresentationLayer/Chat/ChatModule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import SwiftUI

final class ChatModule {

@discardableResult
static func create(app: Application) -> UIViewController {
let router = ChatRouter(app: app)
let interactor = ChatInteractor(chatService: app.chatService)
let presenter = ChatPresenter(topic: "", interactor: interactor, router: router)
let view = ChatView().environmentObject(presenter)
let viewController = SceneViewController(viewModel: presenter, content: view)

router.viewController = viewController

return viewController
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import UIKit
import Combine

final class ChatPresenter: ObservableObject {

private let topic: String
private let interactor: ChatInteractor
private let router: ChatRouter
private var disposeBag = Set<AnyCancellable>()

@Published var messages: [MessageViewModel] = []
@Published var input: String = .empty

init(topic: String, interactor: ChatInteractor, router: ChatRouter) {
self.topic = topic
self.interactor = interactor
self.router = router
}

@MainActor
func setupInitialState() async {
let account = await interactor.getCurrentAccount()

for await messages in interactor.getMessages(topic: topic) {
self.messages = messages
.sorted(by: { $0.timestamp < $1.timestamp })
.map { MessageViewModel(message: $0, currentAccount: account) }
}
}

func didPressSend() {
sendMessage()
}
}

// MARK: SceneViewModel

extension ChatPresenter: SceneViewModel {

var sceneTitle: String? {
return "Chat"
}
}

// MARK: Privates

private extension ChatPresenter {

func sendMessage() {
Task {
try! await interactor.sendMessage(text: input)
input = .empty
}
}
}
12 changes: 12 additions & 0 deletions Example/Showcase/Classes/PresentationLayer/Chat/ChatRouter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import UIKit

final class ChatRouter {

weak var viewController: UIViewController!

private let app: Application

init(app: Application) {
self.app = app
}
}
41 changes: 41 additions & 0 deletions Example/Showcase/Classes/PresentationLayer/Chat/ChatView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import SwiftUI

struct ChatView: View {

@EnvironmentObject var presenter: ChatPresenter

var body: some View {
ZStack {
ChatScrollView {
// TODO: Replace id
ForEach(presenter.messages, id: \.text) { message in
MessageView(message: message)
}

Spacer().frame(height: 72)
}

VStack {
Spacer()

HStack {
InputView(title: "Message...", text: $presenter.input) {
presenter.didPressSend()
}
.padding(16.0)
}
}
}
.task {
await presenter.setupInitialState()
}
}
}

#if DEBUG
struct ChatView_Previews: PreviewProvider {
static var previews: some View {
ChatView()
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

// TODO: After Chat SDK integration
struct Message: Codable, Equatable {
let message: String
let authorAccount: String
let timestamp: Int64
}

struct MessageViewModel {
private let message: Message
private let currentAccount: String

init(message: Message, currentAccount: String) {
self.message = message
self.currentAccount = currentAccount
}

var isCurrentUser: Bool {
return currentAccount == message.authorAccount
}

var text: String {
return message.message
}

var showAvatar: Bool {
return !isCurrentUser
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import SwiftUI

struct ChatScrollView<Content>: View where Content: View {

@ViewBuilder let content: () -> Content

var body: some View {
ScrollView(showsIndicators: false) {
VStack(alignment: .leading, spacing: 12) {
Spacer()
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)

content()
}
.rotationEffect(Angle(degrees: 180))
}
.rotationEffect(Angle(degrees: 180))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import SwiftUI

struct ContentMessageView: View {

let text: String
let isCurrentUser: Bool

var body: some View {
Text(text)
.font(.body)
.padding(.horizontal, 16.0)
.padding(.vertical, 10.0)
.foregroundColor(.white)
.background(
// TODO: Add border
overlayView
.foregroundColor(backgroundColor)
)
}

private var overlayView: some View {
return Rectangle()
.cornerRadius(22, corners: [.topLeft, .topRight])
.cornerRadius(22, corners: isCurrentUser ? .bottomLeft : .bottomRight)
.cornerRadius(4, corners: isCurrentUser ? .bottomRight : .bottomLeft)
}

private var backgroundColor: Color {
return isCurrentUser ? .w_secondaryBackground : .w_purpleBackground
}

private var borderColor: Color {
return isCurrentUser ? .w_tertiaryBackground : .w_purpleForeground
}
}
Loading