ios: fix message view updates (refactor model to make it shallow) (#254)

This commit is contained in:
Evgeny Poberezkin
2022-02-02 12:51:39 +00:00
committed by GitHub
parent 1d1ba8607e
commit 7ce305e16f
9 changed files with 153 additions and 130 deletions

View File

@@ -25,8 +25,7 @@ struct ContentView: View {
}
do {
let chats = try apiGetChats()
chatModel.chatPreviews = chats
chatModel.chats = try apiGetChats()
} catch {
print(error)
}

View File

@@ -12,29 +12,76 @@ import SwiftUI
final class ChatModel: ObservableObject {
@Published var currentUser: User?
@Published var chats: Dictionary<String, Chat> = [:]
@Published var chatPreviews: [Chat] = []
// list of chat "previews"
@Published var chats: [Chat] = []
// current chat
@Published var chatId: String?
@Published var chatItems: [ChatItem] = []
// items in the terminal view
@Published var terminalItems: [TerminalItem] = []
@Published var userAddress: String?
@Published var appOpenUrl: URL?
@Published var connectViaUrl = false
func hasChat(_ id: String) -> Bool {
chats.first(where: { $0.id == id }) != nil
}
func getChat(_ id: String) -> Chat? {
chats.first(where: { $0.id == id })
}
func addChat(_ chat: Chat) {
chats.insert(chat, at: 0)
}
func updateChatInfo(_ cInfo: ChatInfo) {
if let ix = chats.firstIndex(where: { $0.id == cInfo.id }) {
chats[ix].chatInfo = cInfo
}
}
func replaceChat(_ id: String, _ chat: Chat) {
if let ix = chats.firstIndex(where: { $0.id == id }) {
chats[ix] = chat
} else {
// invalid state, correcting
chats.insert(chat, at: 0)
}
}
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
if let ix = chats.firstIndex(where: { $0.id == cInfo.id }) {
chats[ix].chatItems = [cItem]
if chatId != cInfo.id {
let chat = chats.remove(at: ix)
chats.insert(chat, at: 0)
}
}
if chatId == cInfo.id {
chatItems.append(cItem)
}
}
func removeChat(_ id: String) {
chats.removeAll(where: { $0.id == id })
}
}
class User: Decodable {
struct User: Decodable {
var userId: Int64
var userContactId: Int64
var localDisplayName: ContactName
var profile: Profile
var activeUser: Bool
internal init(userId: Int64, userContactId: Int64, localDisplayName: ContactName, profile: Profile, activeUser: Bool) {
self.userId = userId
self.userContactId = userContactId
self.localDisplayName = localDisplayName
self.profile = profile
self.activeUser = activeUser
}
// internal init(userId: Int64, userContactId: Int64, localDisplayName: ContactName, profile: Profile, activeUser: Bool) {
// self.userId = userId
// self.userContactId = userContactId
// self.localDisplayName = localDisplayName
// self.profile = profile
// self.activeUser = activeUser
// }
}
let sampleUser = User(
@@ -103,9 +150,9 @@ enum ChatInfo: Identifiable, Decodable {
var apiId: Int64 {
get {
switch self {
case let .direct(contact): return contact.contactId
case let .group(groupInfo): return groupInfo.groupId
case let .contactRequest(contactRequest): return contactRequest.contactRequestId
case let .direct(contact): return contact.apiId
case let .group(groupInfo): return groupInfo.apiId
case let .contactRequest(contactRequest): return contactRequest.apiId
}
}
}
@@ -117,11 +164,16 @@ let sampleGroupChatInfo = ChatInfo.group(groupInfo: sampleGroupInfo)
let sampleContactRequestChatInfo = ChatInfo.contactRequest(contactRequest: sampleContactRequest)
class Chat: Decodable, Identifiable {
var chatInfo: ChatInfo
var chatItems: [ChatItem]
final class Chat: ObservableObject, Identifiable {
@Published var chatInfo: ChatInfo
@Published var chatItems: [ChatItem]
init(chatInfo: ChatInfo, chatItems: [ChatItem]) {
init(_ cData: ChatData) {
self.chatInfo = cData.chatInfo
self.chatItems = cData.chatItems
}
init(chatInfo: ChatInfo, chatItems: [ChatItem] = []) {
self.chatInfo = chatInfo
self.chatItems = chatItems
}
@@ -129,6 +181,13 @@ class Chat: Decodable, Identifiable {
var id: String { get { chatInfo.id } }
}
struct ChatData: Decodable, Identifiable {
var chatInfo: ChatInfo
var chatItems: [ChatItem]
var id: String { get { chatInfo.id } }
}
struct Contact: Identifiable, Decodable {
var contactId: Int64
var localDisplayName: ContactName
@@ -137,7 +196,7 @@ struct Contact: Identifiable, Decodable {
var viaGroup: Int64?
var id: String { get { "@\(contactId)" } }
var apiId: Int64 { get { contactId } }
var connected: Bool { get { activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" } }
}
@@ -160,6 +219,8 @@ struct UserContactRequest: Decodable {
var profile: Profile
var id: String { get { "<@\(contactRequestId)" } }
var apiId: Int64 { get { contactRequestId } }
}
let sampleContactRequest = UserContactRequest(
@@ -174,6 +235,8 @@ struct GroupInfo: Identifiable, Decodable {
var groupProfile: GroupProfile
var id: String { get { "#\(groupId)" } }
var apiId: Int64 { get { groupId } }
}
let sampleGroupInfo = GroupInfo(

View File

@@ -69,8 +69,8 @@ struct APIResponse: Decodable {
enum ChatResponse: Decodable, Error {
case response(type: String, json: String)
case apiChats(chats: [Chat])
case apiChat(chat: Chat)
case apiChats(chats: [ChatData])
case apiChat(chat: ChatData)
case invitation(connReqInvitation: String)
case sentConfirmation
case sentInvitation
@@ -210,13 +210,13 @@ func chatRecvMsg() throws -> ChatResponse {
func apiGetChats() throws -> [Chat] {
let r = try chatSendCmd(.apiGetChats)
if case let .apiChats(chats) = r { return chats }
if case let .apiChats(chats) = r { return chats.map { Chat.init($0) } }
throw r
}
func apiGetChat(type: ChatType, id: Int64) throws -> Chat {
let r = try chatSendCmd(.apiGetChat(type: type, id: id))
if case let .apiChat(chat) = r { return chat }
if case let .apiChat(chat) = r { return Chat.init(chat) }
throw r
}
@@ -296,27 +296,19 @@ func processReceivedMsg(_ chatModel: ChatModel, _ res: ChatResponse) {
chatModel.terminalItems.append(.resp(Date.now, res))
switch res {
case let .contactConnected(contact):
if let chat = chatModel.chats[contact.id] {
chat.chatInfo = ChatInfo.direct(contact: contact)
let cInfo = ChatInfo.direct(contact: contact)
if chatModel.hasChat(contact.id) {
chatModel.updateChatInfo(cInfo)
} else {
let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: [])
chatModel.chats[contact.id] = chat
chatModel.chatPreviews.insert(chat, at: 0)
chatModel.addChat(Chat(chatInfo: cInfo, chatItems: []))
}
case let .receivedContactRequest(contactRequest):
let chat = Chat(chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest), chatItems: [])
chatModel.chats[contactRequest.id] = chat
chatModel.chatPreviews.insert(chat, at: 0)
chatModel.addChat(Chat(
chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest),
chatItems: []
))
case let .newChatItem(aChatItem):
let ci = aChatItem.chatInfo
let chat = chatModel.chats[ci.id] ?? Chat(chatInfo: ci, chatItems: [])
chatModel.chats[ci.id] = chat
chat.chatItems.append(aChatItem.chatItem)
if let cp = chatModel.chatPreviews.first(where: { $0.id == ci.id } ) {
cp.chatItems = [aChatItem.chatItem]
} else {
chatModel.chatPreviews.insert(Chat(chatInfo: ci, chatItems: [aChatItem.chatItem]), at: 0)
}
chatModel.addChatItem(aChatItem.chatInfo, aChatItem.chatItem)
default:
print("unsupported response: ", res.responseType)
}

View File

@@ -10,9 +10,7 @@ import SwiftUI
struct ChatListNavLink: View {
@EnvironmentObject var chatModel: ChatModel
@Binding var chatId: String?
@State var chatPreview: Chat
@State var chat: Chat
var width: CGFloat
@State private var showDeleteContactAlert = false
@@ -24,7 +22,7 @@ struct ChatListNavLink: View {
@State private var alertContactRequest: UserContactRequest?
var body: some View {
switch chatPreview.chatInfo {
switch chat.chatInfo {
case let .direct(contact):
contactNavLink(contact)
case let .group(groupInfo):
@@ -36,15 +34,15 @@ struct ChatListNavLink: View {
private func chatView() -> some View {
ChatView(
chatId: $chatId,
chatInfo: chatPreview.chatInfo,
chatInfo: chat.chatInfo,
width: width
)
.onAppear {
do {
let ci = chatPreview.chatInfo
let chat = try apiGetChat(type: ci.chatType, id: ci.apiId)
chatModel.chats[ci.id] = chat
let cInfo = chat.chatInfo
let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId)
chatModel.updateChatInfo(chat.chatInfo)
chatModel.chatItems = chat.chatItems
} catch {
print("apiGetChatItems", error)
}
@@ -53,10 +51,10 @@ struct ChatListNavLink: View {
private func contactNavLink(_ contact: Contact) -> some View {
NavigationLink(
tag: chatPreview.chatInfo.id,
selection: $chatId,
tag: chat.chatInfo.id,
selection: $chatModel.chatId,
destination: { chatView() },
label: { ChatPreviewView(chatPreview: chatPreview) }
label: { ChatPreviewView(chat: chat) }
)
.disabled(!contact.connected)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
@@ -75,10 +73,10 @@ struct ChatListNavLink: View {
private func groupNavLink(_ groupInfo: GroupInfo) -> some View {
NavigationLink(
tag: chatPreview.chatInfo.id,
selection: $chatId,
tag: chat.chatInfo.id,
selection: $chatModel.chatId,
destination: { chatView() },
label: { ChatPreviewView(chatPreview: chatPreview) }
label: { ChatPreviewView(chat: chat) }
)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
@@ -95,7 +93,7 @@ struct ChatListNavLink: View {
}
private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View {
ChatPreviewView(chatPreview: chatPreview)
ChatPreviewView(chat: chat)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button { acceptContactRequest(contactRequest) }
label: { Label("Accept", systemImage: "checkmark") }
@@ -125,9 +123,8 @@ struct ChatListNavLink: View {
message: Text("Contact and all messages will be deleted"),
primaryButton: .destructive(Text("Delete")) {
do {
try apiDeleteChat(type: .direct, id: contact.contactId)
chatModel.chats.removeValue(forKey: contact.id)
chatModel.chatPreviews.removeAll(where: { $0.id == contact.id })
try apiDeleteChat(type: .direct, id: contact.apiId)
chatModel.removeChat(contact.id)
} catch let error {
print("Error: \(error)")
}
@@ -160,15 +157,9 @@ struct ChatListNavLink: View {
private func acceptContactRequest(_ contactRequest: UserContactRequest) {
do {
let contact = try apiAcceptContactRequest(contactReqId: contactRequest.contactRequestId)
chatModel.chats.removeValue(forKey: contactRequest.id)
let contact = try apiAcceptContactRequest(contactReqId: contactRequest.apiId)
let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: [])
chatModel.chats[contact.id] = chat
if let i = chatModel.chatPreviews.firstIndex(where: { $0.id == contactRequest.id }) {
chatModel.chatPreviews[i] = chat
} else {
chatModel.chatPreviews.insert(chat, at: 0)
}
chatModel.replaceChat(contactRequest.id, chat)
} catch let error {
print("Error: \(error)")
}
@@ -176,9 +167,8 @@ struct ChatListNavLink: View {
private func rejectContactRequest(_ contactRequest: UserContactRequest) {
do {
try apiRejectContactRequest(contactReqId: contactRequest.contactRequestId)
chatModel.chats.removeValue(forKey: contactRequest.id)
chatModel.chatPreviews.removeAll(where: { $0.id == contactRequest.id })
try apiRejectContactRequest(contactReqId: contactRequest.apiId)
chatModel.removeChat(contactRequest.id)
} catch let error {
print("Error: \(error)")
}
@@ -189,15 +179,15 @@ struct ChatListNavLink_Previews: PreviewProvider {
static var previews: some View {
@State var chatId: String? = "@1"
return Group {
ChatListNavLink(chatId: $chatId, chatPreview: Chat(
ChatListNavLink(chat: Chat(
chatInfo: sampleDirectChatInfo,
chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")]
), width: 300)
ChatListNavLink(chatId: $chatId, chatPreview: Chat(
ChatListNavLink(chat: Chat(
chatInfo: sampleDirectChatInfo,
chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")]
), width: 300)
ChatListNavLink(chatId: $chatId, chatPreview: Chat(
ChatListNavLink(chat: Chat(
chatInfo: sampleContactRequestChatInfo,
chatItems: []
), width: 300)

View File

@@ -10,7 +10,6 @@ import SwiftUI
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
@State private var chatId: String?
@State private var connectAlert = false
@State private var connectError: Error?
@@ -34,10 +33,9 @@ struct ChatListView: View {
Text("Terminal")
}
ForEach(chatModel.chatPreviews) { chatPreview in
ForEach(chatModel.chats) { chat in
ChatListNavLink(
chatId: $chatId,
chatPreview: chatPreview,
chat: chat,
width: geometry.size.width
)
}
@@ -95,7 +93,7 @@ struct ChatListView: View {
struct ChatListView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.chatPreviews = [
chatModel.chats = [
Chat(
chatInfo: sampleDirectChatInfo,
chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")]

View File

@@ -9,21 +9,21 @@
import SwiftUI
struct ChatPreviewView: View {
var chatPreview: Chat
@ObservedObject var chat: Chat
var body: some View {
let ci = chatPreview.chatItems.last
let cItem = chat.chatItems.last
return VStack(spacing: 4) {
HStack(alignment: .top) {
Text(chatPreview.chatInfo.localDisplayName)
Text(chat.chatInfo.localDisplayName)
.font(.title3)
.fontWeight(.bold)
.padding(.leading, 8)
.padding(.top, 4)
.frame(maxHeight: .infinity, alignment: .topLeading)
Spacer()
if let ci = ci {
Text(getDateFormatter().string(from: ci.meta.itemTs))
if let cItem = cItem {
Text(getDateFormatter().string(from: cItem.meta.itemTs))
.font(.subheadline)
.padding(.trailing, 8)
.padding(.top, 4)
@@ -31,8 +31,8 @@ struct ChatPreviewView: View {
.foregroundColor(.secondary)
}
}
if let ci = ci {
Text(ci.content.text)
if let cItem = cItem {
Text(cItem.content.text)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
.padding([.leading, .trailing], 8)
.padding(.bottom, 4)
@@ -52,15 +52,15 @@ struct ChatPreviewView: View {
struct ChatPreviewView_Previews: PreviewProvider {
static var previews: some View {
Group{
ChatPreviewView(chatPreview: Chat(
ChatPreviewView(chat: Chat(
chatInfo: sampleDirectChatInfo,
chatItems: []
))
ChatPreviewView(chatPreview: Chat(
ChatPreviewView(chat: Chat(
chatInfo: sampleDirectChatInfo,
chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")]
))
ChatPreviewView(chatPreview: Chat(
ChatPreviewView(chat: Chat(
chatInfo: sampleGroupChatInfo,
chatItems: [chatItemSample(1, .directSnd, Date.now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")]
))

View File

@@ -10,23 +10,18 @@ import SwiftUI
struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel
@Binding var chatId: String?
var chatInfo: ChatInfo
var width: CGFloat
@State private var inProgress: Bool = false
var body: some View {
VStack {
if let chat: Chat = chatModel.chats[chatInfo.id] {
ScrollView {
LazyVStack(spacing: 5) {
ForEach(chat.chatItems) {
ChatItemView(chatItem: $0)
}
ScrollView {
LazyVStack(spacing: 5) {
ForEach(chatModel.chatItems) {
ChatItemView(chatItem: $0)
}
}
} else {
Text("unexpected: chat not found...")
}
Spacer(minLength: 0)
@@ -35,7 +30,9 @@ struct ChatView: View {
}
.toolbar {
HStack {
Button { chatId = nil } label: { Image(systemName: "chevron.backward") }
Button { chatModel.chatId = nil } label: {
Image(systemName: "chevron.backward")
}
Spacer()
Text(chatInfo.localDisplayName)
.font(.title3)
@@ -52,9 +49,7 @@ struct ChatView: View {
func sendMessage(_ msg: String) {
do {
let chatItem = try apiSendMessage(type: chatInfo.chatType, id: chatInfo.apiId, msg: .text(msg))
let chat = chatModel.chats[chatInfo.id] ?? Chat(chatInfo: chatInfo, chatItems: [])
chatModel.chats[chatInfo.id] = chat
chat.chatItems.append(chatItem)
chatModel.addChatItem(chatInfo, chatItem)
} catch {
print(error)
}
@@ -63,23 +58,18 @@ struct ChatView: View {
struct ChatView_Previews: PreviewProvider {
static var previews: some View {
@State var chatId: String? = "@1"
let chatModel = ChatModel()
chatModel.chats = [
"@1": Chat(
chatInfo: sampleDirectChatInfo,
chatItems: [
chatItemSample(1, .directSnd, Date.now, "hello"),
chatItemSample(2, .directRcv, Date.now, "hi"),
chatItemSample(3, .directRcv, Date.now, "hi there"),
chatItemSample(4, .directRcv, Date.now, "hello again"),
chatItemSample(5, .directSnd, Date.now, "hi there!!!"),
chatItemSample(6, .directSnd, Date.now, "how are you?"),
chatItemSample(7, .directSnd, Date.now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
]
)
chatModel.chatId = "@1"
chatModel.chatItems = [
chatItemSample(1, .directSnd, Date.now, "hello"),
chatItemSample(2, .directRcv, Date.now, "hi"),
chatItemSample(3, .directRcv, Date.now, "hi there"),
chatItemSample(4, .directRcv, Date.now, "hello again"),
chatItemSample(5, .directSnd, Date.now, "hi there!!!"),
chatItemSample(6, .directSnd, Date.now, "how are you?"),
chatItemSample(7, .directSnd, Date.now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
]
return ChatView(chatId: $chatId, chatInfo: sampleDirectChatInfo, width: 300)
return ChatView(chatInfo: sampleDirectChatInfo, width: 300)
.environmentObject(chatModel)
}
}

View File

@@ -9,7 +9,6 @@
import SwiftUI
struct ChatListToolbar: View {
@EnvironmentObject var chatModel: ChatModel
var width: CGFloat
var body: some View {
@@ -28,15 +27,7 @@ struct ChatListToolbar: View {
struct ChatListToolbar_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.chats = [
"@1": Chat(
chatInfo: sampleDirectChatInfo,
chatItems: [chatItemSample(1, .directSnd, Date.now, "hello")]
)
]
return ChatListToolbar(width: 300)
.previewLayout(.fixed(width: 300, height: 70))
.environmentObject(chatModel)
}
}

View File

@@ -34,7 +34,7 @@ struct UserProfile: View {
.padding(.bottom)
HStack(spacing: 20) {
Button("Cancel") { editProfile = false }
Button("Save (and notify contacts)") { saveProfile(user) }
Button("Save (and notify contacts)") { saveProfile() }
}
}
.frame(maxWidth: .infinity, minHeight: 120, alignment: .leading)
@@ -63,10 +63,10 @@ struct UserProfile: View {
.padding()
}
func saveProfile(_ user: User) {
func saveProfile() {
do {
if let newProfile = try apiUpdateProfile(profile: profile) {
user.profile = newProfile
chatModel.currentUser?.profile = newProfile
profile = newProfile
}
} catch {