Skip to content
Snippets Groups Projects
Commit 80b177b0 authored by Bruno Muniz's avatar Bruno Muniz :apple:
Browse files

Working on fixing the changes

parent ed0d88ce
No related branches found
No related tags found
2 merge requests!40v1.1.2b166,!38Using new database structure
Showing
with 630 additions and 541 deletions
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Integration"
BuildableName = "Integration"
BlueprintName = "Integration"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Integration"
BuildableName = "Integration"
BlueprintName = "Integration"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CLIENT_ID</key> <key>CLIENT_ID</key>
<string></string> <string>662236151640-herpu89qikpfs9m4kvbi9bs5fpdji5de.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key> <key>REVERSED_CLIENT_ID</key>
<string></string> <string>com.googleusercontent.apps.662236151640-herpu89qikpfs9m4kvbi9bs5fpdji5de</string>
<key>ANDROID_CLIENT_ID</key> <key>ANDROID_CLIENT_ID</key>
<string></string> <string>662236151640-2ughgo2dvc59dm4o39b45lbdungp2mct.apps.googleusercontent.com</string>
<key>API_KEY</key> <key>API_KEY</key>
<string></string> <string>AIzaSyCbI2yQ7pbuVSRvraqanjGcS9CDrjD7lNU</string>
<key>GCM_SENDER_ID</key> <key>GCM_SENDER_ID</key>
<string></string> <string>662236151640</string>
<key>PLIST_VERSION</key> <key>PLIST_VERSION</key>
<string></string> <string>1</string>
<key>BUNDLE_ID</key> <key>BUNDLE_ID</key>
<string></string> <string>io.xxlabs.messenger</string>
<key>PROJECT_ID</key> <key>PROJECT_ID</key>
<string></string> <string>xx-messenger-6e03e</string>
<key>STORAGE_BUCKET</key> <key>STORAGE_BUCKET</key>
<string></string> <string>xx-messenger-6e03e.appspot.com</string>
<key>IS_ADS_ENABLED</key> <key>IS_ADS_ENABLED</key>
<false/> <false></false>
<key>IS_ANALYTICS_ENABLED</key> <key>IS_ANALYTICS_ENABLED</key>
<false/> <false></false>
<key>IS_APPINVITE_ENABLED</key> <key>IS_APPINVITE_ENABLED</key>
<false/> <true></true>
<key>IS_GCM_ENABLED</key> <key>IS_GCM_ENABLED</key>
<false/> <true></true>
<key>IS_SIGNIN_ENABLED</key> <key>IS_SIGNIN_ENABLED</key>
<false/> <true></true>
<key>GOOGLE_APP_ID</key> <key>GOOGLE_APP_ID</key>
<string></string> <string>1:662236151640:ios:24badb58ab07515d8cef2d</string>
</dict> </dict>
</plist> </plist>
import UIKit import UIKit
import BackgroundTasks import BackgroundTasks
import XXModels
import Theme import Theme
import XXLogger import XXLogger
import Defaults import Defaults
...@@ -91,7 +92,11 @@ public class AppDelegate: UIResponder, UIApplicationDelegate { ...@@ -91,7 +92,11 @@ public class AppDelegate: UIResponder, UIApplicationDelegate {
guard UIApplication.shared.backgroundTimeRemaining > 9 else { guard UIApplication.shared.backgroundTimeRemaining > 9 else {
if !self.forceFailedPendingMessages { if !self.forceFailedPendingMessages {
self.forceFailedPendingMessages = true self.forceFailedPendingMessages = true
session.forceFailMessages()
// TODO: We need a Message.Assignment for status
// let query = Message.Query(status: [.sending])
// let assignment = Message.Assignments(status: .sendingFailed)
// _ = try? session.dbManager.bulkUpdateMessages(query, assignment)
} }
return return
......
...@@ -259,7 +259,7 @@ extension PushRouter { ...@@ -259,7 +259,7 @@ extension PushRouter {
} }
case .contactChat(id: let id): case .contactChat(id: let id):
if let session = try? DependencyInjection.Container.shared.resolve() as SessionType, if let session = try? DependencyInjection.Container.shared.resolve() as SessionType,
let contact = session.getContactWith(userId: id) { let contact = try? session.dbManager.fetchContacts(.init(id: [id])).first {
navigationController.setViewControllers([ navigationController.setViewControllers([
ChatListController(), ChatListController(),
SingleChatController(contact) SingleChatController(contact)
...@@ -267,7 +267,7 @@ extension PushRouter { ...@@ -267,7 +267,7 @@ extension PushRouter {
} }
case .groupChat(id: let id): case .groupChat(id: let id):
if let session = try? DependencyInjection.Container.shared.resolve() as SessionType, if let session = try? DependencyInjection.Container.shared.resolve() as SessionType,
let info = session.getGroupChatInfoWith(groupId: id) { let info = try? session.dbManager.fetchGroupInfos(.init(groupId: id)).first {
navigationController.setViewControllers([ navigationController.setViewControllers([
ChatListController(), ChatListController(),
GroupChatController(info) GroupChatController(info)
......
...@@ -33,16 +33,16 @@ public final class GroupChatController: UIViewController { ...@@ -33,16 +33,16 @@ public final class GroupChatController: UIViewController {
private let layoutDelegate = LayoutDelegate() private let layoutDelegate = LayoutDelegate()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var drawerCancellables = Set<AnyCancellable>() private var drawerCancellables = Set<AnyCancellable>()
private var sections = [ArraySection<ChatSection, ChatItem>]() private var sections = [ArraySection<ChatSection, Message>]()
private var currentInterfaceActions = SetActor<Set<InterfaceActions>, ReactionTypes>() private var currentInterfaceActions = SetActor<Set<InterfaceActions>, ReactionTypes>()
public override var canBecomeFirstResponder: Bool { true } public override var canBecomeFirstResponder: Bool { true }
public override var inputAccessoryView: UIView? { inputComponent } public override var inputAccessoryView: UIView? { inputComponent }
public init(_ info: GroupChatInfo) { public init(_ info: GroupInfo) {
let viewModel = GroupChatViewModel(info) let viewModel = GroupChatViewModel(info)
self.viewModel = viewModel self.viewModel = viewModel
self.members = .init(with: []) self.members = .init(with: info.members)
self.inputComponent = ChatInputView(store: .init( self.inputComponent = ChatInputView(store: .init(
initialState: .init(canAddAttachments: false), initialState: .init(canAddAttachments: false),
...@@ -60,7 +60,14 @@ public final class GroupChatController: UIViewController { ...@@ -60,7 +60,14 @@ public final class GroupChatController: UIViewController {
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
// header.setup(title: info.group.name, memberList: info.members.map { ($0.username, $0.photo) }) let memberList = info.members.map {
Member(
title: ($0.nickname ?? $0.username) ?? "Fetching username...",
photo: $0.photo
)
}
header.setup(title: info.group.name, memberList: memberList)
} }
public required init?(coder: NSCoder) { nil } public required init?(coder: NSCoder) { nil }
...@@ -155,7 +162,9 @@ public final class GroupChatController: UIViewController { ...@@ -155,7 +162,9 @@ public final class GroupChatController: UIViewController {
viewModel.replyPublisher viewModel.replyPublisher
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [unowned self] in inputComponent.setupReply(message: $0.text, sender: $0.sender) } .sink { [unowned self] senderTitle, messageText in
inputComponent.setupReply(message: messageText, sender: senderTitle)
}
.store(in: &cancellables) .store(in: &cancellables)
} }
...@@ -328,7 +337,7 @@ extension GroupChatController: UICollectionViewDataSource { ...@@ -328,7 +337,7 @@ extension GroupChatController: UICollectionViewDataSource {
let showRound: (String?) -> Void = viewModel.showRoundFrom(_:) let showRound: (String?) -> Void = viewModel.showRoundFrom(_:)
if item.status == .received { if item.status == .received {
if item.payload.reply != nil { if item.replyMessageId != nil {
let cell: IncomingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) let cell: IncomingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
// Bubbler.buildReplyGroup( // Bubbler.buildReplyGroup(
...@@ -356,7 +365,7 @@ extension GroupChatController: UICollectionViewDataSource { ...@@ -356,7 +365,7 @@ extension GroupChatController: UICollectionViewDataSource {
return cell return cell
} }
} else if item.status == .sendingFailed { } else if item.status == .sendingFailed {
if item.payload.reply != nil { if item.replyMessageId != nil {
let cell: OutgoingFailedGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) let cell: OutgoingFailedGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
// Bubbler.buildReplyGroup( // Bubbler.buildReplyGroup(
...@@ -383,7 +392,7 @@ extension GroupChatController: UICollectionViewDataSource { ...@@ -383,7 +392,7 @@ extension GroupChatController: UICollectionViewDataSource {
return cell return cell
} }
} else { } else {
if item.payload.reply != nil { if item.replyMessageId != nil {
let cell: OutgoingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) let cell: OutgoingGroupReplyCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
// Bubbler.buildReplyGroup( // Bubbler.buildReplyGroup(
...@@ -524,7 +533,7 @@ extension GroupChatController: UICollectionViewDelegate { ...@@ -524,7 +533,7 @@ extension GroupChatController: UICollectionViewDelegate {
let item = self.sections[indexPath.section].elements[indexPath.item] let item = self.sections[indexPath.section].elements[indexPath.item]
let copy = UIAction(title: Localized.Chat.BubbleMenu.copy, state: .off) { _ in let copy = UIAction(title: Localized.Chat.BubbleMenu.copy, state: .off) { _ in
UIPasteboard.general.string = item.payload.text UIPasteboard.general.string = item.text
} }
let reply = UIAction(title: Localized.Chat.BubbleMenu.reply, state: .off) { [weak self] _ in let reply = UIAction(title: Localized.Chat.BubbleMenu.reply, state: .off) { [weak self] _ in
......
...@@ -6,9 +6,9 @@ import XXModels ...@@ -6,9 +6,9 @@ import XXModels
final class MembersController: UIViewController { final class MembersController: UIViewController {
lazy private var stackView = UIStackView() lazy private var stackView = UIStackView()
private let members: [GroupMember] private let members: [Contact]
init(with members: [GroupMember]) { init(with members: [Contact]) {
self.members = members self.members = members
super.init(nibName: nil, bundle: nil) super.init(nibName: nil, bundle: nil)
} }
...@@ -26,16 +26,17 @@ final class MembersController: UIViewController { ...@@ -26,16 +26,17 @@ final class MembersController: UIViewController {
stackView.distribution = .fillEqually stackView.distribution = .fillEqually
view.addSubview(stackView) view.addSubview(stackView)
stackView.snp.makeConstraints { make in stackView.snp.makeConstraints {
make.top.equalToSuperview().offset(10) $0.top.equalToSuperview().offset(10)
make.left.right.equalToSuperview() $0.left.right.equalToSuperview()
make.bottom.equalTo(view.safeAreaLayoutGuide) $0.bottom.equalTo(view.safeAreaLayoutGuide)
} }
for member in members { members.forEach {
let memberView = MemberView() let memberView = MemberView()
// memberView.titleLabel.text = member.username let assignedTitle = ($0.nickname ?? $0.username) ?? "Fetching username..."
// memberView.avatarView.setupProfile(title: member.username, image: member.photo, size: .small) memberView.titleLabel.text = assignedTitle
memberView.avatarView.setupProfile(title: assignedTitle, image: $0.photo, size: .small)
stackView.addArrangedSubview(memberView) stackView.addArrangedSubview(memberView)
} }
} }
...@@ -57,24 +58,24 @@ private final class MemberView: UIView { ...@@ -57,24 +58,24 @@ private final class MemberView: UIView {
addSubview(avatarView) addSubview(avatarView)
addSubview(separatorView) addSubview(separatorView)
avatarView.snp.makeConstraints { make in avatarView.snp.makeConstraints {
make.top.equalToSuperview().offset(10) $0.top.equalToSuperview().offset(10)
make.width.height.equalTo(30) $0.width.height.equalTo(30)
make.left.equalToSuperview().offset(25) $0.left.equalToSuperview().offset(25)
make.centerY.equalToSuperview() $0.centerY.equalToSuperview()
} }
titleLabel.snp.makeConstraints { make in titleLabel.snp.makeConstraints {
make.centerY.equalTo(avatarView) $0.centerY.equalTo(avatarView)
make.left.equalTo(avatarView.snp.right).offset(14) $0.left.equalTo(avatarView.snp.right).offset(14)
make.right.lessThanOrEqualToSuperview().offset(-10) $0.right.lessThanOrEqualToSuperview().offset(-10)
} }
separatorView.snp.makeConstraints { make in separatorView.snp.makeConstraints {
make.height.equalTo(1) $0.height.equalTo(1)
make.left.equalToSuperview().offset(25) $0.left.equalToSuperview().offset(25)
make.right.equalToSuperview() $0.right.equalToSuperview()
make.bottom.equalToSuperview() $0.bottom.equalToSuperview()
} }
} }
......
...@@ -19,6 +19,10 @@ extension FlexibleSpace: CollectionCellContent { ...@@ -19,6 +19,10 @@ extension FlexibleSpace: CollectionCellContent {
func prepareForReuse() {} func prepareForReuse() {}
} }
extension Message: Differentiable {
public var differenceIdentifier: Int64 { id! }
}
public final class SingleChatController: UIViewController { public final class SingleChatController: UIViewController {
@Dependency private var hud: HUDType @Dependency private var hud: HUDType
@Dependency private var logger: XXLogger @Dependency private var logger: XXLogger
...@@ -44,7 +48,7 @@ public final class SingleChatController: UIViewController { ...@@ -44,7 +48,7 @@ public final class SingleChatController: UIViewController {
private let layoutDelegate = LayoutDelegate() private let layoutDelegate = LayoutDelegate()
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var drawerCancellables = Set<AnyCancellable>() private var drawerCancellables = Set<AnyCancellable>()
private var sections = [ArraySection<ChatSection, ChatItem>]() private var sections = [ArraySection<ChatSection, Message>]()
private var currentInterfaceActions: SetActor<Set<InterfaceActions>, ReactionTypes> = SetActor() private var currentInterfaceActions: SetActor<Set<InterfaceActions>, ReactionTypes> = SetActor()
var fileURL: URL? var fileURL: URL?
...@@ -204,7 +208,9 @@ public final class SingleChatController: UIViewController { ...@@ -204,7 +208,9 @@ public final class SingleChatController: UIViewController {
viewModel.replyPublisher viewModel.replyPublisher
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [unowned self] in inputComponent.setupReply(message: $0.text, sender: $0.sender) } .sink { [unowned self] senderTitle, messageText in
inputComponent.setupReply(message: messageText, sender: senderTitle)
}
.store(in: &cancellables) .store(in: &cancellables)
viewModel.navigation viewModel.navigation
......
import Models
import XXModels
import Foundation
import DifferenceKit
struct ChatItem: Equatable, Differentiable {
let date: Date
var uniqueId: Data?
let identity: Int64
var roundURL: String?
let payload: Payload
var status: Message.Status
var differenceIdentifier: Int64 { identity }
init(_ message: Message) {
self.identity = message.id!
self.status = message.status
self.payload = Payload(text: message.text, reply: nil)
self.roundURL = message.roundURL
self.uniqueId = message.networkId
self.date = message.date
}
}
...@@ -2,11 +2,6 @@ import Foundation ...@@ -2,11 +2,6 @@ import Foundation
import DifferenceKit import DifferenceKit
struct ChatSection: Equatable, Differentiable { struct ChatSection: Equatable, Differentiable {
// MARK: Properties
var date: Date var date: Date
// MARK: DifferenceKit
var differenceIdentifier: Date { date } var differenceIdentifier: Date { date }
} }
...@@ -15,7 +15,7 @@ enum GroupChatNavigationRoutes: Equatable { ...@@ -15,7 +15,7 @@ enum GroupChatNavigationRoutes: Equatable {
final class GroupChatViewModel { final class GroupChatViewModel {
@Dependency private var session: SessionType @Dependency private var session: SessionType
var replyPublisher: AnyPublisher<ReplyModel, Never> { var replyPublisher: AnyPublisher<(String, String), Never> {
replySubject.eraseToAnyPublisher() replySubject.eraseToAnyPublisher()
} }
...@@ -23,18 +23,17 @@ final class GroupChatViewModel { ...@@ -23,18 +23,17 @@ final class GroupChatViewModel {
routesSubject.eraseToAnyPublisher() routesSubject.eraseToAnyPublisher()
} }
let info: GroupChatInfo let info: GroupInfo
private var stagedReply: Reply? private var stagedReply: Reply?
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private let replySubject = PassthroughSubject<ReplyModel, Never>() private let replySubject = PassthroughSubject<(String, String), Never>()
private let routesSubject = PassthroughSubject<GroupChatNavigationRoutes, Never>() private let routesSubject = PassthroughSubject<GroupChatNavigationRoutes, Never>()
var messages: AnyPublisher<[ArraySection<ChatSection, ChatItem>], Never> { var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> {
session.dbManager.fetchMessagesPublisher(.init(chat: .group(info.group.id))) session.dbManager.fetchMessagesPublisher(.init(chat: .group(info.group.id)))
.assertNoFailure() .assertNoFailure()
.map { messages -> [ArraySection<ChatSection, ChatItem>] in .map { messages -> [ArraySection<ChatSection, Message>] in
let domainModels = messages.map { ChatItem($0) } let groupedByDate = Dictionary(grouping: messages) { domainModel -> Date in
let groupedByDate = Dictionary(grouping: domainModels) { domainModel -> Date in
let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date)
return Calendar.current.date(from: components)! return Calendar.current.date(from: components)!
} }
...@@ -43,14 +42,14 @@ final class GroupChatViewModel { ...@@ -43,14 +42,14 @@ final class GroupChatViewModel {
.map { .init(model: ChatSection(date: $0.key), elements: $0.value) } .map { .init(model: ChatSection(date: $0.key), elements: $0.value) }
.sorted(by: { $0.model.date < $1.model.date }) .sorted(by: { $0.model.date < $1.model.date })
} }
.map { sections -> [ArraySection<ChatSection, ChatItem>] in .map { sections -> [ArraySection<ChatSection, Message>] in
var snapshot = [ArraySection<ChatSection, ChatItem>]() var snapshot = [ArraySection<ChatSection, Message>]()
sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) }
return snapshot return snapshot
}.eraseToAnyPublisher() }.eraseToAnyPublisher()
} }
init(_ info: GroupChatInfo) { init(_ info: GroupInfo) {
self.info = info self.info = info
} }
...@@ -60,8 +59,8 @@ final class GroupChatViewModel { ...@@ -60,8 +59,8 @@ final class GroupChatViewModel {
_ = try? session.dbManager.bulkUpdateMessages(query, assignment) _ = try? session.dbManager.bulkUpdateMessages(query, assignment)
} }
func didRequestDelete(_ items: [ChatItem]) { func didRequestDelete(_ messages: [Message]) {
// try? session.dbManager.deleteMessages(.init(id: items.map(\.identity))) _ = try? session.dbManager.deleteMessages(.init(id: Set(messages.map(\.id))))
} }
func send(_ text: String) { func send(_ text: String) {
...@@ -72,8 +71,9 @@ final class GroupChatViewModel { ...@@ -72,8 +71,9 @@ final class GroupChatViewModel {
stagedReply = nil stagedReply = nil
} }
func retry(_ model: ChatItem) { func retry(_ message: Message) {
// session.retryGroupMessage(model.identity) guard let id = message.id else { return }
session.retryMessage(id)
} }
func showRoundFrom(_ roundURL: String?) { func showRoundFrom(_ roundURL: String?) {
...@@ -89,20 +89,24 @@ final class GroupChatViewModel { ...@@ -89,20 +89,24 @@ final class GroupChatViewModel {
} }
func getName(from senderId: Data) -> String { func getName(from senderId: Data) -> String {
fatalError() guard let contact = try? session.dbManager.fetchContacts(.init(id: [senderId])).first else {
// guard let member = info.members.first(where: { $0.userId == senderId }) else { return "You" } return "You"
// return member.username }
return (contact.nickname ?? contact.username) ?? "Fetching username..."
} }
func getText(from messageId: Data) -> String { func getText(from messageId: Data) -> String {
fatalError() guard let message = try? session.dbManager.fetchMessages(.init(networkId: messageId)).first else {
// session.getTextFromGroupMessage(messageId: messageId) ?? "[DELETED]" return "[DELETED]"
}
return message.text
} }
// func didRequestReply(_ model: GroupChatItem) { func didRequestReply(_ message: Message) {
// guard let messageId = model.uniqueId else { return } guard let networkId = message.networkId else { return }
// stagedReply = Reply(messageId: networkId, senderId: message.senderId)
// stagedReply = Reply(messageId: messageId, senderId: model.sender) replySubject.send((getName(from: message.senderId), message.text))
// replySubject.send(.init(text: model.payload.text, sender: getName(from: model.sender))) }
// }
} }
...@@ -11,11 +11,6 @@ import Permissions ...@@ -11,11 +11,6 @@ import Permissions
import DifferenceKit import DifferenceKit
import DependencyInjection import DependencyInjection
struct ReplyModel {
var text: String
var sender: String
}
enum SingleChatNavigationRoutes: Equatable { enum SingleChatNavigationRoutes: Equatable {
case none case none
case camera case camera
...@@ -36,22 +31,22 @@ final class SingleChatViewModel { ...@@ -36,22 +31,22 @@ final class SingleChatViewModel {
private var stagedReply: Reply? private var stagedReply: Reply?
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private let contactSubject: CurrentValueSubject<Contact, Never> private let contactSubject: CurrentValueSubject<Contact, Never>
private let replySubject = PassthroughSubject<ReplyModel, Never>() private let replySubject = PassthroughSubject<(String, String), Never>()
private let navigationRoutes = PassthroughSubject<SingleChatNavigationRoutes, Never>() private let navigationRoutes = PassthroughSubject<SingleChatNavigationRoutes, Never>()
private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, ChatItem>], Never>([]) private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, Message>], Never>([])
var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() }
private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none)
var isOnline: AnyPublisher<Bool, Never> { session.isOnline } var isOnline: AnyPublisher<Bool, Never> { session.isOnline }
var contactPublisher: AnyPublisher<Contact, Never> { contactSubject.eraseToAnyPublisher() } var contactPublisher: AnyPublisher<Contact, Never> { contactSubject.eraseToAnyPublisher() }
var replyPublisher: AnyPublisher<ReplyModel, Never> { replySubject.eraseToAnyPublisher() } var replyPublisher: AnyPublisher<(String, String), Never> { replySubject.eraseToAnyPublisher() }
var navigation: AnyPublisher<SingleChatNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } var navigation: AnyPublisher<SingleChatNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() }
var shouldDisplayEmptyView: AnyPublisher<Bool, Never> { sectionsRelay.map { $0.isEmpty }.eraseToAnyPublisher() } var shouldDisplayEmptyView: AnyPublisher<Bool, Never> { sectionsRelay.map { $0.isEmpty }.eraseToAnyPublisher() }
var messages: AnyPublisher<[ArraySection<ChatSection, ChatItem>], Never> { var messages: AnyPublisher<[ArraySection<ChatSection, Message>], Never> {
sectionsRelay.map { sections -> [ArraySection<ChatSection, ChatItem>] in sectionsRelay.map { sections -> [ArraySection<ChatSection, Message>] in
var snapshot = [ArraySection<ChatSection, ChatItem>]() var snapshot = [ArraySection<ChatSection, Message>]()
sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) }
return snapshot return snapshot
}.eraseToAnyPublisher() }.eraseToAnyPublisher()
...@@ -82,10 +77,8 @@ final class SingleChatViewModel { ...@@ -82,10 +77,8 @@ final class SingleChatViewModel {
session.dbManager.fetchMessagesPublisher(.init(chat: .direct(session.myId, contact.id))) session.dbManager.fetchMessagesPublisher(.init(chat: .direct(session.myId, contact.id)))
.assertNoFailure() .assertNoFailure()
.map { messages in .map {
let groupedByDate = Dictionary(grouping: $0) { domainModel -> Date in
let domainModels = messages.map { ChatItem($0) }
let groupedByDate = Dictionary(grouping: domainModels) { domainModel -> Date in
let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date)
return Calendar.current.date(from: components)! return Calendar.current.date(from: components)!
} }
...@@ -133,8 +126,9 @@ final class SingleChatViewModel { ...@@ -133,8 +126,9 @@ final class SingleChatViewModel {
_ = try? session.dbManager.deleteMessages(.init(chat: .direct(session.myId, contact.id))) _ = try? session.dbManager.deleteMessages(.init(chat: .direct(session.myId, contact.id)))
} }
func didRequestRetry(_ model: ChatItem) { func didRequestRetry(_ message: Message) {
session.retryMessage(model.identity) guard let id = message.id else { return }
session.retryMessage(id)
} }
func didNavigateSomewhere() { func didNavigateSomewhere() {
...@@ -167,11 +161,11 @@ final class SingleChatViewModel { ...@@ -167,11 +161,11 @@ final class SingleChatViewModel {
return false return false
} }
func didRequestCopy(_ model: ChatItem) { func didRequestCopy(_ model: Message) {
UIPasteboard.general.string = model.payload.text UIPasteboard.general.string = model.text
} }
func didRequestDeleteSingle(_ model: ChatItem) { func didRequestDeleteSingle(_ model: Message) {
didRequestDelete([model]) didRequestDelete([model])
} }
...@@ -186,17 +180,25 @@ final class SingleChatViewModel { ...@@ -186,17 +180,25 @@ final class SingleChatViewModel {
stagedReply = nil stagedReply = nil
} }
func didRequestReply(_ model: ChatItem) { func didRequestReply(_ message: Message) {
// guard let messageId = model.uniqueId else { return } let senderTitle: String = {
// if message.senderId == session.myId {
// let isIncoming = model.status == .received return "You"
// stagedReply = Reply(messageId: messageId, senderId: isIncoming ? contact.userId : session.myId) } else {
// replySubject.send(.init(text: model.payload.text, sender: isIncoming ? contact.nickname ?? contact.username : "You")) return (contact.nickname ?? contact.username) ?? "Fetching username..."
}
}()
replySubject.send((senderTitle, message.text))
stagedReply = Reply(messageId: message.networkId!, senderId: message.senderId)
} }
func getText(from messageId: Data) -> String { func getText(from messageId: Data) -> String {
fatalError() guard let message = try? session.dbManager.fetchMessages(.init(networkId: messageId)).first else {
// session.getTextFromMessage(messageId: messageId) ?? "[DELETED]" return "[DELETED]"
}
return message.text
} }
func showRoundFrom(_ roundURL: String?) { func showRoundFrom(_ roundURL: String?) {
...@@ -207,19 +209,19 @@ final class SingleChatViewModel { ...@@ -207,19 +209,19 @@ final class SingleChatViewModel {
} }
} }
func didRequestDelete(_ items: [ChatItem]) { func didRequestDelete(_ items: [Message]) {
///session.delete(messages: items.map { $0.identity }) _ = try? session.dbManager.deleteMessages(.init(id: Set(items.compactMap(\.id))))
} }
func itemWith(id: Int64) -> ChatItem? { func itemWith(id: Int64) -> Message? {
sectionsRelay.value.flatMap(\.elements).first(where: { $0.identity == id }) sectionsRelay.value.flatMap(\.elements).first(where: { $0.id == id })
} }
func getName(from senderId: Data) -> String { func getName(from senderId: Data) -> String {
senderId == session.myId ? "You" : contact.nickname ?? contact.username! senderId == session.myId ? "You" : contact.nickname ?? contact.username!
} }
func itemAt(indexPath: IndexPath) -> ChatItem? { func itemAt(indexPath: IndexPath) -> Message? {
guard sectionsRelay.value.count > indexPath.section else { return nil } guard sectionsRelay.value.count > indexPath.section else { return nil }
let items = sectionsRelay.value[indexPath.section].elements let items = sectionsRelay.value[indexPath.section].elements
......
import UIKit import UIKit
import Shared import Shared
struct Member {
let title: String
let photo: Data?
}
final class GroupHeaderView: UIView { final class GroupHeaderView: UIView {
let titleLabel = UILabel() let titleLabel = UILabel()
let containerView = UIView() let containerView = UIView()
...@@ -39,14 +44,14 @@ final class GroupHeaderView: UIView { ...@@ -39,14 +44,14 @@ final class GroupHeaderView: UIView {
required init?(coder: NSCoder) { nil } required init?(coder: NSCoder) { nil }
func setup(title: String, memberList: [(String, Data?)]) { func setup(title: String, memberList: [Member]) {
titleLabel.text = title titleLabel.text = title
for member in memberList { memberList.forEach {
let avatarView = AvatarView() let avatarView = AvatarView()
avatarView.layer.borderWidth = 3 avatarView.layer.borderWidth = 3
avatarView.layer.borderColor = UIColor.white.cgColor avatarView.layer.borderColor = UIColor.white.cgColor
avatarView.setupProfile(title: member.0, image: member.1, size: .small) avatarView.setupProfile(title: $0.title, image: $0.photo, size: .small)
avatarView.snp.makeConstraints { $0.width.height.equalTo(25.0) } avatarView.snp.makeConstraints { $0.width.height.equalTo(25.0) }
stackView.addArrangedSubview(avatarView) stackView.addArrangedSubview(avatarView)
} }
......
...@@ -3,9 +3,16 @@ import Theme ...@@ -3,9 +3,16 @@ import Theme
import Models import Models
import Shared import Shared
import Combine import Combine
import XXModels
import MenuFeature import MenuFeature
import DependencyInjection import DependencyInjection
extension Contact: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
public final class ChatListController: UIViewController { public final class ChatListController: UIViewController {
@Dependency private var coordinator: ChatListCoordinating @Dependency private var coordinator: ChatListCoordinating
@Dependency private var statusBarController: StatusBarStyleControlling @Dependency private var statusBarController: StatusBarStyleControlling
...@@ -69,10 +76,10 @@ public final class ChatListController: UIViewController { ...@@ -69,10 +76,10 @@ public final class ChatListController: UIViewController {
} }
}.store(in: &cancellables) }.store(in: &cancellables)
viewModel.badgeCountPublisher // viewModel.badgeCountPublisher
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [unowned self] in topLeftView.updateBadge($0) } // .sink { [unowned self] in topLeftView.updateBadge($0) }
.store(in: &cancellables) // .store(in: &cancellables)
topLeftView.actionPublisher topLeftView.actionPublisher
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
...@@ -115,19 +122,19 @@ public final class ChatListController: UIViewController { ...@@ -115,19 +122,19 @@ public final class ChatListController: UIViewController {
collectionView: screenView.listContainerView.collectionView collectionView: screenView.listContainerView.collectionView
) { collectionView, indexPath, contact in ) { collectionView, indexPath, contact in
let cell: ChatListRecentContactCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) let cell: ChatListRecentContactCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
cell.setup(title: contact.nickname ?? contact.username, image: contact.photo) cell.setup(title: contact.nickname ?? contact.username!, image: contact.photo)
return cell return cell
} }
screenView.listContainerView.collectionView.delegate = self screenView.listContainerView.collectionView.delegate = self
screenView.listContainerView.collectionView.dataSource = collectionDataSource screenView.listContainerView.collectionView.dataSource = collectionDataSource
viewModel.recentsPublisher // viewModel.recentsPublisher
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [unowned self] in // .sink { [unowned self] in
collectionDataSource.apply($0) // collectionDataSource.apply($0)
shouldBeShowingRecents = $0.numberOfItems > 0 // shouldBeShowingRecents = $0.numberOfItems > 0
}.store(in: &cancellables) // }.store(in: &cancellables)
} }
private func setupBindings() { private func setupBindings() {
...@@ -146,33 +153,33 @@ public final class ChatListController: UIViewController { ...@@ -146,33 +153,33 @@ public final class ChatListController: UIViewController {
screenView.searchListContainerView.emptyView.updateSearched(content: query) screenView.searchListContainerView.emptyView.updateSearched(content: query)
}.store(in: &cancellables) }.store(in: &cancellables)
Publishers.CombineLatest( // Publishers.CombineLatest(
viewModel.searchPublisher, // viewModel.searchPublisher,
screenView.searchView.textPublisher.removeDuplicates() // screenView.searchView.textPublisher.removeDuplicates()
) // )
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [unowned self] items, query in // .sink { [unowned self] items, query in
guard query.isEmpty == false else { // guard query.isEmpty == false else {
screenView.searchListContainerView.isHidden = true // screenView.searchListContainerView.isHidden = true
screenView.listContainerView.isHidden = false // screenView.listContainerView.isHidden = false
screenView.bringSubviewToFront(screenView.listContainerView) // screenView.bringSubviewToFront(screenView.listContainerView)
return // return
} // }
//
screenView.listContainerView.isHidden = true // screenView.listContainerView.isHidden = true
screenView.searchListContainerView.isHidden = false // screenView.searchListContainerView.isHidden = false
//
guard items.numberOfItems > 0 else { // guard items.numberOfItems > 0 else {
screenView.searchListContainerView.emptyView.isHidden = false // screenView.searchListContainerView.emptyView.isHidden = false
screenView.bringSubviewToFront(screenView.searchListContainerView) // screenView.bringSubviewToFront(screenView.searchListContainerView)
screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView) // screenView.searchListContainerView.bringSubviewToFront(screenView.searchListContainerView.emptyView)
return // return
} // }
//
screenView.searchListContainerView.bringSubviewToFront(searchTableController.view) // screenView.searchListContainerView.bringSubviewToFront(searchTableController.view)
screenView.searchListContainerView.emptyView.isHidden = true // screenView.searchListContainerView.emptyView.isHidden = true
} // }
.store(in: &cancellables) // .store(in: &cancellables)
screenView.searchView screenView.searchView
.isEditingPublisher .isEditingPublisher
...@@ -181,19 +188,19 @@ public final class ChatListController: UIViewController { ...@@ -181,19 +188,19 @@ public final class ChatListController: UIViewController {
.sink { [unowned self] in isEditingSearch = $0 } .sink { [unowned self] in isEditingSearch = $0 }
.store(in: &cancellables) .store(in: &cancellables)
viewModel.chatsPublisher // viewModel.chatsPublisher
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [unowned self] in // .sink { [unowned self] in
guard $0.isEmpty == false else { // guard $0.isEmpty == false else {
screenView.listContainerView.bringSubviewToFront(screenView.listContainerView.emptyView) // screenView.listContainerView.bringSubviewToFront(screenView.listContainerView.emptyView)
screenView.listContainerView.emptyView.isHidden = false // screenView.listContainerView.emptyView.isHidden = false
return // return
} // }
//
screenView.listContainerView.bringSubviewToFront(tableController.view) // screenView.listContainerView.bringSubviewToFront(tableController.view)
screenView.listContainerView.emptyView.isHidden = true // screenView.listContainerView.emptyView.isHidden = true
} // }
.store(in: &cancellables) // .store(in: &cancellables)
screenView.searchListContainerView screenView.searchListContainerView
.emptyView.searchButton .emptyView.searchButton
......
...@@ -4,16 +4,16 @@ import Models ...@@ -4,16 +4,16 @@ import Models
import Combine import Combine
import DependencyInjection import DependencyInjection
class ChatSearchListTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> { //class ChatSearchListTableViewDiffableDataSource: UITableViewDiffableDataSource<SearchSection, SearchItem> {
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { // override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch snapshot().sectionIdentifiers[section] { // switch snapshot().sectionIdentifiers[section] {
case .chats: // case .chats:
return "CHATS" // return "CHATS"
case .connections: // case .connections:
return "CONNECTIONS" // return "CONNECTIONS"
} // }
} // }
} //}
final class ChatSearchTableController: UITableViewController { final class ChatSearchTableController: UITableViewController {
@Dependency private var coordinator: ChatListCoordinating @Dependency private var coordinator: ChatListCoordinating
...@@ -21,65 +21,65 @@ final class ChatSearchTableController: UITableViewController { ...@@ -21,65 +21,65 @@ final class ChatSearchTableController: UITableViewController {
private let viewModel: ChatListViewModel private let viewModel: ChatListViewModel
private let cellHeight: CGFloat = 83.0 private let cellHeight: CGFloat = 83.0
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var tableDataSource: ChatSearchListTableViewDiffableDataSource? // private var tableDataSource: ChatSearchListTableViewDiffableDataSource?
init(_ viewModel: ChatListViewModel) { init(_ viewModel: ChatListViewModel) {
self.viewModel = viewModel self.viewModel = viewModel
super.init(style: .grouped) super.init(style: .grouped)
tableDataSource = ChatSearchListTableViewDiffableDataSource( // tableDataSource = ChatSearchListTableViewDiffableDataSource(
tableView: tableView // tableView: tableView
) { table, indexPath, item in // ) { table, indexPath, item in
let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) // let cell = table.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self)
switch item { // switch item {
case .chat(let subitem): // case .chat(let subitem):
if case .contact(let info) = subitem { // if case .contact(let info) = subitem {
cell.setupContact( // cell.setupContact(
name: info.contact.nickname ?? info.contact.username, // name: info.contact.nickname ?? info.contact.username,
image: info.contact.photo, // image: info.contact.photo,
date: Date.fromTimestamp(info.lastMessage!.timestamp), // date: Date.fromTimestamp(info.lastMessage!.timestamp),
hasUnread: info.lastMessage!.unread, // hasUnread: info.lastMessage!.unread,
preview: info.lastMessage!.payload.text // preview: info.lastMessage!.payload.text
) // )
} // }
//
if case .group(let info) = subitem { // if case .group(let info) = subitem {
let date: Date = { // let date: Date = {
guard let lastMessage = info.lastMessage else { // guard let lastMessage = info.lastMessage else {
return info.group.createdAt // return info.group.createdAt
} // }
//
return Date.fromTimestamp(lastMessage.timestamp) // return Date.fromTimestamp(lastMessage.timestamp)
}() // }()
//
let hasUnread: Bool = { // let hasUnread: Bool = {
guard let lastMessage = info.lastMessage else { // guard let lastMessage = info.lastMessage else {
return false // return false
} // }
//
return lastMessage.unread // return lastMessage.unread
}() // }()
//
cell.setupGroup( // cell.setupGroup(
name: info.group.name, // name: info.group.name,
date: date, // date: date,
preview: info.lastMessage?.payload.text, // preview: info.lastMessage?.payload.text,
hasUnread: hasUnread // hasUnread: hasUnread
) // )
} // }
//
case .connection(let contact): // case .connection(let contact):
cell.setupContact( // cell.setupContact(
name: contact.nickname ?? contact.username, // name: contact.nickname ?? contact.username!,
image: contact.photo, // image: contact.photo,
date: nil, // date: nil,
hasUnread: false, // hasUnread: false,
preview: contact.username // preview: contact.username!
) // )
} // }
//
return cell // return cell
} // }
} }
required init?(coder: NSCoder) { nil } required init?(coder: NSCoder) { nil }
...@@ -91,13 +91,13 @@ final class ChatSearchTableController: UITableViewController { ...@@ -91,13 +91,13 @@ final class ChatSearchTableController: UITableViewController {
tableView.tableFooterView = UIView() tableView.tableFooterView = UIView()
tableView.sectionIndexColor = .blue tableView.sectionIndexColor = .blue
tableView.register(ChatListCell.self) tableView.register(ChatListCell.self)
tableView.dataSource = tableDataSource // tableView.dataSource = tableDataSource
view.backgroundColor = Asset.neutralWhite.color view.backgroundColor = Asset.neutralWhite.color
viewModel.searchPublisher // viewModel.searchPublisher
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) } // .sink { [unowned self] in tableDataSource?.apply($0, animatingDifferences: false) }
.store(in: &cancellables) // .store(in: &cancellables)
} }
} }
...@@ -110,19 +110,19 @@ extension ChatSearchTableController { ...@@ -110,19 +110,19 @@ extension ChatSearchTableController {
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let item = tableDataSource?.itemIdentifier(for: indexPath) { // if let item = tableDataSource?.itemIdentifier(for: indexPath) {
switch item { // switch item {
case .chat(let chat): // case .chat(let chat):
switch chat { // switch chat {
case .contact(let info): // case .contact(let info):
guard info.contact.status == .friend else { return } // guard info.contact.status == .friend else { return }
coordinator.toSingleChat(with: info.contact, from: self) // coordinator.toSingleChat(with: info.contact, from: self)
case .group(let info): // case .group(let info):
coordinator.toGroupChat(with: info, from: self) // coordinator.toGroupChat(with: info, from: self)
} // }
case .connection(let contact): // case .connection(let contact):
coordinator.toContact(contact, from: self) // coordinator.toContact(contact, from: self)
} // }
} // }
} }
} }
...@@ -9,7 +9,7 @@ import DependencyInjection ...@@ -9,7 +9,7 @@ import DependencyInjection
final class ChatListTableController: UITableViewController { final class ChatListTableController: UITableViewController {
@Dependency private var coordinator: ChatListCoordinating @Dependency private var coordinator: ChatListCoordinating
private var rows = [Chat]() private var rows = [Any]()
private let viewModel: ChatListViewModel private let viewModel: ChatListViewModel
private let cellHeight: CGFloat = 83.0 private let cellHeight: CGFloat = 83.0
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
...@@ -31,28 +31,28 @@ final class ChatListTableController: UITableViewController { ...@@ -31,28 +31,28 @@ final class ChatListTableController: UITableViewController {
tableView.register(ChatListCell.self) tableView.register(ChatListCell.self)
tableView.tableFooterView = UIView() tableView.tableFooterView = UIView()
viewModel // viewModel
.chatsPublisher // .chatsPublisher
.receive(on: DispatchQueue.main) // .receive(on: DispatchQueue.main)
.sink { [unowned self] in // .sink { [unowned self] in
guard !self.rows.isEmpty else { // guard !self.rows.isEmpty else {
self.rows = $0 // self.rows = $0
tableView.reloadData() // tableView.reloadData()
return // return
} // }
//
self.tableView.reload( // self.tableView.reload(
using: StagedChangeset(source: self.rows, target: $0), // using: StagedChangeset(source: self.rows, target: $0),
deleteSectionsAnimation: .automatic, // deleteSectionsAnimation: .automatic,
insertSectionsAnimation: .automatic, // insertSectionsAnimation: .automatic,
reloadSectionsAnimation: .none, // reloadSectionsAnimation: .none,
deleteRowsAnimation: .automatic, // deleteRowsAnimation: .automatic,
insertRowsAnimation: .automatic, // insertRowsAnimation: .automatic,
reloadRowsAnimation: .none // reloadRowsAnimation: .none
) { [unowned self] in // ) { [unowned self] in
self.rows = $0 // self.rows = $0
} // }
}.store(in: &cancellables) // }.store(in: &cancellables)
} }
} }
...@@ -78,7 +78,7 @@ extension ChatListTableController { ...@@ -78,7 +78,7 @@ extension ChatListTableController {
let delete = UIContextualAction(style: .normal, title: nil) { [weak self] _, _, complete in let delete = UIContextualAction(style: .normal, title: nil) { [weak self] _, _, complete in
guard let self = self else { return } guard let self = self else { return }
self.didRequestDeletionOf(self.rows[indexPath.row]) // self.didRequestDeletionOf(self.rows[indexPath.row])
complete(true) complete(true)
} }
...@@ -88,13 +88,13 @@ extension ChatListTableController { ...@@ -88,13 +88,13 @@ extension ChatListTableController {
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch rows[indexPath.row] { // switch rows[indexPath.row] {
case .contact(let info): // case .contact(let info):
guard info.contact.status == .friend else { return } // guard info.contact.status == .friend else { return }
coordinator.toSingleChat(with: info.contact, from: self) // coordinator.toSingleChat(with: info.contact, from: self)
case .group(let info): // case .group(let info):
coordinator.toGroupChat(with: info, from: self) // coordinator.toGroupChat(with: info, from: self)
} // }
} }
override func tableView( override func tableView(
...@@ -102,96 +102,97 @@ extension ChatListTableController { ...@@ -102,96 +102,97 @@ extension ChatListTableController {
cellForRowAt indexPath: IndexPath cellForRowAt indexPath: IndexPath
) -> UITableViewCell { ) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self) // let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: ChatListCell.self)
//
if case .contact(let info) = rows[indexPath.row] { // if case .contact(let info) = rows[indexPath.row] {
cell.setupContact( // cell.setupContact(
name: info.contact.nickname ?? info.contact.username, // name: info.contact.nickname ?? info.contact.username,
image: info.contact.photo, // image: info.contact.photo,
date: Date.fromTimestamp(info.lastMessage!.timestamp), // date: Date.fromTimestamp(info.lastMessage!.timestamp),
hasUnread: info.lastMessage!.unread, // hasUnread: info.lastMessage!.unread,
preview: info.lastMessage!.payload.text // preview: info.lastMessage!.payload.text
) // )
} // }
//
if case .group(let info) = rows[indexPath.row] { // if case .group(let info) = rows[indexPath.row] {
let date: Date = { // let date: Date = {
guard let lastMessage = info.lastMessage else { // guard let lastMessage = info.lastMessage else {
return info.group.createdAt // return info.group.createdAt
} // }
//
return Date.fromTimestamp(lastMessage.timestamp) // return Date.fromTimestamp(lastMessage.timestamp)
}() // }()
//
let hasUnread: Bool = { // let hasUnread: Bool = {
guard let lastMessage = info.lastMessage else { // guard let lastMessage = info.lastMessage else {
return false // return false
} // }
//
return lastMessage.unread // return lastMessage.unread
}() // }()
//
cell.setupGroup( // cell.setupGroup(
name: info.group.name, // name: info.group.name,
date: date, // date: date,
preview: info.lastMessage?.payload.text, // preview: info.lastMessage?.payload.text,
hasUnread: hasUnread // hasUnread: hasUnread
) // )
} // }
//
return cell // return cell
fatalError()
} }
private func didRequestDeletionOf(_ item: Chat) { // private func didRequestDeletionOf(_ item: Chat) {
let title: String // let title: String
let subtitle: String // let subtitle: String
let actionTitle: String // let actionTitle: String
let actionClosure: () -> Void // let actionClosure: () -> Void
//
switch item { // switch item {
case .group(let info): // case .group(let info):
title = Localized.ChatList.DeleteGroup.title // title = Localized.ChatList.DeleteGroup.title
subtitle = Localized.ChatList.DeleteGroup.subtitle // subtitle = Localized.ChatList.DeleteGroup.subtitle
actionTitle = Localized.ChatList.DeleteGroup.action // actionTitle = Localized.ChatList.DeleteGroup.action
actionClosure = { [weak viewModel] in viewModel?.leave(info.group) } // actionClosure = { [weak viewModel] in viewModel?.leave(info.group) }
//
case .contact(let info): // case .contact(let info):
title = Localized.ChatList.Delete.title // title = Localized.ChatList.Delete.title
subtitle = Localized.ChatList.Delete.subtitle // subtitle = Localized.ChatList.Delete.subtitle
actionTitle = Localized.ChatList.Delete.delete // actionTitle = Localized.ChatList.Delete.delete
actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) } // actionClosure = { [weak viewModel] in viewModel?.clear(info.contact) }
} // }
//
let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red)) // let actionButton = DrawerCapsuleButton(model: .init(title: actionTitle, style: .red))
//
let drawer = DrawerController(with: [ // let drawer = DrawerController(with: [
DrawerText( // DrawerText(
font: Fonts.Mulish.bold.font(size: 26.0), // font: Fonts.Mulish.bold.font(size: 26.0),
text: title, // text: title,
color: Asset.neutralActive.color, // color: Asset.neutralActive.color,
alignment: .left, // alignment: .left,
spacingAfter: 19 // spacingAfter: 19
), // ),
DrawerText( // DrawerText(
font: Fonts.Mulish.regular.font(size: 16.0), // font: Fonts.Mulish.regular.font(size: 16.0),
text: subtitle, // text: subtitle,
color: Asset.neutralBody.color, // color: Asset.neutralBody.color,
alignment: .left, // alignment: .left,
lineHeightMultiple: 1.1, // lineHeightMultiple: 1.1,
spacingAfter: 39 // spacingAfter: 39
), // ),
actionButton // actionButton
]) // ])
//
actionButton.action.receive(on: DispatchQueue.main) // actionButton.action.receive(on: DispatchQueue.main)
.sink { // .sink {
drawer.dismiss(animated: true) { [weak self] in // drawer.dismiss(animated: true) { [weak self] in
guard let self = self else { return } // guard let self = self else { return }
self.drawerCancellables.removeAll() // self.drawerCancellables.removeAll()
actionClosure() // actionClosure()
} // }
}.store(in: &drawerCancellables) // }.store(in: &drawerCancellables)
//
coordinator.toDrawer(drawer, from: self) // coordinator.toDrawer(drawer, from: self)
} // }
} }
import UIKit import UIKit
import Shared import Shared
import Models import Models
import XXModels
import MenuFeature import MenuFeature
import ChatFeature import ChatFeature
import Presentation import Presentation
...@@ -16,7 +17,7 @@ public protocol ChatListCoordinating { ...@@ -16,7 +17,7 @@ public protocol ChatListCoordinating {
func toContact(_: Contact, from: UIViewController) func toContact(_: Contact, from: UIViewController)
func toSingleChat(with: Contact, from: UIViewController) func toSingleChat(with: Contact, from: UIViewController)
func toDrawer(_: UIViewController, from: UIViewController) func toDrawer(_: UIViewController, from: UIViewController)
func toGroupChat(with: GroupChatInfo, from: UIViewController) func toGroupChat(with: GroupInfo, from: UIViewController)
} }
public struct ChatListCoordinator: ChatListCoordinating { public struct ChatListCoordinator: ChatListCoordinating {
...@@ -31,7 +32,7 @@ public struct ChatListCoordinator: ChatListCoordinating { ...@@ -31,7 +32,7 @@ public struct ChatListCoordinator: ChatListCoordinating {
var contactsFactory: () -> UIViewController var contactsFactory: () -> UIViewController
var contactFactory: (Contact) -> UIViewController var contactFactory: (Contact) -> UIViewController
var singleChatFactory: (Contact) -> UIViewController var singleChatFactory: (Contact) -> UIViewController
var groupChatFactory: (GroupChatInfo) -> UIViewController var groupChatFactory: (GroupInfo) -> UIViewController
var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController var sideMenuFactory: (MenuItem, UIViewController) -> UIViewController
public init( public init(
...@@ -41,7 +42,7 @@ public struct ChatListCoordinator: ChatListCoordinating { ...@@ -41,7 +42,7 @@ public struct ChatListCoordinator: ChatListCoordinating {
contactsFactory: @escaping () -> UIViewController, contactsFactory: @escaping () -> UIViewController,
contactFactory: @escaping (Contact) -> UIViewController, contactFactory: @escaping (Contact) -> UIViewController,
singleChatFactory: @escaping (Contact) -> UIViewController, singleChatFactory: @escaping (Contact) -> UIViewController,
groupChatFactory: @escaping (GroupChatInfo) -> UIViewController, groupChatFactory: @escaping (GroupInfo) -> UIViewController,
sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController sideMenuFactory: @escaping (MenuItem, UIViewController) -> UIViewController
) { ) {
self.scanFactory = scanFactory self.scanFactory = scanFactory
...@@ -81,7 +82,7 @@ public extension ChatListCoordinator { ...@@ -81,7 +82,7 @@ public extension ChatListCoordinator {
pushPresenter.present(screen, from: parent) pushPresenter.present(screen, from: parent)
} }
func toGroupChat(with group: GroupChatInfo, from parent: UIViewController) { func toGroupChat(with group: GroupInfo, from parent: UIViewController) {
let screen = groupChatFactory(group) let screen = groupChatFactory(group)
pushPresenter.present(screen, from: parent) pushPresenter.present(screen, from: parent)
} }
......
import Models //import Models
import Foundation //import XXModels
import DifferenceKit //import Foundation
//import DifferenceKit
enum Chat: Equatable, Differentiable, Hashable { //
case group(GroupChatInfo) //enum Chat: Equatable, Differentiable, Hashable {
case contact(SingleChatInfo) // case group(GroupChatInfo)
// case contact(SingleChatInfo)
var differenceIdentifier: Data { //
switch self { // var differenceIdentifier: Data {
case .contact(let info): // switch self {
return info.contact.userId // case .contact(let info):
case .group(let info): // return info.contact.userId
return info.group.groupId // case .group(let info):
} // return info.group.groupId
} // }
// }
var orderingDate: Date { //
switch self { // var orderingDate: Date {
case .group(let info): // switch self {
if let lastMessage = info.lastMessage { // case .group(let info):
return Date.fromTimestamp(lastMessage.timestamp) // if let lastMessage = info.lastMessage {
} else { // return Date.fromTimestamp(lastMessage.timestamp)
return info.group.createdAt // } else {
} // return info.group.createdAt
case .contact(let info): // }
guard let lastMessage = info.lastMessage else { // case .contact(let info):
fatalError("Should have an E2E chat without a last message") // guard let lastMessage = info.lastMessage else {
} // fatalError("Should have an E2E chat without a last message")
// }
return Date.fromTimestamp(lastMessage.timestamp) //
} // return Date.fromTimestamp(lastMessage.timestamp)
} // }
} // }
//}
...@@ -3,6 +3,7 @@ import UIKit ...@@ -3,6 +3,7 @@ import UIKit
import Shared import Shared
import Models import Models
import Combine import Combine
import XXModels
import Defaults import Defaults
import Integration import Integration
import DependencyInjection import DependencyInjection
...@@ -12,13 +13,13 @@ enum SearchSection { ...@@ -12,13 +13,13 @@ enum SearchSection {
case connections case connections
} }
enum SearchItem: Equatable, Hashable { //enum SearchItem: Equatable, Hashable {
case chat(Chat) // case chat(Chat)
case connection(Contact) // case connection(Contact)
} //}
typealias RecentsSnapshot = NSDiffableDataSourceSnapshot<SectionId, Contact> typealias RecentsSnapshot = NSDiffableDataSourceSnapshot<SectionId, Contact>
typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem> //typealias SearchSnapshot = NSDiffableDataSourceSnapshot<SearchSection, SearchItem>
final class ChatListViewModel { final class ChatListViewModel {
@Dependency private var session: SessionType @Dependency private var session: SessionType
...@@ -27,96 +28,96 @@ final class ChatListViewModel { ...@@ -27,96 +28,96 @@ final class ChatListViewModel {
session.isOnline session.isOnline
} }
var chatsPublisher: AnyPublisher<[Chat], Never> { // var chatsPublisher: AnyPublisher<[Chat], Never> {
chatsSubject.eraseToAnyPublisher() // chatsSubject.eraseToAnyPublisher()
} // }
var hudPublisher: AnyPublisher<HUDStatus, Never> { var hudPublisher: AnyPublisher<HUDStatus, Never> {
hudSubject.eraseToAnyPublisher() hudSubject.eraseToAnyPublisher()
} }
var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> { // var recentsPublisher: AnyPublisher<RecentsSnapshot, Never> {
session.contacts(.isRecent).map { // session.contacts(.isRecent).map {
let section = SectionId() // let section = SectionId()
var snapshot = RecentsSnapshot() // var snapshot = RecentsSnapshot()
snapshot.appendSections([section]) // snapshot.appendSections([section])
snapshot.appendItems($0, toSection: section) // snapshot.appendItems($0, toSection: section)
return snapshot // return snapshot
}.eraseToAnyPublisher() // }.eraseToAnyPublisher()
} // }
var searchPublisher: AnyPublisher<SearchSnapshot, Never> { // var searchPublisher: AnyPublisher<SearchSnapshot, Never> {
Publishers.CombineLatest3( // Publishers.CombineLatest3(
session.contacts(.all), // session.contacts(.all),
chatsPublisher, // chatsPublisher,
searchSubject // searchSubject
.removeDuplicates() // .removeDuplicates()
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) // .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
.eraseToAnyPublisher() // .eraseToAnyPublisher()
) // )
.map { (contacts, chats, query) in // .map { (contacts, chats, query) in
let connectionItems = contacts.filter { // let connectionItems = contacts.filter {
let username = $0.username.lowercased().contains(query.lowercased()) // let username = $0.username.lowercased().contains(query.lowercased())
let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false // let nickname = $0.nickname?.lowercased().contains(query.lowercased()) ?? false
return username || nickname // return username || nickname
}.map(SearchItem.connection) // }.map(SearchItem.connection)
//
let chatItems = chats.filter { // let chatItems = chats.filter {
switch $0 { // switch $0 {
case .contact(let info): // case .contact(let info):
let username = info.contact.username.lowercased().contains(query.lowercased()) // let username = info.contact.username.lowercased().contains(query.lowercased())
let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false // let nickname = info.contact.nickname?.lowercased().contains(query.lowercased()) ?? false
let lastMessage = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false // let lastMessage = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false
return username || nickname || lastMessage // return username || nickname || lastMessage
//
case .group(let info): // case .group(let info):
let name = info.group.name.lowercased().contains(query.lowercased()) // let name = info.group.name.lowercased().contains(query.lowercased())
let last = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false // let last = info.lastMessage?.payload.text.lowercased().contains(query.lowercased()) ?? false
return name || last // return name || last
} // }
}.map(SearchItem.chat) // }.map(SearchItem.chat)
//
var snapshot = SearchSnapshot() // var snapshot = SearchSnapshot()
//
if connectionItems.count > 0 { // if connectionItems.count > 0 {
snapshot.appendSections([.connections]) // snapshot.appendSections([.connections])
snapshot.appendItems(connectionItems, toSection: .connections) // snapshot.appendItems(connectionItems, toSection: .connections)
} // }
//
if chatItems.count > 0 { // if chatItems.count > 0 {
snapshot.appendSections([.chats]) // snapshot.appendSections([.chats])
snapshot.appendItems(chatItems, toSection: .chats) // snapshot.appendItems(chatItems, toSection: .chats)
} // }
//
return snapshot // return snapshot
}.eraseToAnyPublisher() // }.eraseToAnyPublisher()
} // }
//
var badgeCountPublisher: AnyPublisher<Int, Never> { // var badgeCountPublisher: AnyPublisher<Int, Never> {
Publishers.CombineLatest( // Publishers.CombineLatest(
session.contacts(.received), // session.contacts(.received),
session.groups(.pending) // session.groups(.pending)
) // )
.map { $0.0.count + $0.1.count } // .map { $0.0.count + $0.1.count }
.eraseToAnyPublisher() // .eraseToAnyPublisher()
} // }
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private let searchSubject = CurrentValueSubject<String, Never>("") private let searchSubject = CurrentValueSubject<String, Never>("")
private let chatsSubject = CurrentValueSubject<[Chat], Never>([]) // private let chatsSubject = CurrentValueSubject<[Chat], Never>([])
private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none) private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
init() { init() {
Publishers.CombineLatest( // Publishers.CombineLatest(
session.singleChats(.all), // session.singleChats(.all),
session.groupChats(.accepted) // session.groupChats(.accepted)
).map { // ).map {
let groups = $0.1.map(Chat.group) // let groups = $0.1.map(Chat.group)
let chats = $0.0.map(Chat.contact) // let chats = $0.0.map(Chat.contact)
return (chats + groups).sorted { $0.orderingDate > $1.orderingDate } // return (chats + groups).sorted { $0.orderingDate > $1.orderingDate }
} // }
.sink { [unowned self] in chatsSubject.send($0) } // .sink { [unowned self] in chatsSubject.send($0) }
.store(in: &cancellables) // .store(in: &cancellables)
} }
func updateSearch(query: String) { func updateSearch(query: String) {
...@@ -128,7 +129,7 @@ final class ChatListViewModel { ...@@ -128,7 +129,7 @@ final class ChatListViewModel {
do { do {
try session.leave(group: group) try session.leave(group: group)
session.deleteAll(from: group) try session.dbManager.deleteMessages(.init(chat: .group(group.id)))
hudSubject.send(.none) hudSubject.send(.none)
} catch { } catch {
hudSubject.send(.error(.init(with: error))) hudSubject.send(.error(.init(with: error)))
...@@ -136,6 +137,6 @@ final class ChatListViewModel { ...@@ -136,6 +137,6 @@ final class ChatListViewModel {
} }
func clear(_ contact: Contact) { func clear(_ contact: Contact) {
session.deleteAll(from: contact) _ = try? session.dbManager.deleteMessages(.init(chat: .direct(session.myId, contact.id)))
} }
} }
...@@ -47,8 +47,9 @@ final class ContactListTableController: UITableViewController { ...@@ -47,8 +47,9 @@ final class ContactListTableController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: SmallAvatarAndTitleCell = tableView.dequeueReusableCell(forIndexPath: indexPath) let cell: SmallAvatarAndTitleCell = tableView.dequeueReusableCell(forIndexPath: indexPath)
let contact = sections[indexPath.section][indexPath.row] let contact = sections[indexPath.section][indexPath.row]
cell.titleLabel.text = contact.nickname ?? contact.username let name = (contact.nickname ?? contact.username) ?? "Fetching username..."
cell.avatarView.setupProfile(title: contact.nickname ?? contact.username, image: contact.photo, size: .medium) cell.titleLabel.text = name
cell.avatarView.setupProfile(title: name, image: contact.photo, size: .medium)
return cell return cell
} }
......
...@@ -74,7 +74,7 @@ public final class CreateGroupController: UIViewController { ...@@ -74,7 +74,7 @@ public final class CreateGroupController: UIViewController {
) { [weak viewModel] collectionView, indexPath, contact in ) { [weak viewModel] collectionView, indexPath, contact in
let cell: CreateGroupCollectionCell = collectionView.dequeueReusableCell(forIndexPath: indexPath) let cell: CreateGroupCollectionCell = collectionView.dequeueReusableCell(forIndexPath: indexPath)
cell.setup(title: contact.nickname ?? contact.username, image: contact.photo) cell.setup(title: contact.nickname ?? contact.username!, image: contact.photo)
cell.didTapRemove = { viewModel?.didSelect(contact: contact) } cell.didTapRemove = { viewModel?.didSelect(contact: contact) }
return cell return cell
...@@ -85,7 +85,7 @@ public final class CreateGroupController: UIViewController { ...@@ -85,7 +85,7 @@ public final class CreateGroupController: UIViewController {
) { [weak self] tableView, indexPath, contact in ) { [weak self] tableView, indexPath, contact in
let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SmallAvatarAndTitleCell.self) let cell = tableView.dequeueReusableCell(forIndexPath: indexPath, ofType: SmallAvatarAndTitleCell.self)
cell.titleLabel.text = contact.nickname ?? contact.username cell.titleLabel.text = contact.nickname ?? contact.username
cell.avatarView.setupProfile(title: contact.nickname ?? contact.username, image: contact.photo, size: .medium) cell.avatarView.setupProfile(title: contact.nickname ?? contact.username!, image: contact.photo, size: .medium)
if let selectedElements = self?.selectedElements, selectedElements.contains(contact) { if let selectedElements = self?.selectedElements, selectedElements.contains(contact) {
tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none)
...@@ -183,3 +183,9 @@ extension CreateGroupController: UITableViewDelegate { ...@@ -183,3 +183,9 @@ extension CreateGroupController: UITableViewDelegate {
} }
} }
} }
extension Contact: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment