ios: mark chat unread (#1237)

* ios: mark unread (wip)

* mark unread works
This commit is contained in:
Evgeny Poberezkin
2022-10-21 12:32:11 +01:00
committed by GitHub
parent 98cb1c39f2
commit 34a74da0b9
8 changed files with 101 additions and 47 deletions

View File

@@ -113,6 +113,17 @@ final class ChatModel: ObservableObject {
}
}
private func _updateChat(_ id: ChatId, _ update: @escaping (Chat) -> Void) {
if let i = getChatIndex(id) {
// we need to separately update the chat object, as it is ObservedObject,
// and chat in the list so the list view is updated...
// simply updating chats[i] replaces the object without updating the current object in the list
let chat = chats[i]
update(chat)
chats[i] = chat
}
}
func updateNetworkStatus(_ id: ChatId, _ status: Chat.NetworkStatus) {
if let i = getChatIndex(id) {
chats[i].serverInfo.networkStatus = status
@@ -257,7 +268,7 @@ final class ChatModel: ObservableObject {
func markChatItemsRead(_ cInfo: ChatInfo) {
// update preview
if let chat = getChat(cInfo.id) {
_updateChat(cInfo.id) { chat in
NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount)
chat.chatStats = ChatStats()
}
@@ -282,11 +293,11 @@ final class ChatModel: ObservableObject {
if let cItem = aboveItem {
if chatId == cInfo.id, let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
markCurrentChatRead(fromIndex: i)
if let chat = getChat(cInfo.id) {
_updateChat(cInfo.id) { chat in
var unreadBelow = 0
var j = i - 1
while j >= 0 {
if case .rcvNew = reversedChatItems[j].meta.itemStatus {
if case .rcvNew = self.reversedChatItems[j].meta.itemStatus {
unreadBelow += 1
}
j -= 1
@@ -303,6 +314,12 @@ final class ChatModel: ObservableObject {
markChatItemsRead(cInfo)
}
}
func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) {
_updateChat(cInfo.id) { chat in
chat.chatStats.unreadChat = unreadChat
}
}
func clearChat(_ cInfo: ChatInfo) {
// clear preview

View File

@@ -529,6 +529,10 @@ func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async thr
try await sendCommandOkResp(.apiChatRead(type: type, id: id, itemRange: itemRange))
}
func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
}
func receiveFile(fileId: Int64) async {
if let chatItem = await apiReceiveFile(fileId: fileId) {
DispatchQueue.main.async { chatItemSimpleUpdate(chatItem) }
@@ -634,16 +638,31 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws {
func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async {
do {
let minItemId = chat.chatStats.minUnreadItemId
let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId)
let cInfo = chat.chatInfo
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
DispatchQueue.main.async { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) }
if chat.chatStats.unreadCount > 0 {
let minItemId = chat.chatStats.minUnreadItemId
let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId)
let cInfo = chat.chatInfo
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
await MainActor.run { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) }
}
if chat.chatStats.unreadChat {
await markChatUnread(chat, unreadChat: false)
}
} catch {
logger.error("markChatRead apiChatRead error: \(responseError(error))")
}
}
func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async {
do {
let cInfo = chat.chatInfo
try await apiChatUnread(type: cInfo.chatType, id: cInfo.apiId, unreadChat: unreadChat)
await MainActor.run { ChatModel.shared.markChatUnread(cInfo, unreadChat: unreadChat) }
} catch {
logger.error("markChatUnread apiChatUnread error: \(responseError(error))")
}
}
func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
do {
logger.debug("apiMarkChatItemRead: \(cItem.id)")

View File

@@ -62,6 +62,13 @@ struct ChatView: View {
.navigationTitle(cInfo.chatViewName)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.onAppear {
if chat.chatStats.unreadChat {
Task {
await markChatUnread(chat, unreadChat: false)
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {

View File

@@ -53,9 +53,7 @@ struct ChatListNavLink: View {
disabled: !contact.ready
)
.swipeActions(edge: .leading, allowsFullSwipe: true) {
if chat.chatStats.unreadCount > 0 {
markReadButton()
}
markReadButton()
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if !chat.chatItems.isEmpty {
@@ -116,9 +114,7 @@ struct ChatListNavLink: View {
)
.frame(height: rowHeights[dynamicTypeSize])
.swipeActions(edge: .leading, allowsFullSwipe: true) {
if chat.chatStats.unreadCount > 0 {
markReadButton()
}
markReadButton()
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if !chat.chatItems.isEmpty {
@@ -150,13 +146,23 @@ struct ChatListNavLink: View {
.tint(chat.chatInfo.incognito ? .indigo : .accentColor)
}
private func markReadButton() -> some View {
Button {
Task { await markChatRead(chat) }
} label: {
Label("Read", systemImage: "checkmark")
@ViewBuilder private func markReadButton() -> some View {
if chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat {
Button {
Task { await markChatRead(chat) }
} label: {
Label("Read", systemImage: "checkmark")
}
.tint(Color.accentColor)
} else {
Button {
Task { await markChatUnread(chat) }
} label: {
Label("Unread", systemImage: "circlebadge.fill")
}
.tint(Color.accentColor)
}
.tint(Color.accentColor)
}
private func clearChatButton() -> some View {

View File

@@ -17,7 +17,6 @@ struct ChatPreviewView: View {
var body: some View {
let cItem = chat.chatItems.last
let unread = chat.chatStats.unreadCount
return HStack(spacing: 8) {
ZStack(alignment: .bottomTrailing) {
ChatInfoImage(chat: chat)
@@ -41,7 +40,7 @@ struct ChatPreviewView: View {
.padding(.horizontal, 8)
ZStack(alignment: .topTrailing) {
chatPreviewText(cItem, unread)
chatPreviewText(cItem)
if case .direct = chat.chatInfo {
chatStatusImage()
.padding(.top, 24)
@@ -100,7 +99,7 @@ struct ChatPreviewView: View {
}
}
@ViewBuilder private func chatPreviewText(_ cItem: ChatItem?, _ unread: Int) -> some View {
@ViewBuilder private func chatPreviewText(_ cItem: ChatItem?) -> some View {
if let cItem = cItem {
ZStack(alignment: .topTrailing) {
(itemStatusMark(cItem) + messageText(cItem.text, cItem.formattedText, cItem.memberDisplayName, preview: true))
@@ -109,8 +108,9 @@ struct ChatPreviewView: View {
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.leading, 8)
.padding(.trailing, 36)
if unread > 0 {
unreadCountText(unread)
let s = chat.chatStats
if s.unreadCount > 0 || s.unreadChat {
unreadCountText(s.unreadCount)
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 4)
@@ -185,7 +185,7 @@ struct ChatPreviewView: View {
}
func unreadCountText(_ n: Int) -> Text {
Text(n > 999 ? "\(n / 1000)k" : "\(n)")
Text(n > 999 ? "\(n / 1000)k" : n > 0 ? "\(n)" : "")
}
struct ChatPreviewView_Previews: PreviewProvider {

View File

@@ -89,6 +89,11 @@
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; };
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; };
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
5CCA7DED2901E32900C8FEBA /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCA7DE82901E32800C8FEBA /* libgmp.a */; };
5CCA7DEE2901E32900C8FEBA /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCA7DE92901E32800C8FEBA /* libffi.a */; };
5CCA7DEF2901E32900C8FEBA /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCA7DEA2901E32800C8FEBA /* libgmpxx.a */; };
5CCA7DF02901E32900C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCA7DEB2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a */; };
5CCA7DF12901E32900C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCA7DEC2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a */; };
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
@@ -123,11 +128,6 @@
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; };
6448BBB028F9C5FF000D2AB9 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBAB28F9C5FF000D2AB9 /* libffi.a */; };
6448BBB128F9C5FF000D2AB9 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBAC28F9C5FF000D2AB9 /* libgmp.a */; };
6448BBB228F9C5FF000D2AB9 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBAD28F9C5FF000D2AB9 /* libgmpxx.a */; };
6448BBB328F9C5FF000D2AB9 /* libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBAE28F9C5FF000D2AB9 /* libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC-ghc8.10.7.a */; };
6448BBB428F9C5FF000D2AB9 /* libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBAF28F9C5FF000D2AB9 /* libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC.a */; };
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; };
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; };
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; };
@@ -289,6 +289,11 @@
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; };
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CCA7DE82901E32800C8FEBA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CCA7DE92901E32800C8FEBA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CCA7DEA2901E32800C8FEBA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CCA7DEB2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a"; sourceTree = "<group>"; };
5CCA7DEC2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a"; sourceTree = "<group>"; };
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -321,11 +326,6 @@
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; };
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = "<group>"; };
6448BBAB28F9C5FF000D2AB9 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
6448BBAC28F9C5FF000D2AB9 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
6448BBAD28F9C5FF000D2AB9 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
6448BBAE28F9C5FF000D2AB9 /* libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC-ghc8.10.7.a"; sourceTree = "<group>"; };
6448BBAF28F9C5FF000D2AB9 /* libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC.a"; sourceTree = "<group>"; };
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = "<group>"; };
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; };
@@ -373,12 +373,12 @@
buildActionMask = 2147483647;
files = (
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
6448BBB428F9C5FF000D2AB9 /* libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC.a in Frameworks */,
6448BBB228F9C5FF000D2AB9 /* libgmpxx.a in Frameworks */,
6448BBB028F9C5FF000D2AB9 /* libffi.a in Frameworks */,
6448BBB328F9C5FF000D2AB9 /* libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC-ghc8.10.7.a in Frameworks */,
6448BBB128F9C5FF000D2AB9 /* libgmp.a in Frameworks */,
5CCA7DF02901E32900C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a in Frameworks */,
5CCA7DEF2901E32900C8FEBA /* libgmpxx.a in Frameworks */,
5CCA7DED2901E32900C8FEBA /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5CCA7DF12901E32900C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a in Frameworks */,
5CCA7DEE2901E32900C8FEBA /* libffi.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -433,11 +433,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
6448BBAB28F9C5FF000D2AB9 /* libffi.a */,
6448BBAC28F9C5FF000D2AB9 /* libgmp.a */,
6448BBAD28F9C5FF000D2AB9 /* libgmpxx.a */,
6448BBAE28F9C5FF000D2AB9 /* libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC-ghc8.10.7.a */,
6448BBAF28F9C5FF000D2AB9 /* libHSsimplex-chat-4.1.0-KX468bxbJE45DCoSvMEVhC.a */,
5CCA7DE92901E32800C8FEBA /* libffi.a */,
5CCA7DE82901E32800C8FEBA /* libgmp.a */,
5CCA7DEA2901E32800C8FEBA /* libgmpxx.a */,
5CCA7DEB2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX-ghc8.10.7.a */,
5CCA7DEC2901E32800C8FEBA /* libHSsimplex-chat-4.1.1-YMBxN0i6bGIdLfYB8ndX.a */,
);
path = Libraries;
sourceTree = "<group>";

View File

@@ -78,6 +78,7 @@ public enum ChatCommand {
case apiGetCallInvitations
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
case receiveFile(fileId: Int64)
case string(String)
@@ -151,6 +152,7 @@ public enum ChatCommand {
case .apiGetCallInvitations: return "/_call get"
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
case let .receiveFile(fileId): return "/freceive \(fileId)"
case let .string(str): return str
}
@@ -224,6 +226,7 @@ public enum ChatCommand {
case .apiGetCallInvitations: return "apiGetCallInvitations"
case .apiCallStatus: return "apiCallStatus"
case .apiChatRead: return "apiChatRead"
case .apiChatUnread: return "apiChatUnread"
case .receiveFile: return "receiveFile"
case .string: return "console command"
}

View File

@@ -303,13 +303,15 @@ public struct ChatData: Decodable, Identifiable {
}
public struct ChatStats: Decodable {
public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0) {
public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) {
self.unreadCount = unreadCount
self.minUnreadItemId = minUnreadItemId
self.unreadChat = unreadChat
}
public var unreadCount: Int = 0
public var minUnreadItemId: Int64 = 0
public var unreadChat: Bool = false
}
public struct Contact: Identifiable, Decodable, NamedChat {