mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
core, iOS: hidden and muted user profiles (#2025)
* core, ios: profile privacy design * migration * core: user profile privacy * update nix dependencies * update simplexmq * import stateTVar * update core library * update UI * update hide/show user profile * update API, UI, fix test * update api, UI, test * update api call * fix api * update UI for hidden profiles * filter notifications on hidden/muted profiles when inactive, alerts * updates * update schema, test, icon
This commit is contained in:
committed by
GitHub
parent
bcdf502ce6
commit
06a0dbd0f2
@@ -67,6 +67,31 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
|
||||
|
||||
func getUser(_ userId: Int64) -> User? {
|
||||
currentUser?.userId == userId
|
||||
? currentUser
|
||||
: users.first { $0.user.userId == userId }?.user
|
||||
}
|
||||
|
||||
func getUserIndex(_ user: User) -> Int? {
|
||||
users.firstIndex { $0.user.userId == user.userId }
|
||||
}
|
||||
|
||||
func updateUser(_ user: User) {
|
||||
if let i = getUserIndex(user) {
|
||||
users[i].user = user
|
||||
}
|
||||
if currentUser?.userId == user.userId {
|
||||
currentUser = user
|
||||
}
|
||||
}
|
||||
|
||||
func removeUser(_ user: User) {
|
||||
if let i = getUserIndex(user), users[i].user.userId != currentUser?.userId {
|
||||
users.remove(at: i)
|
||||
}
|
||||
}
|
||||
|
||||
func hasChat(_ id: String) -> Bool {
|
||||
chats.first(where: { $0.id == id }) != nil
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
|
||||
if let userId = content.userInfo["userId"] as? Int64,
|
||||
userId != chatModel.currentUser?.userId {
|
||||
changeActiveUser(userId)
|
||||
changeActiveUser(userId, viewPwd: nil)
|
||||
}
|
||||
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
|
||||
let chatId = content.userInfo["chatId"] as? String {
|
||||
@@ -87,13 +87,17 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
switch content.categoryIdentifier {
|
||||
case ntfCategoryMessageReceived:
|
||||
let recent = recentInTheSameChat(content)
|
||||
if model.chatId == nil {
|
||||
let userId = content.userInfo["userId"] as? Int64
|
||||
if let userId = userId, let user = model.getUser(userId), !user.showNotifications {
|
||||
// ... inactive user with disabled notifications
|
||||
return []
|
||||
} else if model.chatId == nil {
|
||||
// in the chat list...
|
||||
if model.currentUser?.userId == (content.userInfo["userId"] as? Int64) {
|
||||
// ... of the current user
|
||||
if model.currentUser?.userId == userId {
|
||||
// ... of the active user
|
||||
return recent ? [] : [.sound, .list]
|
||||
} else {
|
||||
// ... of different user
|
||||
// ... of inactive user
|
||||
return recent ? [.banner] : [.sound, .banner, .list]
|
||||
}
|
||||
} else if model.chatId == content.targetContentIdentifier {
|
||||
|
||||
@@ -132,21 +132,56 @@ func apiCreateActiveUser(_ p: Profile) throws -> User {
|
||||
}
|
||||
|
||||
func listUsers() throws -> [UserInfo] {
|
||||
let r = chatSendCmdSync(.listUsers)
|
||||
return try listUsersResponse(chatSendCmdSync(.listUsers))
|
||||
}
|
||||
|
||||
func listUsersAsync() async throws -> [UserInfo] {
|
||||
return try listUsersResponse(await chatSendCmd(.listUsers))
|
||||
}
|
||||
|
||||
private func listUsersResponse(_ r: ChatResponse) throws -> [UserInfo] {
|
||||
if case let .usersList(users) = r {
|
||||
return users.sorted { $0.user.chatViewName.compare($1.user.chatViewName) == .orderedAscending }
|
||||
}
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetActiveUser(_ userId: Int64) throws -> User {
|
||||
let r = chatSendCmdSync(.apiSetActiveUser(userId: userId))
|
||||
func apiSetActiveUser(_ userId: Int64, viewPwd: String?) throws -> User {
|
||||
let r = chatSendCmdSync(.apiSetActiveUser(userId: userId, viewPwd: viewPwd))
|
||||
if case let .activeUser(user) = r { return user }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool) throws {
|
||||
let r = chatSendCmdSync(.apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues))
|
||||
func apiSetActiveUserAsync(_ userId: Int64, viewPwd: String?) async throws -> User {
|
||||
let r = await chatSendCmd(.apiSetActiveUser(userId: userId, viewPwd: viewPwd))
|
||||
if case let .activeUser(user) = r { return user }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User {
|
||||
try await setUserPrivacy_(.apiHideUser(userId: userId, viewPwd: viewPwd))
|
||||
}
|
||||
|
||||
func apiUnhideUser(_ userId: Int64, viewPwd: String?) async throws -> User {
|
||||
try await setUserPrivacy_(.apiUnhideUser(userId: userId, viewPwd: viewPwd))
|
||||
}
|
||||
|
||||
func apiMuteUser(_ userId: Int64, viewPwd: String?) async throws -> User {
|
||||
try await setUserPrivacy_(.apiMuteUser(userId: userId, viewPwd: viewPwd))
|
||||
}
|
||||
|
||||
func apiUnmuteUser(_ userId: Int64, viewPwd: String?) async throws -> User {
|
||||
try await setUserPrivacy_(.apiUnmuteUser(userId: userId, viewPwd: viewPwd))
|
||||
}
|
||||
|
||||
func setUserPrivacy_(_ cmd: ChatCommand) async throws -> User {
|
||||
let r = await chatSendCmd(cmd)
|
||||
if case let .userPrivacy(user) = r { return user }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) async throws {
|
||||
let r = await chatSendCmd(.apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: viewPwd))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
@@ -209,8 +244,16 @@ func apiStorageEncryption(currentKey: String = "", newKey: String = "") async th
|
||||
}
|
||||
|
||||
func apiGetChats() throws -> [ChatData] {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetChats: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetChats(userId: userId))
|
||||
let userId = try currentUserId("apiGetChats")
|
||||
return try apiChatsResponse(chatSendCmdSync(.apiGetChats(userId: userId)))
|
||||
}
|
||||
|
||||
func apiGetChatsAsync() async throws -> [ChatData] {
|
||||
let userId = try currentUserId("apiGetChats")
|
||||
return try apiChatsResponse(await chatSendCmd(.apiGetChats(userId: userId)))
|
||||
}
|
||||
|
||||
private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] {
|
||||
if case let .apiChats(_, chats) = r { return chats }
|
||||
throw r
|
||||
}
|
||||
@@ -337,19 +380,27 @@ func apiDeleteToken(token: DeviceToken) async throws {
|
||||
}
|
||||
|
||||
func getUserSMPServers() throws -> ([ServerCfg], [String]) {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getUserSMPServers: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetUserSMPServers(userId: userId))
|
||||
let userId = try currentUserId("getUserSMPServers")
|
||||
return try userSMPServersResponse(chatSendCmdSync(.apiGetUserSMPServers(userId: userId)))
|
||||
}
|
||||
|
||||
func getUserSMPServersAsync() async throws -> ([ServerCfg], [String]) {
|
||||
let userId = try currentUserId("getUserSMPServersAsync")
|
||||
return try userSMPServersResponse(await chatSendCmd(.apiGetUserSMPServers(userId: userId)))
|
||||
}
|
||||
|
||||
private func userSMPServersResponse(_ r: ChatResponse) throws -> ([ServerCfg], [String]) {
|
||||
if case let .userSMPServers(_, smpServers, presetServers) = r { return (smpServers, presetServers) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func setUserSMPServers(smpServers: [ServerCfg]) async throws {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("setUserSMPServers: no current user") }
|
||||
let userId = try currentUserId("setUserSMPServers")
|
||||
try await sendCommandOkResp(.apiSetUserSMPServers(userId: userId, smpServers: smpServers))
|
||||
}
|
||||
|
||||
func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("testSMPServer: no current user") }
|
||||
let userId = try currentUserId("testSMPServer")
|
||||
let r = await chatSendCmd(.apiTestSMPServer(userId: userId, smpServer: smpServer))
|
||||
if case let .smpTestResult(_, testFailure) = r {
|
||||
if let t = testFailure {
|
||||
@@ -361,14 +412,22 @@ func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure>
|
||||
}
|
||||
|
||||
func getChatItemTTL() throws -> ChatItemTTL {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getChatItemTTL: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetChatItemTTL(userId: userId))
|
||||
let userId = try currentUserId("getChatItemTTL")
|
||||
return try chatItemTTLResponse(chatSendCmdSync(.apiGetChatItemTTL(userId: userId)))
|
||||
}
|
||||
|
||||
func getChatItemTTLAsync() async throws -> ChatItemTTL {
|
||||
let userId = try currentUserId("getChatItemTTLAsync")
|
||||
return try chatItemTTLResponse(await chatSendCmd(.apiGetChatItemTTL(userId: userId)))
|
||||
}
|
||||
|
||||
private func chatItemTTLResponse(_ r: ChatResponse) throws -> ChatItemTTL {
|
||||
if case let .chatItemTTL(_, chatItemTTL) = r { return ChatItemTTL(chatItemTTL) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func setChatItemTTL(_ chatItemTTL: ChatItemTTL) async throws {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("setChatItemTTL: no current user") }
|
||||
let userId = try currentUserId("setChatItemTTL")
|
||||
try await sendCommandOkResp(.apiSetChatItemTTL(userId: userId, seconds: chatItemTTL.seconds))
|
||||
}
|
||||
|
||||
@@ -539,14 +598,14 @@ func clearChat(_ chat: Chat) async {
|
||||
}
|
||||
|
||||
func apiListContacts() throws -> [Contact] {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiListContacts: no current user") }
|
||||
let userId = try currentUserId("apiListContacts")
|
||||
let r = chatSendCmdSync(.apiListContacts(userId: userId))
|
||||
if case let .contactsList(_, contacts) = r { return contacts }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiUpdateProfile(profile: Profile) async throws -> Profile? {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiUpdateProfile: no current user") }
|
||||
let userId = try currentUserId("apiUpdateProfile")
|
||||
let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile))
|
||||
switch r {
|
||||
case .userProfileNoChange: return nil
|
||||
@@ -574,22 +633,30 @@ func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> Pe
|
||||
}
|
||||
|
||||
func apiCreateUserAddress() async throws -> String {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiCreateUserAddress: no current user") }
|
||||
let userId = try currentUserId("apiCreateUserAddress")
|
||||
let r = await chatSendCmd(.apiCreateMyAddress(userId: userId))
|
||||
if case let .userContactLinkCreated(_, connReq) = r { return connReq }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteUserAddress() async throws {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiDeleteUserAddress: no current user") }
|
||||
let userId = try currentUserId("apiDeleteUserAddress")
|
||||
let r = await chatSendCmd(.apiDeleteMyAddress(userId: userId))
|
||||
if case .userContactLinkDeleted = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetUserAddress() throws -> UserContactLink? {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetUserAddress: no current user") }
|
||||
let r = chatSendCmdSync(.apiShowMyAddress(userId: userId))
|
||||
let userId = try currentUserId("apiGetUserAddress")
|
||||
return try userAddressResponse(chatSendCmdSync(.apiShowMyAddress(userId: userId)))
|
||||
}
|
||||
|
||||
func apiGetUserAddressAsync() async throws -> UserContactLink? {
|
||||
let userId = try currentUserId("apiGetUserAddressAsync")
|
||||
return try userAddressResponse(await chatSendCmd(.apiShowMyAddress(userId: userId)))
|
||||
}
|
||||
|
||||
private func userAddressResponse(_ r: ChatResponse) throws -> UserContactLink? {
|
||||
switch r {
|
||||
case let .userContactLink(_, contactLink): return contactLink
|
||||
case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
|
||||
@@ -598,7 +665,7 @@ func apiGetUserAddress() throws -> UserContactLink? {
|
||||
}
|
||||
|
||||
func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("userAddressAutoAccept: no current user") }
|
||||
let userId = try currentUserId("userAddressAutoAccept")
|
||||
let r = await chatSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept))
|
||||
switch r {
|
||||
case let .userContactLinkUpdated(_, contactLink): return contactLink
|
||||
@@ -793,7 +860,7 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
|
||||
}
|
||||
|
||||
func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiNewGroup: no current user") }
|
||||
let userId = try currentUserId("apiNewGroup")
|
||||
let r = chatSendCmdSync(.apiNewGroup(userId: userId, groupProfile: p))
|
||||
if case let .groupCreated(_, groupInfo) = r { return groupInfo }
|
||||
throw r
|
||||
@@ -909,6 +976,13 @@ func apiGetVersion() throws -> CoreVersionInfo {
|
||||
throw r
|
||||
}
|
||||
|
||||
private func currentUserId(_ funcName: String) throws -> Int64 {
|
||||
if let userId = ChatModel.shared.currentUser?.userId {
|
||||
return userId
|
||||
}
|
||||
throw RuntimeError("\(funcName): no current user")
|
||||
}
|
||||
|
||||
func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool = true) throws {
|
||||
logger.debug("initializeChat")
|
||||
let m = ChatModel.shared
|
||||
@@ -958,21 +1032,38 @@ func startChat(refreshInvitations: Bool = true) throws {
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
}
|
||||
|
||||
func changeActiveUser(_ userId: Int64) {
|
||||
func changeActiveUser(_ userId: Int64, viewPwd: String?) {
|
||||
do {
|
||||
try changeActiveUser_(userId)
|
||||
try changeActiveUser_(userId, viewPwd: viewPwd)
|
||||
} catch let error {
|
||||
logger.error("Unable to set active user: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func changeActiveUser_(_ userId: Int64) throws {
|
||||
private func changeActiveUser_(_ userId: Int64, viewPwd: String?) throws {
|
||||
let m = ChatModel.shared
|
||||
m.currentUser = try apiSetActiveUser(userId)
|
||||
m.currentUser = try apiSetActiveUser(userId, viewPwd: viewPwd)
|
||||
m.users = try listUsers()
|
||||
try getUserChatData()
|
||||
}
|
||||
|
||||
func changeActiveUserAsync_(_ userId: Int64, viewPwd: String?) async throws {
|
||||
let currentUser = try await apiSetActiveUserAsync(userId, viewPwd: viewPwd)
|
||||
let users = try await listUsersAsync()
|
||||
await MainActor.run {
|
||||
let m = ChatModel.shared
|
||||
m.currentUser = currentUser
|
||||
m.users = users
|
||||
}
|
||||
try await getUserChatDataAsync()
|
||||
await MainActor.run {
|
||||
if var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) {
|
||||
invitation.user = currentUser
|
||||
activateCall(invitation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getUserChatData() throws {
|
||||
let m = ChatModel.shared
|
||||
m.userAddress = try apiGetUserAddress()
|
||||
@@ -982,6 +1073,20 @@ func getUserChatData() throws {
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
}
|
||||
|
||||
private func getUserChatDataAsync() async throws {
|
||||
let userAddress = try await apiGetUserAddressAsync()
|
||||
let servers = try await getUserSMPServersAsync()
|
||||
let chatItemTTL = try await getChatItemTTLAsync()
|
||||
let chats = try await apiGetChatsAsync()
|
||||
await MainActor.run {
|
||||
let m = ChatModel.shared
|
||||
m.userAddress = userAddress
|
||||
(m.userSMPServers, m.presetSMPServers) = servers
|
||||
m.chatItemTTL = chatItemTTL
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
}
|
||||
}
|
||||
|
||||
class ChatReceiver {
|
||||
private var receiveLoop: Task<Void, Never>?
|
||||
private var receiveMessages = true
|
||||
@@ -1050,18 +1155,18 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
m.removeChat(contact.activeConn.id)
|
||||
}
|
||||
case let .receivedContactRequest(user, contactRequest):
|
||||
if !active(user) { return }
|
||||
|
||||
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
|
||||
if m.hasChat(contactRequest.id) {
|
||||
m.updateChatInfo(cInfo)
|
||||
} else {
|
||||
m.addChat(Chat(
|
||||
chatInfo: cInfo,
|
||||
chatItems: []
|
||||
))
|
||||
NtfManager.shared.notifyContactRequest(user, contactRequest)
|
||||
if active(user) {
|
||||
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
|
||||
if m.hasChat(contactRequest.id) {
|
||||
m.updateChatInfo(cInfo)
|
||||
} else {
|
||||
m.addChat(Chat(
|
||||
chatInfo: cInfo,
|
||||
chatItems: []
|
||||
))
|
||||
}
|
||||
}
|
||||
NtfManager.shared.notifyContactRequest(user, contactRequest)
|
||||
case let .contactUpdated(user, toContact):
|
||||
if active(user) && m.hasChat(toContact.id) {
|
||||
let cInfo = ChatInfo.direct(contact: toContact)
|
||||
@@ -1304,7 +1409,7 @@ func refreshCallInvitations() throws {
|
||||
let invitation = m.callInvitations.removeValue(forKey: chatId) {
|
||||
m.ntfCallInvitationAction = nil
|
||||
CallController.shared.callAction(invitation: invitation, action: ntfAction)
|
||||
} else if let invitation = callInvitations.last {
|
||||
} else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) {
|
||||
activateCall(invitation)
|
||||
}
|
||||
}
|
||||
@@ -1317,6 +1422,7 @@ func justRefreshCallInvitations() throws -> [RcvCallInvitation] {
|
||||
}
|
||||
|
||||
func activateCall(_ callInvitation: RcvCallInvitation) {
|
||||
if !callInvitation.user.showNotifications { return }
|
||||
let m = ChatModel.shared
|
||||
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
|
||||
if let error = error {
|
||||
|
||||
Reference in New Issue
Block a user