This commit is contained in:
spaced4ndy 2025-06-27 15:43:48 +00:00 committed by GitHub
commit 51b1c6aa30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
161 changed files with 7373 additions and 2982 deletions

View file

@ -437,6 +437,7 @@ struct ContentView: View {
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
planAndConnect(
link,
theme: theme,
dismiss: false
)
} else {

View file

@ -23,7 +23,7 @@ enum ChatCommand: ChatCmdProtocol {
case apiMuteUser(userId: Int64)
case apiUnmuteUser(userId: Int64)
case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?)
case startChat(mainApp: Bool, enableSndFiles: Bool)
case startChat(mainApp: Bool, enableSndFiles: Bool, largeLinkData: Bool)
case checkChatRunning
case apiStopChat
case apiActivateChat(restoreChat: Bool)
@ -76,7 +76,7 @@ enum ChatCommand: ChatCmdProtocol {
case apiLeaveGroup(groupId: Int64)
case apiListMembers(groupId: Int64)
case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile)
case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole, short: Bool)
case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole)
case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole)
case apiDeleteGroupLink(groupId: Int64)
case apiGetGroupLink(groupId: Int64)
@ -116,10 +116,16 @@ enum ChatCommand: ChatCmdProtocol {
case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64)
case apiVerifyContact(contactId: Int64, connectionCode: String?)
case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?)
case apiAddContact(userId: Int64, short: Bool, incognito: Bool)
case apiAddContact(userId: Int64, incognito: Bool)
case apiSetConnectionIncognito(connId: Int64, incognito: Bool)
case apiChangeConnectionUser(connId: Int64, userId: Int64)
case apiConnectPlan(userId: Int64, connLink: String)
case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData)
case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, groupShortLinkData: GroupShortLinkData)
case apiChangePreparedContactUser(contactId: Int64, newUserId: Int64)
case apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64)
case apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?)
case apiConnectPreparedGroup(groupId: Int64, incognito: Bool, msg: MsgContent?)
case apiConnect(userId: Int64, incognito: Bool, connLink: CreatedConnLink)
case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64)
case apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode)
@ -132,12 +138,12 @@ enum ChatCommand: ChatCmdProtocol {
case apiSetConnectionAlias(connId: Int64, localAlias: String)
case apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?)
case apiSetChatUIThemes(chatId: String, themes: ThemeModeOverrides?)
case apiCreateMyAddress(userId: Int64, short: Bool)
case apiCreateMyAddress(userId: Int64)
case apiDeleteMyAddress(userId: Int64)
case apiShowMyAddress(userId: Int64)
case apiAddMyAddressShortLink(userId: Int64)
case apiSetProfileAddress(userId: Int64, on: Bool)
case apiAddressAutoAccept(userId: Int64, autoAccept: AutoAccept?)
case apiSetAddressSettings(userId: Int64, addressSettings: AddressSettings)
case apiAcceptContact(incognito: Bool, contactReqId: Int64)
case apiRejectContact(contactReqId: Int64)
// WebRTC calls
@ -197,7 +203,7 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiMuteUser(userId): return "/_mute user \(userId)"
case let .apiUnmuteUser(userId): return "/_unmute user \(userId)"
case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))"
case let .startChat(mainApp, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))"
case let .startChat(mainApp, enableSndFiles, largeLinkData): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles)) large_link_data=\(onOff(largeLinkData))"
case .checkChatRunning: return "/_check running"
case .apiStopChat: return "/_stop"
case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))"
@ -260,7 +266,7 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)"
case let .apiListMembers(groupId): return "/_members #\(groupId)"
case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))"
case let .apiCreateGroupLink(groupId, memberRole, short): return "/_create link #\(groupId) \(memberRole) short=\(onOff(short))"
case let .apiCreateGroupLink(groupId, memberRole): return "/_create link #\(groupId) \(memberRole)"
case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)"
case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)"
case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)"
@ -310,10 +316,16 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiVerifyContact(contactId, .none): return "/_verify code @\(contactId)"
case let .apiVerifyGroupMember(groupId, groupMemberId, .some(connectionCode)): return "/_verify code #\(groupId) \(groupMemberId) \(connectionCode)"
case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)"
case let .apiAddContact(userId, short, incognito): return "/_connect \(userId) short=\(onOff(short)) incognito=\(onOff(incognito))"
case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))"
case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))"
case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)"
case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)"
case let .apiPrepareContact(userId, connLink, contactShortLinkData): return "/_prepare contact \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(contactShortLinkData))"
case let .apiPrepareGroup(userId, connLink, groupShortLinkData): return "/_prepare group \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(groupShortLinkData))"
case let .apiChangePreparedContactUser(contactId, newUserId): return "/_set contact user @\(contactId) \(newUserId)"
case let .apiChangePreparedGroupUser(groupId, newUserId): return "/_set group user #\(groupId) \(newUserId)"
case let .apiConnectPreparedContact(contactId, incognito, mc): return "/_connect contact @\(contactId) incognito=\(onOff(incognito))\(maybeContent(mc))"
case let .apiConnectPreparedGroup(groupId, incognito, mc): return "/_connect group #\(groupId) incognito=\(onOff(incognito))\(maybeContent(mc))"
case let .apiConnect(userId, incognito, connLink): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connLink.connFullLink) \(connLink.connShortLink ?? "")"
case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)"
case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id, scope: nil)) \(chatDeleteMode.cmdString)"
@ -326,12 +338,12 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiSetConnectionAlias(connId, localAlias): return "/_set alias :\(connId) \(localAlias.trimmingCharacters(in: .whitespaces))"
case let .apiSetUserUIThemes(userId, themes): return "/_set theme user \(userId) \(themes != nil ? encodeJSON(themes) : "")"
case let .apiSetChatUIThemes(chatId, themes): return "/_set theme \(chatId) \(themes != nil ? encodeJSON(themes) : "")"
case let .apiCreateMyAddress(userId, short): return "/_address \(userId) short=\(onOff(short))"
case let .apiCreateMyAddress(userId): return "/_address \(userId)"
case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)"
case let .apiShowMyAddress(userId): return "/_show_address \(userId)"
case let .apiAddMyAddressShortLink(userId): return "/_short_link_address \(userId)"
case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))"
case let .apiAddressAutoAccept(userId, autoAccept): return "/_auto_accept \(userId) \(AutoAccept.cmdString(autoAccept))"
case let .apiSetAddressSettings(userId, addressSettings): return "/_address_settings \(userId) \(encodeJSON(addressSettings))"
case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)"
case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)"
case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))"
@ -482,6 +494,12 @@ enum ChatCommand: ChatCmdProtocol {
case .apiSetConnectionIncognito: return "apiSetConnectionIncognito"
case .apiChangeConnectionUser: return "apiChangeConnectionUser"
case .apiConnectPlan: return "apiConnectPlan"
case .apiPrepareContact: return "apiPrepareContact"
case .apiPrepareGroup: return "apiPrepareGroup"
case .apiChangePreparedContactUser: return "apiChangePreparedContactUser"
case .apiChangePreparedGroupUser: return "apiChangePreparedGroupUser"
case .apiConnectPreparedContact: return "apiConnectPreparedContact"
case .apiConnectPreparedGroup: return "apiConnectPreparedGroup"
case .apiConnect: return "apiConnect"
case .apiDeleteChat: return "apiDeleteChat"
case .apiClearChat: return "apiClearChat"
@ -498,7 +516,7 @@ enum ChatCommand: ChatCmdProtocol {
case .apiShowMyAddress: return "apiShowMyAddress"
case .apiAddMyAddressShortLink: return "apiAddMyAddressShortLink"
case .apiSetProfileAddress: return "apiSetProfileAddress"
case .apiAddressAutoAccept: return "apiAddressAutoAccept"
case .apiSetAddressSettings: return "apiSetAddressSettings"
case .apiAcceptContact: return "apiAcceptContact"
case .apiRejectContact: return "apiRejectContact"
case .apiSendCallInvitation: return "apiSendCallInvitation"
@ -603,6 +621,16 @@ enum ChatCommand: ChatCmdProtocol {
private func maybePwd(_ pwd: String?) -> String {
pwd == "" || pwd == nil ? "" : " " + encodeJSON(pwd)
}
private func maybeContent(_ mc: MsgContent?) -> String {
if case let .text(s) = mc, s.isEmpty {
""
} else if let mc {
" " + mc.cmdString
} else {
""
}
}
}
// ChatResponse is split to three enums to reduce stack size used when parsing it, parsing large enums is very inefficient.
@ -729,8 +757,13 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef)
case connectionPlan(user: UserRef, connLink: CreatedConnLink, connectionPlan: ConnectionPlan)
case newPreparedChat(user: UserRef, chat: ChatData)
case contactUserChanged(user: UserRef, fromContact: Contact, newUser: UserRef, toContact: Contact)
case groupUserChanged(user: UserRef, fromGroup: GroupInfo, newUser: UserRef, toGroup: GroupInfo)
case sentConfirmation(user: UserRef, connection: PendingContactConnection)
case sentInvitation(user: UserRef, connection: PendingContactConnection)
case startedConnectionToContact(user: UserRef, contact: Contact)
case startedConnectionToGroup(user: UserRef, groupInfo: GroupInfo)
case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?)
case contactAlreadyExists(user: UserRef, contact: Contact)
case contactDeleted(user: UserRef, contact: Contact)
@ -750,7 +783,7 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case userContactLinkCreated(user: User, connLinkContact: CreatedConnLink)
case userContactLinkDeleted(user: User)
case acceptingContactRequest(user: UserRef, contact: Contact)
case contactRequestRejected(user: UserRef)
case contactRequestRejected(user: UserRef, contactRequest: UserContactRequest, contact_: Contact?)
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
case newChatItems(user: UserRef, chatItems: [AChatItem])
case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: Set<Int64>, byUser: Bool, member_: GroupMember?)
@ -768,8 +801,13 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case .connectionIncognitoUpdated: "connectionIncognitoUpdated"
case .connectionUserChanged: "connectionUserChanged"
case .connectionPlan: "connectionPlan"
case .newPreparedChat: "newPreparedChat"
case .contactUserChanged: "contactUserChanged"
case .groupUserChanged: "groupUserChanged"
case .sentConfirmation: "sentConfirmation"
case .sentInvitation: "sentInvitation"
case .startedConnectionToContact: "startedConnectionToContact"
case .startedConnectionToGroup: "startedConnectionToGroup"
case .sentInvitationToContact: "sentInvitationToContact"
case .contactAlreadyExists: "contactAlreadyExists"
case .contactDeleted: "contactDeleted"
@ -817,12 +855,12 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case let .groupAliasUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
case let .connectionAliasUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
case let .contactPrefsUpdated(u, fromContact, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))")
case let .userContactLink(u, contactLink): return withUser(u, contactLink.responseDetails)
case let .userContactLinkUpdated(u, contactLink): return withUser(u, contactLink.responseDetails)
case let .userContactLink(u, contactLink): return withUser(u, String(describing: contactLink))
case let .userContactLinkUpdated(u, contactLink): return withUser(u, String(describing: contactLink))
case let .userContactLinkCreated(u, connLink): return withUser(u, String(describing: connLink))
case .userContactLinkDeleted: return noDetails
case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact))
case .contactRequestRejected: return noDetails
case let .contactRequestRejected(u, contactRequest, contact_): return withUser(u, "contactRequest: \(String(describing: contactRequest))\ncontact_: \(String(describing: contact_))")
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
case let .newChatItems(u, chatItems):
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
@ -843,8 +881,13 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
case let .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\nnewUserId: \(String(describing: newUser.userId))")
case let .connectionPlan(u, connLink, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))")
case let .newPreparedChat(u, chat): return withUser(u, String(describing: chat))
case let .contactUserChanged(u, fromContact, newUser, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\nnewUserId: \(String(describing: newUser.userId))\ntoContact: \(String(describing: toContact))")
case let .groupUserChanged(u, fromGroup, newUser, toGroup): return withUser(u, "fromGroup: \(String(describing: fromGroup))\nnewUserId: \(String(describing: newUser.userId))\ntoGroup: \(String(describing: toGroup))")
case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection))
case let .sentInvitation(u, connection): return withUser(u, String(describing: connection))
case let .startedConnectionToContact(u, contact): return withUser(u, String(describing: contact))
case let .startedConnectionToGroup(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact))
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
}
@ -864,8 +907,8 @@ enum ChatResponse2: Decodable, ChatAPIResult {
case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole)
case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool)
case groupUpdated(user: UserRef, toGroup: GroupInfo)
case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole)
case groupLink(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole)
case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink)
case groupLink(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink)
case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo)
case newMemberContact(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember)
case newMemberContactSentInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember)
@ -960,8 +1003,8 @@ enum ChatResponse2: Decodable, ChatAPIResult {
case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)")
case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)")
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
case let .groupLinkCreated(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)")
case let .groupLink(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)")
case let .groupLinkCreated(u, groupInfo, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)")
case let .groupLink(u, groupInfo, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)")
case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .newMemberContact(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
case let .newMemberContactSentInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
@ -1004,7 +1047,7 @@ enum ChatEvent: Decodable, ChatAPIResult {
case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?)
case contactConnecting(user: UserRef, contact: Contact)
case contactSndReady(user: UserRef, contact: Contact)
case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest)
case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest, chat_: ChatData?)
case contactUpdated(user: UserRef, toContact: Contact)
case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember)
case contactsMerged(user: UserRef, intoContact: Contact, mergedContact: Contact)
@ -1155,7 +1198,7 @@ enum ChatEvent: Decodable, ChatAPIResult {
case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact))
case let .contactConnecting(u, contact): return withUser(u, String(describing: contact))
case let .contactSndReady(u, contact): return withUser(u, String(describing: contact))
case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest))
case let .receivedContactRequest(u, contactRequest, chat_): return withUser(u, "contactRequest: \(String(describing: contactRequest))\nchat_: \(String(describing: chat_))")
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)")
case let .contactsMerged(u, intoContact, mergedContact): return withUser(u, "intoContact: \(intoContact)\nmergedContact: \(mergedContact)")
@ -1261,14 +1304,14 @@ enum ConnectionPlan: Decodable, Hashable {
}
enum InvitationLinkPlan: Decodable, Hashable {
case ok
case ok(contactSLinkData_: ContactShortLinkData?)
case ownLink
case connecting(contact_: Contact?)
case known(contact: Contact)
}
enum ContactAddressPlan: Decodable, Hashable {
case ok
case ok(contactSLinkData_: ContactShortLinkData?)
case ownLink
case connectingConfirmReconnect
case connectingProhibit(contact: Contact)
@ -1277,7 +1320,7 @@ enum ContactAddressPlan: Decodable, Hashable {
}
enum GroupLinkPlan: Decodable, Hashable {
case ok
case ok(groupSLinkData_: GroupShortLinkData?)
case ownLink(groupInfo: GroupInfo)
case connectingConfirmReconnect
case connectingProhibit(groupInfo_: GroupInfo?)
@ -1371,29 +1414,27 @@ struct UserMsgReceiptSettings: Codable {
struct UserContactLink: Decodable, Hashable {
var connLinkContact: CreatedConnLink
var autoAccept: AutoAccept?
var shortLinkDataSet: Bool
var addressSettings: AddressSettings
}
var responseDetails: String {
"connLinkContact: \(connLinkContact)\nautoAccept: \(AutoAccept.cmdString(autoAccept))"
}
struct AddressSettings: Codable, Hashable {
var businessAddress: Bool
var welcomeMessage: String?
var autoAccept: AutoAccept?
var autoReply: MsgContent?
}
struct AutoAccept: Codable, Hashable {
var businessAddress: Bool
var acceptIncognito: Bool
var autoReply: MsgContent?
}
static func cmdString(_ autoAccept: AutoAccept?) -> String {
guard let autoAccept = autoAccept else { return "off" }
var s = "on"
if autoAccept.acceptIncognito {
s += " incognito=on"
} else if autoAccept.businessAddress {
s += " business"
}
guard let msg = autoAccept.autoReply else { return s }
return s + " " + msg.cmdString
}
struct GroupLink: Decodable, Hashable {
var userContactLinkId: Int64
var connLinkContact: CreatedConnLink
var shortLinkDataSet: Bool
var groupLinkId: String
var acceptMemberRole: GroupMemberRole
}
struct DeviceToken: Decodable {

View file

@ -34,7 +34,7 @@ actor TerminalItems {
await add(.cmd(start, cmd))
await addResult(res)
}
func addResult<R: ChatAPIResult>(_ res: APIResult<R>) async {
let item: TerminalItem = switch res {
case let .result(r): .res(.now, r)
@ -181,12 +181,12 @@ class PreloadState {
class ChatTagsModel: ObservableObject {
static let shared = ChatTagsModel()
@Published var userTags: [ChatTag] = []
@Published var activeFilter: ActiveFilter? = nil
@Published var presetTags: [PresetTag:Int] = [:]
@Published var unreadTags: [Int64:Int] = [:]
func updateChatTags(_ chats: [Chat]) {
let tm = ChatTagsModel.shared
var newPresetTags: [PresetTag:Int] = [:]
@ -240,13 +240,13 @@ class ChatTagsModel: ObservableObject {
}
clearActiveChatFilterIfNeeded()
}
func markChatTagRead(_ chat: Chat) -> Void {
if chat.unreadTag, let tags = chat.chatInfo.chatTags {
decTagsReadCount(tags)
}
}
func updateChatTagRead(_ chat: Chat, wasUnread: Bool) -> Void {
guard let tags = chat.chatInfo.chatTags else { return }
let nowUnread = chat.unreadTag
@ -404,6 +404,10 @@ final class ChatModel: ObservableObject {
remoteCtrlSession?.active ?? false
}
var addressShortLinkDataSet: Bool {
userAddress?.shortLinkDataSet ?? true
}
func getUser(_ userId: Int64) -> User? {
currentUser?.userId == userId
? currentUser
@ -560,8 +564,15 @@ final class ChatModel: ObservableObject {
}
}
func updateChats(_ newChats: [ChatData]) {
chats = newChats.map { Chat($0) }
func updateChats(_ newChats: [ChatData], keepingChatId: String? = nil) {
if let keepingChatId,
let chatToKeep = getChat(keepingChatId),
let i = newChats.firstIndex(where: { $0.id == keepingChatId }) {
let remainingNewChats = Array(newChats[..<i] + newChats[(i + 1)...])
chats = [chatToKeep] + remainingNewChats.map { Chat($0) }
} else {
chats = newChats.map { Chat($0) }
}
NtfManager.shared.setNtfBadgeCount(totalUnreadCountForAllUsers())
popChatCollector.clear()
}
@ -570,15 +581,18 @@ final class ChatModel: ObservableObject {
// groups[group.groupInfo.id] = group
// }
func addChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
func addChatItem(_ chatInfo: ChatInfo, _ cItem: ChatItem) {
// updates membersRequireAttention
updateChatInfo(cInfo)
// mark chat non deleted
if case let .direct(contact) = cInfo, contact.chatDeleted {
let cInfo: ChatInfo
if case let .direct(contact) = chatInfo, contact.chatDeleted {
// mark chat non deleted
var updatedContact = contact
updatedContact.chatDeleted = false
updateContact(updatedContact)
cInfo = .direct(contact: updatedContact)
} else {
cInfo = chatInfo
}
updateChatInfo(cInfo)
// update chat list
if let i = getChatIndex(cInfo.id) {
// update preview
@ -749,7 +763,7 @@ final class ChatModel: ObservableObject {
let updatedItem = removedUpdatedItem(chat.chatItems[0]) {
chat.chatItems = [updatedItem]
}
func removedUpdatedItem(_ item: ChatItem) -> ChatItem? {
let newContent: CIContent
if case .groupSnd = item.chatDir, removedMember.groupMemberId == groupInfo.membership.groupMemberId {
@ -923,7 +937,7 @@ final class ChatModel: ObservableObject {
}
let popChatCollector = PopChatCollector()
class PopChatCollector {
private let subject = PassthroughSubject<Void, Never>()
private var bag = Set<AnyCancellable>()
@ -936,7 +950,7 @@ final class ChatModel: ObservableObject {
.sink { self.popCollectedChats() }
.store(in: &bag)
}
func throttlePopChat(_ chatId: ChatId, currentPosition: Int) {
let m = ChatModel.shared
if currentPosition > 0 && m.chatId == chatId {
@ -947,7 +961,7 @@ final class ChatModel: ObservableObject {
subject.send()
}
}
func clear() {
chatsToPop = [:]
}
@ -1217,7 +1231,6 @@ struct ShowingInvitation {
}
struct NTFContactRequest {
var incognito: Bool
var chatId: String
}
@ -1255,7 +1268,7 @@ final class Chat: ObservableObject, Identifiable, ChatLike {
default: chatStats.unreadChat
}
}
var id: ChatId { get { chatInfo.id } }
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }

View file

@ -12,7 +12,6 @@ import UIKit
import SimpleXChat
let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT"
let ntfActionAcceptContactIncognito = "NTF_ACT_ACCEPT_CONTACT_INCOGNITO"
let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL"
let ntfActionRejectCall = "NTF_ACT_REJECT_CALL"
@ -59,13 +58,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
logger.debug("NtfManager.processNotificationResponse changeActiveUser")
changeActiveUser(userId, viewPwd: nil)
}
if content.categoryIdentifier == ntfCategoryContactRequest && (action == ntfActionAcceptContact || action == ntfActionAcceptContactIncognito),
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
let chatId = content.userInfo["chatId"] as? String {
let incognito = action == ntfActionAcceptContactIncognito
if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
Task { await acceptContactRequest(incognito: incognito, contactRequest: contactRequest) }
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) }
} else {
chatModel.ntfContactRequest = NTFContactRequest(incognito: incognito, chatId: chatId)
chatModel.ntfContactRequest = NTFContactRequest(chatId: chatId)
}
} else if let (chatId, ntfAction) = ntfCallAction(content, action) {
if let invitation = chatModel.callInvitations.removeValue(forKey: chatId) {
@ -161,10 +159,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
identifier: ntfActionAcceptContact,
title: NSLocalizedString("Accept", comment: "accept contact request via notification"),
options: .foreground
), UNNotificationAction(
identifier: ntfActionAcceptContactIncognito,
title: NSLocalizedString("Accept incognito", comment: "accept contact request via notification"),
options: .foreground
)
],
intentIdentifiers: [],

View file

@ -227,7 +227,7 @@ func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) asyn
}
func apiStartChat(ctrl: chat_ctrl? = nil) throws -> Bool {
let r: ChatResponse0 = try chatSendCmdSync(.startChat(mainApp: true, enableSndFiles: true), ctrl: ctrl)
let r: ChatResponse0 = try chatSendCmdSync(.startChat(mainApp: true, enableSndFiles: true, largeLinkData: true), ctrl: ctrl)
switch r {
case .chatStarted: return true
case .chatRunning: return false
@ -874,8 +874,7 @@ func apiAddContact(incognito: Bool) async -> ((CreatedConnLink, PendingContactCo
logger.error("apiAddContact: no current user")
return (nil, nil)
}
let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)
let r: APIResult<ChatResponse1> = await chatApiSendCmd(.apiAddContact(userId: userId, short: short, incognito: incognito), bgTask: false)
let r: APIResult<ChatResponse1> = await chatApiSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false)
if case let .result(.invitation(_, connLinkInv, connection)) = r { return ((connLinkInv, connection), nil) }
let alert = connectionErrorAlert(r)
return (nil, alert)
@ -889,7 +888,6 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P
func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection {
let r: ChatResponse1 = try await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId))
if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection}
throw r.unexpected
}
@ -901,8 +899,7 @@ func apiConnectPlan(connLink: String) async -> ((CreatedConnLink, ConnectionPlan
}
let r: APIResult<ChatResponse1> = await chatApiSendCmd(.apiConnectPlan(userId: userId, connLink: connLink))
if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) }
let alert = apiConnectResponseAlert(r.unexpected) ?? connectionErrorAlert(r)
return (nil, alert)
return (nil, apiConnectResponseAlert(r))
}
func apiConnect(incognito: Bool, connLink: CreatedConnLink) async -> (ConnReqType, PendingContactConnection)? {
@ -935,12 +932,11 @@ func apiConnect_(incognito: Bool, connLink: CreatedConnLink) async -> ((ConnReqT
return (nil, alert)
default: ()
}
let alert = apiConnectResponseAlert(r.unexpected) ?? connectionErrorAlert(r)
return (nil, alert)
return (nil, apiConnectResponseAlert(r))
}
private func apiConnectResponseAlert(_ r: ChatError) -> Alert? {
switch r {
private func apiConnectResponseAlert<R>(_ r: APIResult<R>) -> Alert {
switch r.unexpected {
case .error(.invalidConnReq):
mkAlert(
title: "Invalid connection link",
@ -976,12 +972,12 @@ private func apiConnectResponseAlert(_ r: ChatError) -> Alert? {
if internalErr == "SEUniqueID" {
mkAlert(
title: "Already connected?",
message: "It seems like you are already connected via this link. If it is not the case, there was an error (\(responseError(r)))."
message: "It seems like you are already connected via this link. If it is not the case, there was an error (\(internalErr))."
)
} else {
nil
connectionErrorAlert(r)
}
default: nil
default: connectionErrorAlert(r)
}
}
@ -1003,6 +999,46 @@ private func connectionErrorAlert<R>(_ r: APIResult<R>) -> Alert {
}
}
func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) async throws -> ChatData {
let userId = try currentUserId("apiPrepareContact")
let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData))
if case let .newPreparedChat(_, chat) = r { return chat }
throw r.unexpected
}
func apiPrepareGroup(connLink: CreatedConnLink, groupShortLinkData: GroupShortLinkData) async throws -> ChatData {
let userId = try currentUserId("apiPrepareGroup")
let r: ChatResponse1 = try await chatSendCmd(.apiPrepareGroup(userId: userId, connLink: connLink, groupShortLinkData: groupShortLinkData))
if case let .newPreparedChat(_, chat) = r { return chat }
throw r.unexpected
}
func apiChangePreparedContactUser(contactId: Int64, newUserId: Int64) async throws -> Contact {
let r: ChatResponse1 = try await chatSendCmd(.apiChangePreparedContactUser(contactId: contactId, newUserId: newUserId))
if case let .contactUserChanged(_, _, _, toContact) = r {return toContact}
throw r.unexpected
}
func apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) async throws -> GroupInfo {
let r: ChatResponse1 = try await chatSendCmd(.apiChangePreparedGroupUser(groupId: groupId, newUserId: newUserId))
if case let .groupUserChanged(_, _, _, toGroup) = r {return toGroup}
throw r.unexpected
}
func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) async -> Contact? {
let r: APIResult<ChatResponse1> = await chatApiSendCmd(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg))
if case let .result(.startedConnectionToContact(_, contact)) = r { return contact }
AlertManager.shared.showAlert(apiConnectResponseAlert(r))
return nil
}
func apiConnectPreparedGroup(groupId: Int64, incognito: Bool, msg: MsgContent?) async -> GroupInfo? {
let r: APIResult<ChatResponse1> = await chatApiSendCmd(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg))
if case let .result(.startedConnectionToGroup(_, groupInfo)) = r { return groupInfo }
AlertManager.shared.showAlert(apiConnectResponseAlert(r))
return nil
}
func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Contact?, Alert?) {
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiConnectContactViaAddress: no current user")
@ -1178,9 +1214,9 @@ func apiSetChatUIThemes(chatId: ChatId, themes: ThemeModeOverrides?) async -> Bo
}
func apiCreateUserAddress(short: Bool) async throws -> CreatedConnLink {
func apiCreateUserAddress() async throws -> CreatedConnLink {
let userId = try currentUserId("apiCreateUserAddress")
let r: ChatResponse1 = try await chatSendCmd(.apiCreateMyAddress(userId: userId, short: short))
let r: ChatResponse1 = try await chatSendCmd(.apiCreateMyAddress(userId: userId))
if case let .userContactLinkCreated(_, connLink) = r { return connLink }
throw r.unexpected
}
@ -1217,9 +1253,9 @@ func apiAddMyAddressShortLink() async throws -> UserContactLink {
throw r.unexpected
}
func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? {
let userId = try currentUserId("userAddressAutoAccept")
let r: APIResult<ChatResponse1> = await chatApiSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept))
func apiSetUserAddressSettings(_ settings: AddressSettings) async throws -> UserContactLink? {
let userId = try currentUserId("apiSetUserAddressSettings")
let r: APIResult<ChatResponse1> = await chatApiSendCmd(.apiSetAddressSettings(userId: userId, addressSettings: settings))
switch r {
case let .result(.userContactLinkUpdated(_, contactLink)): return contactLink
case .error(.errorStore(storeError: .userContactLinkNotFound)): return nil
@ -1249,9 +1285,9 @@ func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Cont
return nil
}
func apiRejectContactRequest(contactReqId: Int64) async throws {
func apiRejectContactRequest(contactReqId: Int64) async throws -> Contact? {
let r: ChatResponse1 = try await chatSendCmd(.apiRejectContact(contactReqId: contactReqId))
if case .contactRequestRejected = r { return }
if case let .contactRequestRejected(_, _, contact_) = r { return contact_ }
throw r.unexpected
}
@ -1314,7 +1350,7 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool
var fileIdsToApprove: [Int64] = []
var srvsToApprove: Set<String> = []
var otherFileErrs: [APIResult<ChatResponse2>] = []
for fileId in fileIds {
let r: APIResult<ChatResponse2> = await chatApiSendCmd(
.receiveFile(
@ -1338,7 +1374,7 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool
otherFileErrs.append(r)
}
}
if !auto {
let otherErrsStr = fileErrorStrs(otherFileErrs)
// If there are not approved files, alert is shown the same way both in case of singular and plural files reception
@ -1403,7 +1439,7 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool
}
}
}
func fileErrorStrs(_ errs: [APIResult<ChatResponse2>]) -> String {
var errStr = ""
if errs.count >= 1 {
@ -1418,7 +1454,7 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool
return errStr
}
}
func cancelFile(user: User, fileId: Int64) async {
if let chatItem = await apiCancelFile(fileId: fileId) {
await chatItemSimpleUpdate(user, chatItem)
@ -1485,29 +1521,49 @@ func networkErrorAlert<R>(_ res: APIResult<R>) -> Alert? {
}
}
func acceptContactRequest(incognito: Bool, contactRequest: UserContactRequest) async {
if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequest.apiId) {
func acceptContactRequest(incognito: Bool, contactRequestId: Int64) async {
if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequestId) {
let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: [])
await MainActor.run {
ChatModel.shared.replaceChat(contactRequest.id, chat)
if contact.contactRequestId != nil { // means contact request was initially created with contact, so we don't need to replace it
ChatModel.shared.updateContact(contact)
} else {
ChatModel.shared.replaceChat(contactRequestChatId(contactRequestId), chat)
}
NetworkModel.shared.setContactNetworkStatus(contact, .connected)
}
if contact.sndReady {
let chatId = chat.id
DispatchQueue.main.async {
dismissAllSheets(animated: true) {
ItemsModel.shared.loadOpenChat(chat.id)
ItemsModel.shared.loadOpenChat(chatId)
}
}
}
}
}
func rejectContactRequest(_ contactRequest: UserContactRequest) async {
func rejectContactRequest(_ contactRequestId: Int64, dismissToChatList: Bool = false) async {
do {
try await apiRejectContactRequest(contactReqId: contactRequest.apiId)
DispatchQueue.main.async { ChatModel.shared.removeChat(contactRequest.id) }
let contact_ = try await apiRejectContactRequest(contactReqId: contactRequestId)
await MainActor.run {
if let contact = contact_ { // means contact request was initially created with contact, so we need to remove contact chat
ChatModel.shared.removeChat(contact.id)
} else {
ChatModel.shared.removeChat(contactRequestChatId(contactRequestId))
}
if dismissToChatList {
ChatModel.shared.chatId = nil
}
}
} catch let error {
logger.error("rejectContactRequest: \(responseError(error))")
await MainActor.run {
showAlert(
NSLocalizedString("Error rejecting contact request", comment: "alert title"),
message: responseError(error)
)
}
}
}
@ -1703,7 +1759,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
return ChatModel.shared.chats
.compactMap{ c in c.chatInfo.sendMsgEnabled ? c.chatInfo.contact : nil }
.filter{ c in !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) }
.filter{ c in !c.sendMsgToConnect && !memberContactIds.contains(c.apiId) }
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
}
@ -1713,16 +1769,15 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
throw r.unexpected
}
func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (CreatedConnLink, GroupMemberRole) {
let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)
let r: ChatResponse2 = try await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole, short: short))
if case let .groupLinkCreated(_, _, connLink, memberRole) = r { return (connLink, memberRole) }
func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink {
let r: ChatResponse2 = try await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole))
if case let .groupLinkCreated(_, _, groupLink) = r { return groupLink }
throw r.unexpected
}
func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (CreatedConnLink, GroupMemberRole) {
func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink {
let r: ChatResponse2 = try await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole))
if case let .groupLink(_, _, connLink, memberRole) = r { return (connLink, memberRole) }
if case let .groupLink(_, _, groupLink) = r { return groupLink }
throw r.unexpected
}
@ -1732,20 +1787,20 @@ func apiDeleteGroupLink(_ groupId: Int64) async throws {
throw r.unexpected
}
func apiGetGroupLink(_ groupId: Int64) throws -> (CreatedConnLink, GroupMemberRole)? {
func apiGetGroupLink(_ groupId: Int64) throws -> GroupLink? {
let r: APIResult<ChatResponse2> = chatApiSendCmdSync(.apiGetGroupLink(groupId: groupId))
switch r {
case let .result(.groupLink(_, _, connLink, memberRole)):
return (connLink, memberRole)
case let .result(.groupLink(_, _, groupLink)):
return groupLink
case .error(.errorStore(storeError: .groupLinkNotFound)):
return nil
default: throw r.unexpected
}
}
func apiAddGroupShortLink(_ groupId: Int64) async throws -> (CreatedConnLink, GroupMemberRole) {
func apiAddGroupShortLink(_ groupId: Int64) async throws -> GroupLink {
let r: ChatResponse2 = try await chatSendCmd(.apiAddGroupShortLink(groupId: groupId))
if case let .groupLink(_, _, connLink, memberRole) = r { return (connLink, memberRole) }
if case let .groupLink(_, _, groupLink) = r { return groupLink }
throw r.unexpected
}
@ -1924,7 +1979,7 @@ private func changeActiveUser_(_ userId: Int64, viewPwd: String?) throws {
try getUserChatData()
}
func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?) async throws {
func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?, keepingChatId: String? = nil) async throws {
let currentUser = if let userId = userId {
try await apiSetActiveUserAsync(userId, viewPwd: viewPwd)
} else {
@ -1936,7 +1991,7 @@ func changeActiveUserAsync_(_ userId: Int64?, viewPwd: String?) async throws {
m.currentUser = currentUser
m.users = users
}
try await getUserChatDataAsync()
try await getUserChatDataAsync(keepingChatId: keepingChatId)
await MainActor.run {
if let currentUser = currentUser, var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) {
invitation.user = currentUser
@ -1958,7 +2013,7 @@ func getUserChatData() throws {
tm.updateChatTags(m.chats)
}
private func getUserChatDataAsync() async throws {
private func getUserChatDataAsync(keepingChatId: String?) async throws {
let m = ChatModel.shared
let tm = ChatTagsModel.shared
if m.currentUser != nil {
@ -1969,7 +2024,7 @@ private func getUserChatDataAsync() async throws {
await MainActor.run {
m.userAddress = userAddress
m.chatItemTTL = chatItemTTL
m.updateChats(chats)
m.updateChats(chats, keepingChatId: keepingChatId)
tm.activeFilter = nil
tm.userTags = tags
tm.updateChatTags(m.chats)
@ -2080,17 +2135,27 @@ func processReceivedMsg(_ res: ChatEvent) async {
await MainActor.run {
n.setContactNetworkStatus(contact, .connected)
}
case let .receivedContactRequest(user, contactRequest):
case let .receivedContactRequest(user, contactRequest, chat_):
if active(user) {
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
await MainActor.run {
if m.hasChat(contactRequest.id) {
m.updateChatInfo(cInfo)
if let chat = chat_ { // means contact request was created with contact, so we need to add/update contact chat
if !m.hasChat(chat.id) {
m.addChat(Chat(chat))
} else if m.chatId == chat.id {
m.updateChatInfo(chat.chatInfo)
} else {
m.replaceChat(chat.id, Chat(chat))
}
} else {
m.addChat(Chat(
chatInfo: cInfo,
chatItems: []
))
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
if m.hasChat(contactRequest.id) {
m.updateChatInfo(cInfo)
} else {
m.addChat(Chat(
chatInfo: cInfo,
chatItems: []
))
}
}
}
}

View file

@ -164,7 +164,7 @@ struct SimpleXApp: App {
if let ncr = chatModel.ntfContactRequest {
await MainActor.run { chatModel.ntfContactRequest = nil }
if case let .contactRequest(contactRequest) = chatModel.getChat(ncr.chatId)?.chatInfo {
Task { await acceptContactRequest(incognito: ncr.incognito, contactRequest: contactRequest) }
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) }
}
}
} catch let error {

View file

@ -833,7 +833,7 @@ private struct CallButton: View {
))
}
}
} else if contact.nextSendGrpInv {
} else if contact.sendMsgToConnect {
showAlert(SomeAlert(
alert: mkAlert(
title: "Can't call contact",

View file

@ -241,16 +241,21 @@ struct ChatItemContentView<Content: View>: View {
}
private func directE2EEInfoText(_ info: E2EEInfo) -> Text {
info.pqEnabled
? Text("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.")
.font(.caption)
.foregroundColor(theme.colors.secondary)
.fontWeight(.light)
: e2eeInfoNoPQText()
if let pqEnabled = info.pqEnabled {
pqEnabled
? e2eeInfoText("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.")
: e2eeInfoNoPQText()
} else {
e2eeInfoText("Messages are protected by **end-to-end encryption**.")
}
}
private func e2eeInfoNoPQText() -> Text {
Text("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.")
e2eeInfoText("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.")
}
private func e2eeInfoText(_ s: LocalizedStringKey) -> Text {
Text(s)
.font(.caption)
.foregroundColor(theme.colors.secondary)
.fontWeight(.light)

View file

@ -47,7 +47,7 @@ struct ChatView: View {
@State private var selectedMember: GMember? = nil
// opening GroupLinkView on link button (incognito)
@State private var showGroupLinkSheet: Bool = false
@State private var groupLink: CreatedConnLink?
@State private var groupLink: GroupLink?
@State private var groupLinkMemberRole: GroupMemberRole = .member
@State private var forwardedChatItems: [ChatItem] = []
@State private var selectedChatItems: Set<Int64>? = nil
@ -114,9 +114,19 @@ struct ChatView: View {
}
)
}
connectingText()
if let connectingText {
Text(connectingText)
.font(.caption)
.foregroundColor(theme.colors.secondary)
.padding(.top)
}
if selectedChatItems == nil {
let reason = chat.chatInfo.userCantSendReason
let composeEnabled = (
chat.chatInfo.sendMsgEnabled ||
(chat.chatInfo.groupInfo?.nextConnectPrepared ?? false) || // allow to join prepared group without message
(chat.chatInfo.contact?.nextAcceptContactRequest ?? false) // allow to accept or reject contact request
)
ComposeView(
chat: chat,
im: im,
@ -126,8 +136,8 @@ struct ChatView: View {
selectedRange: $selectedRange,
disabledText: reason?.composeLabel
)
.disabled(!cInfo.sendMsgEnabled)
.if(!cInfo.sendMsgEnabled) { v in
.disabled(!composeEnabled)
.if(!composeEnabled) { v in
v.disabled(true).onTapGesture {
AlertManager.shared.showAlertMsg(
title: "You can't send messages!",
@ -756,17 +766,22 @@ struct ChatView: View {
}
}
@ViewBuilder private func connectingText() -> some View {
if case let .direct(contact) = chat.chatInfo,
!contact.sndReady,
contact.active,
!contact.nextSendGrpInv {
Text("connecting…")
.font(.caption)
.foregroundColor(theme.colors.secondary)
.padding(.top)
} else {
EmptyView()
private var connectingText: LocalizedStringKey? {
switch (chat.chatInfo) {
case let .direct(contact):
if !contact.sndReady && contact.active && !contact.sendMsgToConnect && !contact.nextAcceptContactRequest {
contact.preparedContact?.uiConnLinkType == .con
? "contact should accept…"
: "connecting…"
} else {
nil
}
case let .group(groupInfo, _):
switch (groupInfo.membership.memberStatus) {
case .memAccepted: "connecting…" // TODO [short links] add member status to show transition from prepared group to started connection earlier?
default: nil
}
default: nil
}
}
@ -1034,8 +1049,9 @@ struct ChatView: View {
if case let .group(gInfo, _) = chat.chatInfo {
Task {
do {
if let link = try apiGetGroupLink(gInfo.groupId) {
(groupLink, groupLinkMemberRole) = link
if let gLink = try apiGetGroupLink(gInfo.groupId) {
groupLink = gLink
groupLinkMemberRole = gLink.acceptMemberRole
}
} catch let error {
logger.error("ChatView apiGetGroupLink: \(responseError(error))")
@ -1346,7 +1362,7 @@ struct ChatView: View {
} else {
nil
}
let showAvatar = shouldShowAvatar(item, listItem.nextItem)
let showAvatar = shouldShowAvatar(item, merged.oldest().nextItem)
let single = switch merged {
case .single: true
default: false
@ -1498,7 +1514,7 @@ struct ChatView: View {
) -> some View {
let bottomPadding: Double = itemSeparation.largeGap ? 10 : 2
if case let .groupRcv(member) = ci.chatDir,
case .group = chat.chatInfo {
case let .group(groupInfo, _) = chat.chatInfo {
if showAvatar {
VStack(alignment: .leading, spacing: 4) {
if ci.content.showMemberName {
@ -1509,22 +1525,27 @@ struct ChatView: View {
} else {
(nil, 1)
}
if memCount == 1 && member.memberRole > .member {
if memCount == 1 && (member.memberRole > .member || ci.meta.showGroupAsSender) {
let (name, role) = if ci.meta.showGroupAsSender {
(groupInfo.chatViewName, NSLocalizedString("group", comment: "shown on group welcome message"))
} else {
(member.chatViewName, member.memberRole.text)
}
Group {
if #available(iOS 16.0, *) {
MemberLayout(spacing: 16, msgWidth: msgWidth) {
Text(member.chatViewName)
Text(name)
.lineLimit(1)
Text(member.memberRole.text)
Text(role)
.fontWeight(.semibold)
.lineLimit(1)
.padding(.trailing, 8)
}
} else {
HStack(spacing: 16) {
Text(member.chatViewName)
Text(name)
.lineLimit(1)
Text(member.memberRole.text)
Text(role)
.fontWeight(.semibold)
.lineLimit(1)
.layoutPriority(1)
@ -1551,17 +1572,24 @@ struct ChatView: View {
.padding(.trailing, 12)
}
HStack(alignment: .top, spacing: 10) {
MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background)
.simultaneousGesture(TapGesture().onEnded {
if let mem = m.getGroupMember(member.groupMemberId) {
selectedMember = mem
} else {
let mem = GMember.init(member)
m.groupMembers.append(mem)
m.groupMembersIndexes[member.groupMemberId] = m.groupMembers.count - 1
selectedMember = mem
}
})
if ci.meta.showGroupAsSender {
ProfileImage(imageStr: groupInfo.image, iconName: groupInfo.chatIconName, size: memberImageSize, backgroundColor: theme.colors.background)
.simultaneousGesture(TapGesture().onEnded {
showChatInfoSheet = true
})
} else {
MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background)
.simultaneousGesture(TapGesture().onEnded {
if let mem = m.getGroupMember(member.groupMemberId) {
selectedMember = mem
} else {
let mem = GMember.init(member)
m.groupMembers.append(mem)
m.groupMembersIndexes[member.groupMemberId] = m.groupMembers.count - 1
selectedMember = mem
}
})
}
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
}

View file

@ -1,10 +1,3 @@
//
// ComposeView.swift
// SimpleX
//
// Created by Evgeny on 13/03/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
@ -114,7 +107,7 @@ struct ComposeState {
mentions: mentions ?? self.mentions
)
}
func mentionMemberName(_ name: String) -> String {
var n = 0
var tryName = name
@ -124,11 +117,11 @@ struct ComposeState {
}
return tryName
}
var memberMentions: [String: Int64] {
self.mentions.compactMapValues { $0.memberRef?.groupMemberId }
}
var editing: Bool {
switch contextItem {
case .editingItem: return true
@ -149,14 +142,14 @@ struct ComposeState {
default: return false
}
}
var reporting: Bool {
switch contextItem {
case .reportedItem: return true
default: return false
}
}
var submittingValidReport: Bool {
switch contextItem {
case let .reportedItem(_, reason):
@ -167,13 +160,13 @@ struct ComposeState {
default: return false
}
}
var sendEnabled: Bool {
switch preview {
case let .mediaPreviews(media): return !media.isEmpty
case .voicePreview: return voiceMessageRecordingState == .finished
case .filePreview: return true
default: return !message.isEmpty || forwarding || liveMessage != nil || submittingValidReport
default: return !whitespaceOnly || forwarding || liveMessage != nil || submittingValidReport
}
}
@ -254,7 +247,11 @@ struct ComposeState {
}
var empty: Bool {
message == "" && noPreview
whitespaceOnly && noPreview
}
var whitespaceOnly: Bool {
message.allSatisfy { $0.isWhitespace }
}
}
@ -357,6 +354,16 @@ struct ComposeView: View {
var body: some View {
VStack(spacing: 0) {
Divider()
let contact = chat.chatInfo.contact
if (contact?.nextConnectPrepared ?? false) || (chat.chatInfo.groupInfo?.nextConnectPrepared ?? false),
let user = chatModel.currentUser {
ContextProfilePickerView(
chat: chat,
selectedUser: user
)
Divider()
}
if let groupInfo = chat.chatInfo.groupInfo,
case let .groupChatScopeContext(groupScopeInfo) = im.secondaryIMFilter,
case let .memberSupport(member) = groupScopeInfo,
@ -371,11 +378,6 @@ struct ComposeView: View {
Divider()
}
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
ContextInvitingContactMemberView()
Divider()
}
if case let .reportedItem(_, reason) = composeState.contextItem {
reportReasonView(reason)
Divider()
@ -384,6 +386,7 @@ struct ComposeView: View {
let simplexLinkProhibited = im.secondaryIMFilter == nil && hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
let fileProhibited = im.secondaryIMFilter == nil && composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
let disableSendButton = simplexLinkProhibited || fileProhibited || voiceProhibited
if simplexLinkProhibited {
msgNotAllowedView("SimpleX links not allowed", icon: "link")
Divider()
@ -400,73 +403,44 @@ struct ComposeView: View {
case (true, .voicePreview): EmptyView() // ? we may allow playback when editing is allowed
default: previewView()
}
HStack (alignment: .bottom) {
let b = Button {
showChooseSource = true
} label: {
Image(systemName: "paperclip")
.resizable()
}
.disabled(composeState.attachmentDisabled || !chat.chatInfo.sendMsgEnabled || (chat.chatInfo.contact?.nextSendGrpInv ?? false))
.frame(width: 25, height: 25)
.padding(.bottom, 16)
.padding(.leading, 12)
.tint(theme.colors.primary)
if im.secondaryIMFilter == nil,
case let .group(g, _) = chat.chatInfo,
!g.fullGroupPreferences.files.on(for: g.membership) {
b.disabled(true).onTapGesture {
AlertManager.shared.showAlertMsg(
title: "Files and media prohibited!",
message: "Only group owners can enable files and media."
)
}
} else {
b
}
ZStack(alignment: .leading) {
SendMessageView(
composeState: $composeState,
selectedRange: $selectedRange,
sendMessage: { ttl in
sendMessage(ttl: ttl)
resetLinkPreview()
},
sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil,
updateLiveMessage: updateLiveMessage,
cancelLiveMessage: {
composeState.liveMessage = nil
chatModel.removeLiveDummy()
},
nextSendGrpInv: chat.chatInfo.contact?.nextSendGrpInv ?? false,
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
disableSendButton: simplexLinkProhibited || fileProhibited || voiceProhibited,
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
startVoiceMessageRecording: {
Task {
await startVoiceMessageRecording()
}
},
finishVoiceMessageRecording: finishVoiceMessageRecording,
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages),
onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }},
keyboardVisible: $keyboardVisible,
keyboardHiddenDate: $keyboardHiddenDate,
sendButtonColor: chat.chatInfo.incognito
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
: theme.colors.primary
)
.padding(.trailing, 12)
.disabled(!chat.chatInfo.sendMsgEnabled)
if let disabledText {
Text(disabledText)
.italic()
.foregroundColor(theme.colors.secondary)
.padding(.horizontal, 12)
if chat.chatInfo.groupInfo?.nextConnectPrepared == true {
if chat.chatInfo.groupInfo?.businessChat == nil {
Button(action: connectPreparedGroup) {
Label("Join group", systemImage: "person.2.fill")
}
.frame(height: 60)
.disabled(composeState.inProgress)
} else {
sendContactRequestView(disableSendButton, icon: "briefcase.fill", sendRequest: connectPreparedGroup)
}
} else if contact?.nextSendGrpInv == true {
contextSendMessageToConnect("Send direct message to connect")
Divider()
HStack (alignment: .center) {
attachmentButton().disabled(true)
sendMessageView(disableSendButton, sendToConnect: sendMemberContactInvitation)
}
.padding(.horizontal, 12)
} else if contact?.nextConnectPrepared == true, let linkType = contact?.preparedContact?.uiConnLinkType {
switch linkType {
case .inv:
Button(action: sendConnectPreparedContact) {
Label("Connect", systemImage: "person.fill.badge.plus")
}
.frame(height: 60)
.disabled(composeState.inProgress)
case .con:
sendContactRequestView(disableSendButton, icon: "person.fill.badge.plus", sendRequest: sendConnectPreparedContactRequest)
}
} else if contact?.nextAcceptContactRequest == true, let crId = contact?.contactRequestId {
ContextContactRequestActionsView(contactRequestId: crId)
} else {
HStack (alignment: .center) {
attachmentButton()
sendMessageView(disableSendButton)
}
.padding(.horizontal, 12)
}
}
.background {
@ -632,6 +606,196 @@ struct ComposeView: View {
}
}
private func sendContactRequestView(_ disableSendButton: Bool, icon: String, sendRequest: @escaping () -> Void) -> some View {
HStack (alignment: .center) {
sendMessageView(
disableSendButton,
placeholder: NSLocalizedString("Add message", comment: "placeholder for sending contact request"),
sendToConnect: sendRequest
)
if composeState.whitespaceOnly {
Button(action: sendRequest) {
HStack {
Text("Connect").fontWeight(.medium)
Image(systemName: icon)
}
}
.padding(.horizontal, 8)
.disabled(composeState.inProgress)
}
}
.padding(.horizontal, 12)
}
private func sendMessageView(_ disableSendButton: Bool, placeholder: String? = nil, sendToConnect: (() -> Void)? = nil) -> some View {
ZStack(alignment: .leading) {
SendMessageView(
placeholder: placeholder,
composeState: $composeState,
selectedRange: $selectedRange,
sendMessage: { ttl in
sendMessage(ttl: ttl)
resetLinkPreview()
},
sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil,
updateLiveMessage: updateLiveMessage,
cancelLiveMessage: {
composeState.liveMessage = nil
chatModel.removeLiveDummy()
},
sendToConnect: sendToConnect,
hideSendButton: chat.chatInfo.nextConnect && chat.chatInfo.contact?.nextSendGrpInv != true && composeState.whitespaceOnly,
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
disableSendButton: disableSendButton,
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
startVoiceMessageRecording: {
Task {
await startVoiceMessageRecording()
}
},
finishVoiceMessageRecording: finishVoiceMessageRecording,
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages),
onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }},
keyboardVisible: $keyboardVisible,
keyboardHiddenDate: $keyboardHiddenDate,
sendButtonColor: chat.chatInfo.incognito
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
: theme.colors.primary
)
.disabled(!chat.chatInfo.sendMsgEnabled)
if let disabledText {
Text(disabledText)
.italic()
.foregroundColor(theme.colors.secondary)
.padding(.horizontal, 12)
}
}
}
@ViewBuilder private func attachmentButton() -> some View {
let b = Button {
showChooseSource = true
} label: {
Image(systemName: "paperclip")
.resizable()
}
.disabled(composeState.attachmentDisabled || !chat.chatInfo.sendMsgEnabled)
.frame(width: 25, height: 25)
.tint(theme.colors.primary)
if im.secondaryIMFilter == nil,
case let .group(g, _) = chat.chatInfo,
!g.fullGroupPreferences.files.on(for: g.membership) {
b.disabled(true).onTapGesture {
AlertManager.shared.showAlertMsg(
title: "Files and media prohibited!",
message: "Only group owners can enable files and media."
)
}
} else {
b
}
}
private func sendMemberContactInvitation() {
Task {
do {
if let mc = connectCheckLinkPreview() {
await sending()
let contact = try await apiSendMemberContactInvitation(chat.chatInfo.apiId, mc)
await MainActor.run {
self.chatModel.updateContact(contact)
clearState()
NetworkModel.shared.setContactNetworkStatus(contact, .connected)
}
} else {
AlertManager.shared.showAlertMsg(title: "Empty message!")
}
} catch {
await MainActor.run { composeState.inProgress = false }
logger.error("ChatView.sendMemberContactInvitation error: \(error.localizedDescription)")
AlertManager.shared.showAlertMsg(title: "Error sending member contact invitation", message: "Error: \(responseError(error))")
}
}
}
// TODO [short links] different messages for business
private func sendConnectPreparedContactRequest() {
hideKeyboard()
let empty = composeState.whitespaceOnly
AlertManager.shared.showAlert(Alert(
title: Text("Send contact request?"),
message: Text("You will be able to send messages **only after your request is accepted**."),
primaryButton: .default(
Text(empty ? "Send request without message" : "Send request"),
action: sendConnectPreparedContact
),
secondaryButton:
empty
? .cancel(Text("Add message")) { keyboardVisible = true }
: .cancel()
))
}
private func sendConnectPreparedContact() {
Task {
await sending()
let mc = connectCheckLinkPreview()
if let contact = await apiConnectPreparedContact(contactId: chat.chatInfo.apiId, incognito: incognitoGroupDefault.get(), msg: mc) {
await MainActor.run {
self.chatModel.updateContact(contact)
NetworkModel.shared.setContactNetworkStatus(contact, .connected)
clearState()
}
} else {
await MainActor.run { composeState.inProgress = false }
}
}
}
private func connectPreparedGroup() {
Task {
await sending()
let mc = connectCheckLinkPreview()
if let groupInfo = await apiConnectPreparedGroup(groupId: chat.chatInfo.apiId, incognito: incognitoGroupDefault.get(), msg: mc) {
await MainActor.run {
self.chatModel.updateGroup(groupInfo)
clearState()
}
} else {
await MainActor.run { composeState.inProgress = false }
}
}
}
@inline(__always)
private func connectCheckLinkPreview() -> MsgContent? {
let msgText = composeState.message.trimmingCharacters(in: .whitespacesAndNewlines)
return msgText.isEmpty ? nil : checkLinkPreview_(msgText)
}
@inline(__always)
private func checkLinkPreview() -> MsgContent {
checkLinkPreview_(composeState.message.trimmingCharacters(in: .whitespacesAndNewlines))
}
private func checkLinkPreview_(_ msgText: String) -> MsgContent {
switch (composeState.preview) {
case let .linkPreview(linkPreview: linkPreview):
if let parsedMsg = parseSimpleXMarkdown(msgText),
let url = getSimplexLink(parsedMsg).url,
let linkPreview = linkPreview,
url == linkPreview.uri {
return .link(text: msgText, preview: linkPreview)
} else {
return .text(msgText)
}
default:
return .text(msgText)
}
}
private func addMediaContent(_ content: UploadContent) async {
if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
var newMedia: [(String, UploadContent?)] = []
@ -768,8 +932,19 @@ struct ComposeView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial)
}
private func contextSendMessageToConnect(_ s: LocalizedStringKey) -> some View {
HStack {
Image(systemName: "message")
.foregroundColor(theme.colors.secondary)
Text(s)
}
.padding(12)
.frame(minHeight: 54)
.frame(maxWidth: .infinity, alignment: .leading)
.background(ToolbarMaterial.material(toolbarMaterial))
}
private func reportReasonView(_ reason: ReportReason) -> some View {
let reportText = switch reason {
case .spam: NSLocalizedString("Report spam: only group moderators will see it.", comment: "report reason")
@ -847,9 +1022,7 @@ struct ComposeView: View {
if liveMessage != nil { composeState = composeState.copy(liveMessage: nil) }
await sending()
}
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
await sendMemberContactInvitation()
} else if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem {
if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem {
// Composed text is send as a reply to the last forwarded item
sent = await forwardItems(chatItems, fromChatInfo, ttl).last
if !composeState.message.isEmpty {
@ -929,23 +1102,6 @@ struct ComposeView: View {
nil
}
}
func sending() async {
await MainActor.run { composeState.inProgress = true }
}
func sendMemberContactInvitation() async {
do {
let mc = checkLinkPreview()
let contact = try await apiSendMemberContactInvitation(chat.chatInfo.apiId, mc)
await MainActor.run {
self.chatModel.updateContact(contact)
}
} catch {
logger.error("ChatView.sendMemberContactInvitation error: \(error.localizedDescription)")
AlertManager.shared.showAlertMsg(title: "Error sending member contact invitation", message: "Error: \(responseError(error))")
}
}
func updateMessage(_ ei: ChatItem, live: Bool) async -> ChatItem? {
if let oldMsgContent = ei.content.msgContent {
@ -991,6 +1147,9 @@ struct ComposeView: View {
return .file(msgText)
case .report(_, let reason):
return .report(text: msgText, reason: reason)
// TODO [short links] update chat link
case let .chat(_, chatLink):
return .chat(text: msgText, chatLink: chatLink)
case .unknown(let type, _):
return .unknown(type: type, text: msgText)
}
@ -1010,7 +1169,7 @@ struct ComposeView: View {
return nil
}
}
func send(_ reportReason: ReportReason, chatItemId: Int64) async -> ChatItem? {
if let chatItems = await apiReportMessage(
groupId: chat.chatInfo.apiId,
@ -1025,7 +1184,7 @@ struct ComposeView: View {
}
return chatItems.first
}
return nil
}
@ -1114,22 +1273,10 @@ struct ComposeView: View {
return []
}
}
}
func checkLinkPreview() -> MsgContent {
switch (composeState.preview) {
case let .linkPreview(linkPreview: linkPreview):
if let parsedMsg = parseSimpleXMarkdown(msgText),
let url = getSimplexLink(parsedMsg).url,
let linkPreview = linkPreview,
url == linkPreview.uri {
return .link(text: msgText, preview: linkPreview)
} else {
return .text(msgText)
}
default:
return .text(msgText)
}
}
func sending() async {
await MainActor.run { composeState.inProgress = true }
}
private func startVoiceMessageRecording() async {

View file

@ -0,0 +1,82 @@
//
// ContextContactRequestActionsView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 02.05.2025.
// Copyright © 2025 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ContextContactRequestActionsView: View {
@EnvironmentObject var theme: AppTheme
var contactRequestId: Int64
@UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
@State private var inProgress = false
var body: some View {
HStack(spacing: 0) {
Button(role: .destructive, action: showRejectRequestAlert) {
Label("Reject", systemImage: "multiply")
}
.frame(maxWidth: .infinity, minHeight: 60)
Button {
if ChatModel.shared.addressShortLinkDataSet {
acceptRequest()
} else {
showAcceptRequestAlert()
}
} label: {
Label("Accept", systemImage: "checkmark")
}
.frame(maxWidth: .infinity, minHeight: 60)
}
.disabled(inProgress)
.frame(maxWidth: .infinity)
.background(ToolbarMaterial.material(toolbarMaterial))
}
private func showRejectRequestAlert() {
showAlert(
NSLocalizedString("Reject contact request", comment: "alert title"),
message: NSLocalizedString("The sender will NOT be notified", comment: "alert message"),
actions: {[
UIAlertAction(title: NSLocalizedString("Reject", comment: "alert action"), style: .destructive) { _ in
Task { await rejectContactRequest(contactRequestId, dismissToChatList: true) }
},
cancelAlertAction
]}
)
}
private func showAcceptRequestAlert() {
showAlert(
NSLocalizedString("Accept contact request", comment: "alert title"),
actions: {[
UIAlertAction(title: NSLocalizedString("Accept", comment: "alert action"), style: .default) { _ in
acceptRequest()
},
UIAlertAction(title: NSLocalizedString("Accept incognito", comment: "alert action"), style: .default) { _ in
acceptRequest(incognito: true)
},
cancelAlertAction
]}
)
}
private func acceptRequest(incognito: Bool = false) {
inProgress = true
Task {
await acceptContactRequest(incognito: false, contactRequestId: contactRequestId)
await MainActor.run { inProgress = false }
}
}
}
#Preview {
ContextContactRequestActionsView(
contactRequestId: 1
)
}

View file

@ -1,31 +0,0 @@
//
// ContextInvitingContactMemberView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 18.09.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct ContextInvitingContactMemberView: View {
@EnvironmentObject var theme: AppTheme
var body: some View {
HStack {
Image(systemName: "message")
.foregroundColor(theme.colors.secondary)
Text("Send direct message to connect")
}
.padding(12)
.frame(minHeight: 54)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial)
}
}
struct ContextInvitingContactMemberView_Previews: PreviewProvider {
static var previews: some View {
ContextInvitingContactMemberView()
}
}

View file

@ -14,6 +14,7 @@ struct ContextPendingMemberActionsView: View {
@Environment(\.dismiss) var dismiss
var groupInfo: GroupInfo
var member: GroupMember
@UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
var body: some View {
HStack(spacing: 0) {
@ -39,7 +40,7 @@ struct ContextPendingMemberActionsView: View {
}
.frame(minHeight: 54)
.frame(maxWidth: .infinity)
.background(.thinMaterial)
.background(ToolbarMaterial.material(toolbarMaterial))
}
}

View file

@ -0,0 +1,284 @@
//
// ContextProfilePickerView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 13.06.2025.
// Copyright © 2025 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
let USER_ROW_SIZE: CGFloat = 60
let MAX_VISIBLE_USER_ROWS: CGFloat = 4.8
struct ContextProfilePickerView: View {
@ObservedObject var chat: Chat
@EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme
@State var selectedUser: User
@State private var users: [User] = []
@State private var listExpanded = false
@State private var expandedListReady = false
@State private var showIncognitoSheet = false
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
var body: some View {
viewBody()
.onAppear {
users = chatModel.users
.map { $0.user }
.filter { u in u.activeUser || !u.hidden }
}
.sheet(isPresented: $showIncognitoSheet) {
IncognitoHelp()
}
}
private func viewBody() -> some View {
Group {
if !listExpanded {
currentSelection()
} else {
profilePicker()
}
}
}
private func currentSelection() -> some View {
VStack(spacing: 0) {
HStack {
Text("Your profile")
.font(.callout)
.foregroundColor(theme.colors.secondary)
Spacer()
}
.padding(.top, 8)
.padding(.bottom, -4)
.padding(.leading, 12)
.padding(.trailing)
if incognitoDefault {
incognitoOption()
} else {
profilerPickerUserOption(selectedUser)
}
}
}
private func profilePicker() -> some View {
ScrollViewReader { proxy in
Group {
if expandedListReady {
let scroll = ScrollView {
LazyVStack(spacing: 0) {
let otherUsers = users
.filter { u in u.userId != selectedUser.userId }
.sorted(using: KeyPathComparator<User>(\.activeOrder))
ForEach(otherUsers) { p in
profilerPickerUserOption(p)
.contentShape(Rectangle())
Divider()
.padding(.leading)
.padding(.leading, 48)
}
if incognitoDefault {
profilerPickerUserOption(selectedUser)
.contentShape(Rectangle())
Divider()
.padding(.leading)
.padding(.leading, 48)
incognitoOption()
.contentShape(Rectangle())
.id("BOTTOM_ANCHOR")
} else {
incognitoOption()
.contentShape(Rectangle())
Divider()
.padding(.leading)
.padding(.leading, 48)
profilerPickerUserOption(selectedUser)
.contentShape(Rectangle())
.id("BOTTOM_ANCHOR")
}
}
}
.frame(maxHeight: USER_ROW_SIZE * min(MAX_VISIBLE_USER_ROWS, CGFloat(users.count + 1))) // + 1 for incognito
.onAppear {
DispatchQueue.main.async {
withAnimation(nil) {
proxy.scrollTo("BOTTOM_ANCHOR", anchor: .bottom)
}
}
}
.onDisappear {
expandedListReady = false
}
if #available(iOS 16.0, *) {
scroll.scrollDismissesKeyboard(.never)
} else {
scroll
}
} else {
// Keep showing current selection to avoid flickering of scroll to bottom
currentSelection()
.onAppear {
// Delay rendering of expanded profile list
DispatchQueue.main.async {
expandedListReady = true
}
}
}
}
}
}
private func profilerPickerUserOption(_ user: User) -> some View {
Button {
if selectedUser == user {
if !incognitoDefault {
listExpanded.toggle()
} else {
incognitoDefault = false
listExpanded = false
}
} else if selectedUser != user {
changeProfile(user)
}
} label: {
HStack {
ProfileImage(imageStr: user.image, size: 38)
Text(user.chatViewName)
.fontWeight(selectedUser == user && !incognitoDefault ? .medium : .regular)
.foregroundColor(theme.colors.onBackground)
.lineLimit(1)
Spacer()
if selectedUser == user && !incognitoDefault {
if listExpanded {
Image(systemName: "chevron.down")
.font(.system(size: 12, weight: .bold))
.foregroundColor(theme.colors.secondary)
.opacity(0.7)
} else {
Image(systemName: "chevron.up")
.font(.system(size: 12, weight: .bold))
.foregroundColor(theme.colors.secondary)
.opacity(0.7)
}
}
}
.padding(.leading, 12)
.padding(.trailing)
.frame(height: USER_ROW_SIZE)
}
}
private func changeProfile(_ newUser: User) {
Task {
do {
if let contact = chat.chatInfo.contact {
let updatedContact = try await apiChangePreparedContactUser(contactId: contact.contactId, newUserId: newUser.userId)
await MainActor.run {
selectedUser = newUser
incognitoDefault = false
listExpanded = false
chatModel.updateContact(updatedContact)
}
} else if let groupInfo = chat.chatInfo.groupInfo {
let updatedGroupInfo = try await apiChangePreparedGroupUser(groupId: groupInfo.groupId, newUserId: newUser.userId)
await MainActor.run {
selectedUser = newUser
incognitoDefault = false
listExpanded = false
chatModel.updateGroup(updatedGroupInfo)
}
}
do {
try await changeActiveUserAsync_(newUser.userId, viewPwd: nil, keepingChatId: chat.id)
} catch {
await MainActor.run {
showAlert(
NSLocalizedString("Error switching profile", comment: "alert title"),
message: String.localizedStringWithFormat(NSLocalizedString("Your chat was moved to %@ but an unexpected error occurred while redirecting you to the profile.", comment: "alert message"), newUser.chatViewName)
)
}
}
} catch let error {
await MainActor.run {
if let currentUser = chatModel.currentUser {
selectedUser = currentUser
}
showAlert(
NSLocalizedString("Error changing chat profile", comment: "alert title"),
message: responseError(error)
)
}
}
}
}
private func incognitoOption() -> some View {
Button {
if incognitoDefault {
listExpanded.toggle()
} else {
incognitoDefault = true
listExpanded = false
}
} label : {
HStack {
incognitoProfileImage()
Text("Incognito")
.fontWeight(incognitoDefault ? .medium : .regular)
.foregroundColor(theme.colors.onBackground)
Image(systemName: "info.circle")
.font(.system(size: 16))
.foregroundColor(theme.colors.primary)
.onTapGesture {
showIncognitoSheet = true
}
Spacer()
if incognitoDefault {
if listExpanded {
Image(systemName: "chevron.down")
.font(.system(size: 12, weight: .bold))
.foregroundColor(theme.colors.secondary)
.opacity(0.7)
} else {
Image(systemName: "chevron.up")
.font(.system(size: 12, weight: .bold))
.foregroundColor(theme.colors.secondary)
.opacity(0.7)
}
}
}
.padding(.leading, 12)
.padding(.trailing)
.frame(height: USER_ROW_SIZE)
}
}
private func incognitoProfileImage() -> some View {
Image(systemName: "theatermasks.fill")
.resizable()
.scaledToFit()
.frame(width: 38)
.foregroundColor(.indigo)
}
}
#Preview {
ContextProfilePickerView(
chat: Chat.sampleData,
selectedUser: User.sampleData
)
}

View file

@ -70,6 +70,13 @@ struct NativeTextEditor: UIViewRepresentable {
if field.selectedRange != selectedRange {
field.selectedRange = selectedRange
}
if focused && !field.isFocused {
DispatchQueue.main.async {
if !field.isFocused {
field.becomeFirstResponder()
}
}
}
}
}

View file

@ -12,6 +12,7 @@ import SimpleXChat
private let liveMsgInterval: UInt64 = 3000_000000
struct SendMessageView: View {
var placeholder: String?
@Binding var composeState: ComposeState
@Binding var selectedRange: NSRange
@EnvironmentObject var theme: AppTheme
@ -20,7 +21,8 @@ struct SendMessageView: View {
var sendLiveMessage: (() async -> Void)? = nil
var updateLiveMessage: (() async -> Void)? = nil
var cancelLiveMessage: (() -> Void)? = nil
var nextSendGrpInv: Bool = false
var sendToConnect: (() -> Void)? = nil
var hideSendButton: Bool = false
var showVoiceMessageButton: Bool = true
var voiceMessageAllowed: Bool = true
var disableSendButton = false
@ -64,7 +66,7 @@ struct SendMessageView: View {
height: $teHeight,
focused: $keyboardVisible,
lastUnfocusedDate: $keyboardHiddenDate,
placeholder: Binding(get: { composeState.placeholder }, set: { _ in }),
placeholder: Binding(get: { placeholder ?? composeState.placeholder }, set: { _ in }),
selectedRange: $selectedRange,
onImagesAdded: onMediaAdded
)
@ -78,7 +80,7 @@ struct SendMessageView: View {
deleteTextButton()
}
})
.overlay(alignment: .bottomTrailing, content: {
.overlay(alignment: .bottomTrailing) {
if progressByTimeout {
ProgressView()
.scaleEffect(1.4)
@ -89,7 +91,7 @@ struct SendMessageView: View {
// required for intercepting clicks
.background(.white.opacity(0.000001))
}
})
}
.padding(.vertical, 1)
.background(theme.colors.background)
.clipShape(composeShape)
@ -109,8 +111,10 @@ struct SendMessageView: View {
@ViewBuilder private func composeActionButtons() -> some View {
let vmrs = composeState.voiceMessageRecordingState
if nextSendGrpInv {
inviteMemberContactButton()
if hideSendButton {
EmptyView()
} else if let connect = sendToConnect {
sendToConnectButton(connect)
} else if case .reportedItem = composeState.contextItem {
sendMessageButton()
} else if showVoiceMessageButton
@ -158,20 +162,16 @@ struct SendMessageView: View {
.padding([.top, .trailing], 4)
}
private func inviteMemberContactButton() -> some View {
Button {
sendMessage(nil)
} label: {
private func sendToConnectButton(_ connect: @escaping () -> Void) -> some View {
let disabled = !composeState.sendEnabled || composeState.inProgress || disableSendButton
return Button(action: connect) {
Image(systemName: "arrow.up.circle.fill")
.resizable()
.foregroundColor(sendButtonColor)
.foregroundColor(disabled ? theme.colors.secondary.opacity(0.67) : sendButtonColor)
.frame(width: sendButtonSize, height: sendButtonSize)
.opacity(sendButtonOpacity)
}
.disabled(
!composeState.sendEnabled ||
composeState.inProgress
)
.disabled(disabled)
.frame(width: 31, height: 31)
.padding([.bottom, .trailing], 4)
}

View file

@ -22,7 +22,7 @@ struct GroupChatInfoView: View {
@State var localAlias: String
@FocusState private var aliasTextFieldFocused: Bool
@State private var alert: GroupChatInfoViewAlert? = nil
@State private var groupLink: CreatedConnLink?
@State private var groupLink: GroupLink?
@State private var groupLinkMemberRole: GroupMemberRole = .member
@State private var groupLinkNavLinkActive: Bool = false
@State private var addMembersNavLinkActive: Bool = false
@ -140,36 +140,38 @@ struct GroupChatInfoView: View {
} footer: {
Text("Delete chat messages from your device.")
}
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
if groupInfo.canAddMembers {
if (chat.chatInfo.incognito) {
Label("Invite members", systemImage: "plus")
.foregroundColor(Color(uiColor: .tertiaryLabel))
.onTapGesture { alert = .cantInviteIncognitoAlert }
} else {
addMembersButton()
if !groupInfo.nextConnectPrepared {
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
if groupInfo.canAddMembers {
if (chat.chatInfo.incognito) {
Label("Invite members", systemImage: "plus")
.foregroundColor(Color(uiColor: .tertiaryLabel))
.onTapGesture { alert = .cantInviteIncognitoAlert }
} else {
addMembersButton()
}
}
}
searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary)
.padding(.leading, 8)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let filteredMembers = s == ""
searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary)
.padding(.leading, 8)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let filteredMembers = s == ""
? members
: members.filter { $0.wrapped.localAliasAndFullName.localizedLowercase.contains(s) }
MemberRowView(
chat: chat,
groupInfo: groupInfo,
groupMember: GMember(groupInfo.membership),
scrollToItemId: $scrollToItemId,
user: true,
alert: $alert
)
ForEach(filteredMembers) { member in
MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, scrollToItemId: $scrollToItemId, alert: $alert)
MemberRowView(
chat: chat,
groupInfo: groupInfo,
groupMember: GMember(groupInfo.membership),
scrollToItemId: $scrollToItemId,
user: true,
alert: $alert
)
ForEach(filteredMembers) { member in
MemberRowView(chat: chat, groupInfo: groupInfo, groupMember: member, scrollToItemId: $scrollToItemId, alert: $alert)
}
}
}
Section {
clearChatButton()
if groupInfo.canDelete {
@ -219,8 +221,9 @@ struct GroupChatInfoView: View {
}
sendReceipts = SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault)
do {
if let link = try apiGetGroupLink(groupInfo.groupId) {
(groupLink, groupLinkMemberRole) = link
if let gLink = try apiGetGroupLink(groupInfo.groupId) {
groupLink = gLink
groupLinkMemberRole = gLink.acceptMemberRole
}
} catch let error {
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
@ -884,7 +887,6 @@ struct GroupPreferencesButton: View {
}
}
}
}

View file

@ -12,7 +12,7 @@ import SimpleXChat
struct GroupLinkView: View {
@EnvironmentObject var theme: AppTheme
var groupId: Int64
@Binding var groupLink: CreatedConnLink?
@Binding var groupLink: GroupLink?
@Binding var groupLinkMemberRole: GroupMemberRole
var showTitle: Bool = false
var creatingGroup: Bool = false
@ -78,20 +78,26 @@ struct GroupLinkView: View {
}
}
.frame(height: 36)
SimpleXCreatedLinkQRCode(link: groupLink, short: $showShortLink)
.id("simplex-qrcode-view-for-\(groupLink.simplexChatUri(short: showShortLink))")
SimpleXCreatedLinkQRCode(link: groupLink.connLinkContact, short: $showShortLink)
.id("simplex-qrcode-view-for-\(groupLink.connLinkContact.simplexChatUri(short: showShortLink))")
Button {
showShareSheet(items: [groupLink.simplexChatUri(short: showShortLink)])
showShareSheet(items: [groupLink.connLinkContact.simplexChatUri(short: showShortLink)])
} label: {
Label("Share link", systemImage: "square.and.arrow.up")
}
if (groupLink.connShortLink == nil && UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)) {
if groupLink.connLinkContact.connShortLink == nil {
Button {
addShortLink()
} label: {
Label("Add short link", systemImage: "plus")
}
} else if !groupLink.shortLinkDataSet {
Button {
addShortLink()
} label: {
Label("Share group profile via link", systemImage: "plus")
}
}
if !creatingGroup {
@ -106,8 +112,8 @@ struct GroupLinkView: View {
.disabled(creatingLink)
}
} header: {
if let groupLink, groupLink.connShortLink != nil {
ToggleShortLinkHeader(text: Text(""), link: groupLink, short: $showShortLink)
if let groupLink, groupLink.connLinkContact.connShortLink != nil {
ToggleShortLinkHeader(text: Text(""), link: groupLink.connLinkContact, short: $showShortLink)
}
}
.alert(item: $alert) { alert in
@ -134,7 +140,7 @@ struct GroupLinkView: View {
.onChange(of: groupLinkMemberRole) { _ in
Task {
do {
_ = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole)
groupLink = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole)
} catch let error {
let a = getErrorAlert(error, "Error updating group link")
alert = .error(title: a.title, error: a.message)
@ -155,10 +161,10 @@ struct GroupLinkView: View {
Task {
do {
creatingLink = true
let link = try await apiCreateGroupLink(groupId)
let gLink = try await apiCreateGroupLink(groupId)
await MainActor.run {
creatingLink = false
(groupLink, groupLinkMemberRole) = link
groupLink = gLink
}
} catch let error {
logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))")
@ -175,10 +181,10 @@ struct GroupLinkView: View {
Task {
do {
creatingLink = true
let link = try await apiAddGroupShortLink(groupId)
let gLink = try await apiAddGroupShortLink(groupId)
await MainActor.run {
creatingLink = false
(groupLink, groupLinkMemberRole) = link
groupLink = gLink
}
} catch let error {
logger.error("apiAddGroupShortLink: \(responseError(error))")
@ -194,8 +200,14 @@ struct GroupLinkView: View {
struct GroupLinkView_Previews: PreviewProvider {
static var previews: some View {
@State var groupLink: CreatedConnLink? = CreatedConnLink(connFullLink: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connShortLink: nil)
@State var noGroupLink: CreatedConnLink? = nil
@State var groupLink: GroupLink? = GroupLink(
userContactLinkId: 1,
connLinkContact: CreatedConnLink(connFullLink: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connShortLink: nil),
shortLinkDataSet: false,
groupLinkId: "abc",
acceptMemberRole: .member
)
@State var noGroupLink: GroupLink? = nil
return Group {
GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member))

View file

@ -345,6 +345,7 @@ struct GroupMemberInfoView: View {
Button {
planAndConnect(
contactLink,
theme: theme,
dismiss: true
)
} label: {

View file

@ -122,29 +122,53 @@ struct ChatListNavLink: View {
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }
)
.frameCompat(height: dynamicRowHeight)
.swipeActions(edge: .leading, allowsFullSwipe: true) {
markReadButton()
toggleFavoriteButton()
toggleNtfsButton(chat: chat)
.if(!contact.nextAcceptContactRequest) { v in
v.swipeActions(edge: .leading, allowsFullSwipe: true) {
markReadButton()
toggleFavoriteButton()
toggleNtfsButton(chat: chat)
}
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
tagChatButton(chat)
if !chat.chatItems.isEmpty {
clearChatButton()
if contact.nextAcceptContactRequest,
let contactRequestId = contact.contactRequestId {
Button {
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) }
} label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) }
.tint(theme.colors.primary)
if !ChatModel.shared.addressShortLinkDataSet {
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) }
} label: {
SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI)
}
.tint(.indigo)
}
Button {
AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequestId))
} label: {
SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply", inverted: oneHandUI)
}
.tint(.red)
} else {
tagChatButton(chat)
if !chat.chatItems.isEmpty {
clearChatButton()
}
Button {
deleteContactDialog(
chat,
contact,
dismissToChatList: false,
showAlert: { alert = $0 },
showActionSheet: { actionSheet = $0 },
showSheetContent: { sheet = $0 }
)
} label: {
deleteLabel
}
.tint(.red)
}
Button {
deleteContactDialog(
chat,
contact,
dismissToChatList: false,
showAlert: { alert = $0 },
showActionSheet: { actionSheet = $0 },
showSheetContent: { sheet = $0 }
)
} label: {
deleteLabel
}
.tint(.red)
}
}
}
@ -436,28 +460,32 @@ struct ChatListNavLink: View {
.frameCompat(height: dynamicRowHeight)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) }
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) }
} label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) }
.tint(theme.colors.primary)
Button {
Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) }
} label: {
SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI)
if !ChatModel.shared.addressShortLinkDataSet {
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) }
} label: {
SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI)
}
.tint(.indigo)
}
.tint(.indigo)
Button {
AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest))
AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest.apiId))
} label: {
SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply.fill", inverted: oneHandUI)
SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply", inverted: oneHandUI)
}
.tint(.red)
}
.contentShape(Rectangle())
.onTapGesture { showContactRequestDialog = true }
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } }
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } }
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } }
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } }
if !ChatModel.shared.addressShortLinkDataSet {
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } }
}
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest.apiId) } }
}
}
@ -619,12 +647,12 @@ extension View {
}
}
func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert {
func rejectContactRequestAlert(_ contactRequestId: Int64) -> Alert {
Alert(
title: Text("Reject contact request"),
message: Text("The sender will NOT be notified"),
primaryButton: .destructive(Text("Reject")) {
Task { await rejectContactRequest(contactRequest) }
Task { await rejectContactRequest(contactRequestId) }
},
secondaryButton: .cancel()
)

View file

@ -668,6 +668,7 @@ struct ChatListSearchBar: View {
private func connect(_ link: String) {
planAndConnect(
link,
theme: theme,
dismiss: false,
cleanup: {
searchText = ""

View file

@ -24,7 +24,7 @@ struct ChatPreviewView: View {
var dynamicMediaSize: CGFloat { dynamicSize(userFont).mediaSize }
var dynamicChatInfoSize: CGFloat { dynamicSize(userFont).chatInfoSize }
var body: some View {
let cItem = chat.chatItems.last
return ZStack {
@ -35,7 +35,7 @@ struct ChatPreviewView: View {
.padding([.bottom, .trailing], 1)
}
.padding(.leading, 4)
let chatTs = if let cItem {
cItem.meta.itemTs
} else {
@ -53,7 +53,7 @@ struct ChatPreviewView: View {
}
.padding(.bottom, 4)
.padding(.horizontal, 8)
ZStack(alignment: .topTrailing) {
let chat = activeContentPreview?.chat ?? chat
let ci = activeContentPreview?.ci ?? chat.chatItems.last
@ -88,14 +88,14 @@ struct ChatPreviewView: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.trailing, 8)
Spacer()
}
.frame(maxHeight: .infinity)
}
.opacity(deleting ? 0.4 : 1)
.padding(.bottom, -8)
if deleting {
ProgressView()
.scaleEffect(2)
@ -164,14 +164,26 @@ struct ChatPreviewView: View {
let t = Text(chat.chatInfo.chatViewName).font(.title3).fontWeight(.bold)
switch chat.chatInfo {
case let .direct(contact):
previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(deleting ? Color.secondary : nil)
let color =
deleting
? theme.colors.secondary
: contact.nextAcceptContactRequest || contact.sendMsgToConnect
? theme.colors.primary
: !contact.sndReady
? theme.colors.secondary
: nil
previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(color)
case let .group(groupInfo, _):
let v = previewTitle(t)
switch (groupInfo.membership.memberStatus) {
case .memInvited: v.foregroundColor(deleting ? theme.colors.secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary)
case .memAccepted, .memRejected: v.foregroundColor(theme.colors.secondary)
default: if deleting { v.foregroundColor(theme.colors.secondary) } else { v }
let color = if deleting {
theme.colors.secondary
} else {
switch (groupInfo.membership.memberStatus) {
case .memInvited: chat.chatInfo.incognito ? .indigo : theme.colors.primary
case .memAccepted, .memRejected: theme.colors.secondary
default: groupInfo.nextConnectPrepared ? theme.colors.primary : nil
}
}
previewTitle(t).foregroundColor(color)
default: previewTitle(t)
}
}
@ -251,7 +263,7 @@ struct ChatPreviewView: View {
Color.clear.frame(width: 0)
}
}
private func mentionColor(_ chat: Chat) -> Color {
switch chat.chatInfo.chatSettings?.enableNtfs {
case .all: theme.colors.primary
@ -285,7 +297,7 @@ struct ChatPreviewView: View {
func chatItemPreview(_ cItem: ChatItem) -> (Text, Bool) {
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText()
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
let r = messageText(itemText, itemFormattedText, sender: cItem.memberDisplayName, preview: true, mentions: cItem.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, showSecrets: nil, backgroundColor: UIColor(theme.colors.background), prefix: prefix())
let r = messageText(itemText, itemFormattedText, sender: cItem.meta.showGroupAsSender ? nil : cItem.memberDisplayName, preview: true, mentions: cItem.mentions, userMemberId: chat.chatInfo.groupInfo?.membership.memberId, showSecrets: nil, backgroundColor: UIColor(theme.colors.background), prefix: prefix())
return (Text(AttributedString(r.string)), r.hasSecrets)
// same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey;
@ -312,7 +324,7 @@ struct ChatPreviewView: View {
default: return nil
}
}
func prefix() -> NSAttributedString? {
switch cItem.content.msgContent {
case let .report(_, reason): reason.attrString
@ -325,32 +337,44 @@ struct ChatPreviewView: View {
if chatModel.draftChatId == chat.id, let draft = chatModel.draft {
let (t, hasSecrets) = messageDraft(draft)
chatPreviewLayout(t, draft: true, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets)
} else if cItem?.content.hasMsgContent != true, let previewText = chatPreviewInfoText() {
chatPreviewInfoTextLayout(previewText)
} else if let cItem = cItem {
let (t, hasSecrets) = chatItemPreview(cItem)
chatPreviewLayout(itemStatusMark(cItem) + t, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets)
} else {
switch (chat.chatInfo) {
case let .direct(contact):
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
chatPreviewInfoText("Tap to Connect")
.foregroundColor(theme.colors.primary)
} else if !contact.sndReady && contact.activeConn != nil {
if contact.nextSendGrpInv {
chatPreviewInfoText("send direct message")
} else if contact.active {
chatPreviewInfoText("connecting…")
}
}
case let .group(groupInfo, _):
switch (groupInfo.membership.memberStatus) {
case .memRejected: chatPreviewInfoText("rejected")
case .memInvited: groupInvitationPreviewText(groupInfo)
case .memAccepted: chatPreviewInfoText("connecting…")
case .memPendingReview, .memPendingApproval: chatPreviewInfoText("reviewed by admins")
default: EmptyView()
}
default: EmptyView()
}
}
private func chatPreviewInfoText() -> Text? {
switch (chat.chatInfo) {
case let .direct(contact):
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
Text("Tap to Connect")
.foregroundColor(theme.colors.primary)
} else if contact.sendMsgToConnect {
Text("Open to connect")
} else if contact.nextAcceptContactRequest {
Text("Open to accept")
} else if !contact.sndReady && contact.activeConn != nil && contact.active {
contact.preparedContact?.uiConnLinkType == .con
? Text("contact should accept…")
: Text("connecting…")
} else {
nil
}
case let .group(groupInfo, _):
if groupInfo.nextConnectPrepared {
Text("Open to join")
} else {
switch (groupInfo.membership.memberStatus) {
case .memRejected: Text("rejected")
case .memInvited: groupInvitationPreviewText(groupInfo)
case .memAccepted: Text("connecting…")
case .memPendingReview, .memPendingApproval: Text("reviewed by admins")
default: nil
}
}
default: nil
}
}
@ -400,14 +424,14 @@ struct ChatPreviewView: View {
}
@ViewBuilder private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> some View {
private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> Text {
groupInfo.membership.memberIncognito
? chatPreviewInfoText("join as \(groupInfo.membership.memberProfile.displayName)")
: chatPreviewInfoText("you are invited to group")
? Text("Join as \(groupInfo.membership.memberProfile.displayName)")
: Text("You are invited to group")
}
private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View {
Text(text)
private func chatPreviewInfoTextLayout(_ text: Text) -> some View {
text
.frame(maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .topLeading)
.padding([.leading, .trailing], 8)
.padding(.bottom, 4)
@ -431,7 +455,7 @@ struct ChatPreviewView: View {
let size = dynamicSize(userFont).incognitoSize
switch chat.chatInfo {
case let .direct(contact):
if contact.active && contact.activeConn != nil {
if contact.active, let status = contact.activeConn?.connStatus, status == .ready || status == .sndReady {
NetworkStatusView(contact: contact, size: size)
} else {
incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size)

View file

@ -20,20 +20,17 @@ struct ContactListNavLink: View {
@State private var showContactRequestDialog = false
var body: some View {
let contactType = chatContactType(chat)
Group {
switch (chat.chatInfo) {
case let .direct(contact):
switch contactType {
case .recent:
recentContactNavLink(contact)
case .chatDeleted:
deletedChatNavLink(contact)
case .card:
if contact.nextAcceptContactRequest {
contactWithRequestNavLink(contact)
} else if contact.isContactCard {
contactCardNavLink(contact)
default:
EmptyView()
} else if contact.chatDeleted {
deletedChatNavLink(contact)
} else if contact.active {
recentContactNavLink(contact)
}
case let .contactRequest(contactRequest):
contactRequestNavLink(contactRequest)
@ -59,7 +56,7 @@ struct ContactListNavLink: View {
ItemsModel.shared.loadOpenChat(contact.id)
}
} label: {
contactPreview(contact, titleColor: theme.colors.onBackground)
contactPreview(contact, titleColor: contact.sendMsgToConnect ? theme.colors.primary : theme.colors.onBackground)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
@ -78,6 +75,38 @@ struct ContactListNavLink: View {
}
}
func contactWithRequestNavLink(_ contact: Contact) -> some View {
Button {
dismissAllSheets(animated: true) {
ItemsModel.shared.loadOpenChat(contact.id)
}
} label: {
contactRequestPreview()
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if let contactRequestId = contact.contactRequestId {
Button {
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) }
} label: { Label("Accept", systemImage: "checkmark") }
.tint(theme.colors.primary)
if !ChatModel.shared.addressShortLinkDataSet {
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) }
} label: {
Label("Accept incognito", systemImage: "theatermasks")
}
.tint(.indigo)
}
Button {
alert = SomeAlert(alert: rejectContactRequestAlert(contactRequestId), id: "rejectContactRequestAlert")
} label: {
Label("Reject", systemImage: "multiply")
}
.tint(.red)
}
}
}
func deletedChatNavLink(_ contact: Contact) -> some View {
Button {
Task {
@ -219,36 +248,40 @@ struct ContactListNavLink: View {
Button {
showContactRequestDialog = true
} label: {
contactRequestPreview(contactRequest)
contactRequestPreview()
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) }
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) }
} label: { Label("Accept", systemImage: "checkmark") }
.tint(theme.colors.primary)
Button {
Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) }
} label: {
Label("Accept incognito", systemImage: "theatermasks")
if !ChatModel.shared.addressShortLinkDataSet {
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) }
} label: {
Label("Accept incognito", systemImage: "theatermasks")
}
.tint(.indigo)
}
.tint(.indigo)
Button {
alert = SomeAlert(alert: rejectContactRequestAlert(contactRequest), id: "rejectContactRequestAlert")
alert = SomeAlert(alert: rejectContactRequestAlert(contactRequest.apiId), id: "rejectContactRequestAlert")
} label: {
Label("Reject", systemImage: "multiply")
}
.tint(.red)
}
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } }
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } }
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } }
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } }
if !ChatModel.shared.addressShortLinkDataSet {
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } }
}
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest.apiId) } }
}
}
func contactRequestPreview(_ contactRequest: UserContactRequest) -> some View {
func contactRequestPreview() -> some View {
HStack{
ProfileImage(imageStr: contactRequest.image, size: 30)
ProfileImage(imageStr: chat.chatInfo.image, size: 30)
Text(chat.chatInfo.chatViewName)
.foregroundColor(.accentColor)

View file

@ -89,3 +89,228 @@ func showSheet(
let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default)
let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel)
let alertProfileImageSize: CGFloat = 103
let alertWidth: CGFloat = 270
let alertButtonHeight: CGFloat = 44
class OpenChatAlertViewController: UIViewController {
private let profileName: String
private let profileFullName: String
private let profileImage: UIView
private let cancelTitle: String
private let confirmTitle: String
private let onCancel: () -> Void
private let onConfirm: () -> Void
init(
profileName: String,
profileFullName: String,
profileImage: UIView,
cancelTitle: String = "Cancel",
confirmTitle: String = "Open",
onCancel: @escaping () -> Void,
onConfirm: @escaping () -> Void
) {
self.profileName = profileName
self.profileFullName = profileFullName
self.profileImage = profileImage
self.cancelTitle = cancelTitle
self.confirmTitle = confirmTitle
self.onCancel = onCancel
self.onConfirm = onConfirm
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black.withAlphaComponent(0.3)
// Container view
let containerView = UIView()
containerView.backgroundColor = .systemBackground
containerView.layer.cornerRadius = 12
containerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(containerView)
// Profile image sizing
profileImage.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
profileImage.widthAnchor.constraint(equalToConstant: alertProfileImageSize),
profileImage.heightAnchor.constraint(equalToConstant: alertProfileImageSize)
])
// Name label
let nameLabel = UILabel()
nameLabel.text = profileName
nameLabel.font = UIFont.preferredFont(forTextStyle: .headline)
nameLabel.textColor = .label
nameLabel.numberOfLines = 2
nameLabel.textAlignment = .center
nameLabel.translatesAutoresizingMaskIntoConstraints = false
var profileViews = [profileImage, nameLabel]
// Full name label
if !profileFullName.isEmpty && profileFullName != profileName {
let fullNameLabel = UILabel()
fullNameLabel.text = profileFullName
fullNameLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
fullNameLabel.textColor = .label
fullNameLabel.numberOfLines = 2
fullNameLabel.textAlignment = .center
fullNameLabel.translatesAutoresizingMaskIntoConstraints = false
profileViews.append(fullNameLabel)
}
// Horizontal stack for image + name
let stack = UIStackView(arrangedSubviews: profileViews)
stack.axis = .vertical
stack.spacing = 12
stack.alignment = .center
stack.translatesAutoresizingMaskIntoConstraints = false
let topRowContainer = UIView()
topRowContainer.translatesAutoresizingMaskIntoConstraints = false
topRowContainer.addSubview(stack)
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: topRowContainer.topAnchor),
stack.bottomAnchor.constraint(equalTo: topRowContainer.bottomAnchor),
stack.leadingAnchor.constraint(equalTo: topRowContainer.leadingAnchor, constant: 20),
stack.trailingAnchor.constraint(equalTo: topRowContainer.trailingAnchor, constant: -20)
])
// Buttons
let cancelButton = UIButton(type: .system)
cancelButton.setTitle(cancelTitle, for: .normal)
let bodyDescr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
cancelButton.titleLabel?.font = UIFont(descriptor: bodyDescr.withSymbolicTraits(.traitBold) ?? bodyDescr, size: 0)
cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside)
let confirmButton = UIButton(type: .system)
confirmButton.setTitle(confirmTitle, for: .normal)
confirmButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside)
let verticalButtons = cancelButton.intrinsicContentSize.width + 20 >= alertWidth / 2 || confirmButton.intrinsicContentSize.width + 20 >= alertWidth / 2
// Button stack with equal width buttons
let buttonStack = UIStackView(arrangedSubviews: verticalButtons ? [confirmButton, cancelButton] : [cancelButton, confirmButton])
buttonStack.axis = verticalButtons ? .vertical : .horizontal
buttonStack.distribution = .fillEqually
buttonStack.spacing = 0 // no spacing, use divider instead
buttonStack.translatesAutoresizingMaskIntoConstraints = false
buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: alertButtonHeight * (verticalButtons ? 2 : 1)).isActive = true
// Vertical stack containing hStack and buttonStack
let vStack = UIStackView(arrangedSubviews: [topRowContainer, buttonStack])
vStack.axis = .vertical
vStack.spacing = 16
vStack.alignment = .fill // important: buttons stretch full width
vStack.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(vStack)
// Add horizontal divider above buttons
let horizontalDivider = UIView()
horizontalDivider.backgroundColor = UIColor.separator
horizontalDivider.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(horizontalDivider)
// Add divider between buttons
let buttonDivider = UIView()
buttonDivider.backgroundColor = UIColor.separator
buttonDivider.translatesAutoresizingMaskIntoConstraints = false
buttonStack.addSubview(buttonDivider)
// Constraints
let buttonDividerConstraints = if verticalButtons {
[
buttonDivider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
buttonDivider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
buttonDivider.centerYAnchor.constraint(equalTo: buttonStack.centerYAnchor),
buttonDivider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale)
]
} else {
[
buttonDivider.topAnchor.constraint(equalTo: buttonStack.topAnchor),
buttonDivider.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
buttonDivider.centerXAnchor.constraint(equalTo: buttonStack.centerXAnchor),
buttonDivider.widthAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale)
]
}
NSLayoutConstraint.activate([
// Container view centering and fixed width
containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
containerView.widthAnchor.constraint(equalToConstant: alertWidth),
// Vertical stack padding inside containerView
vStack.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20),
vStack.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0),
vStack.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0),
vStack.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0),
// Center hStack horizontally inside vStack's padded width
stack.centerXAnchor.constraint(equalTo: vStack.centerXAnchor),
// Horizontal divider above buttons
horizontalDivider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
horizontalDivider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
horizontalDivider.bottomAnchor.constraint(equalTo: buttonStack.topAnchor),
horizontalDivider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale)
] + buttonDividerConstraints)
}
@objc private func cancelTapped() {
dismiss(animated: true) {
self.onCancel()
}
}
@objc private func confirmTapped() {
dismiss(animated: true) {
self.onConfirm()
}
}
}
func showOpenChatAlert<Content: View>(
profileName: String,
profileFullName: String,
profileImage: Content,
theme: AppTheme,
cancelTitle: String = "Cancel",
confirmTitle: String = "Open",
onCancel: @escaping () -> Void = {},
onConfirm: @escaping () -> Void
) {
let themedView = profileImage.environmentObject(theme)
let hostingController = UIHostingController(rootView: themedView)
let hostedView = hostingController.view!
hostedView.backgroundColor = .clear
if let topVC = getTopViewController() {
let alertVC = OpenChatAlertViewController(
profileName: profileName,
profileFullName: profileFullName,
profileImage: hostedView,
cancelTitle: cancelTitle,
confirmTitle: confirmTitle,
onCancel: onCancel,
onConfirm: onConfirm
)
topVC.present(alertVC, animated: true)
}
}

View file

@ -23,7 +23,7 @@ struct AddGroupView: View {
@State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil
@State private var showInvalidNameAlert = false
@State private var groupLink: CreatedConnLink?
@State private var groupLink: GroupLink?
@State private var groupLinkMemberRole: GroupMemberRole = .member
var body: some View {

View file

@ -9,10 +9,6 @@
import SwiftUI
import SimpleXChat
enum ContactType: Int {
case card, request, recent, chatDeleted, unlisted
}
struct NewChatMenuButton: View {
// do not use chatModel here because it prevents showing AddGroupMembersView after group creation and QR code after link creation on iOS 16
// @EnvironmentObject var chatModel: ChatModel
@ -42,7 +38,6 @@ private var indent: CGFloat = 36
struct NewChatSheet: View {
@EnvironmentObject var theme: AppTheme
@State private var baseContactTypes: [ContactType] = [.card, .request, .recent]
@EnvironmentObject var chatModel: ChatModel
@State private var searchMode = false
@FocusState var searchFocussed: Bool
@ -60,7 +55,7 @@ struct NewChatSheet: View {
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
var body: some View {
let showArchive = !filterContactTypes(chats: chatModel.chats, contactTypes: [.chatDeleted]).isEmpty
let showArchive = chatModel.chats.contains { $0.chatInfo.contact?.chatDeleted == true }
let v = NavigationView {
viewBody(showArchive)
.navigationTitle("New message")
@ -145,7 +140,7 @@ struct NewChatSheet: View {
}
ContactsList(
baseContactTypes: $baseContactTypes,
chatPredicate: contactListChatPredicate,
searchMode: $searchMode,
searchText: $searchText,
header: "Your Contacts",
@ -156,7 +151,15 @@ struct NewChatSheet: View {
)
}
}
private func contactListChatPredicate(_ chat: Chat, _ withSearch: Bool) -> Bool {
switch chat.chatInfo {
case .contactRequest: true
case let .direct(contact): contact.isContactCard || contact.active || (contact.chatDeleted && withSearch)
default: false
}
}
/// Extends label's tap area to match `.insetGrouped` list row insets
private func navigateOnTap<L: View>(_ label: L, setActive: @escaping () -> Void) -> some View {
label
@ -186,35 +189,24 @@ struct NewChatSheet: View {
}
}
func chatContactType(_ chat: Chat) -> ContactType {
func chatOrderRank(_ chat: Chat) -> Int {
switch chat.chatInfo {
case .contactRequest:
return .request
case .contactRequest: 4
case let .direct(contact):
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
return .card
} else if contact.chatDeleted {
return .chatDeleted
} else if contact.contactStatus == .active {
return .recent
} else {
return .unlisted
}
default:
return .unlisted
}
}
private func filterContactTypes(chats: [Chat], contactTypes: [ContactType]) -> [Chat] {
return chats.filter { chat in
contactTypes.contains(chatContactType(chat))
contact.isContactCard ? 5
: contact.nextAcceptContactRequest ? 4
: contact.nextConnectPrepared ? 3
: contact.active ? 2
: contact.chatDeleted ? 1
: 0
default: 0
}
}
struct ContactsList: View {
@EnvironmentObject var theme: AppTheme
@EnvironmentObject var chatModel: ChatModel
@Binding var baseContactTypes: [ContactType]
var chatPredicate: (Chat, Bool) -> Bool // (chat, search) -> show
@Binding var searchMode: Bool
@Binding var searchText: String
var header: String? = nil
@ -225,8 +217,7 @@ struct ContactsList: View {
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
var body: some View {
let contactTypes = contactTypesSearchTargets(baseContactTypes: baseContactTypes, searchEmpty: searchText.isEmpty)
let contactChats = filterContactTypes(chats: chatModel.chats, contactTypes: contactTypes)
let contactChats = chatModel.chats.filter { chat in chatPredicate(chat, !searchText.isEmpty) }
let filteredContactChats = filteredContactChats(
showUnreadAndFavorites: showUnreadAndFavorites,
searchShowingSimplexLink: searchShowingSimplexLink,
@ -269,26 +260,11 @@ struct ContactsList: View {
.listRowBackground(Color.clear)
.listRowInsets(EdgeInsets(top: 7, leading: 0, bottom: 7, trailing: 0))
}
private func contactTypesSearchTargets(baseContactTypes: [ContactType], searchEmpty: Bool) -> [ContactType] {
if baseContactTypes.contains(.chatDeleted) || searchEmpty {
return baseContactTypes
} else {
return baseContactTypes + [.chatDeleted]
}
}
private func chatsByTypeComparator(chat1: Chat, chat2: Chat) -> Bool {
let chat1Type = chatContactType(chat1)
let chat2Type = chatContactType(chat2)
if chat1Type.rawValue < chat2Type.rawValue {
return true
} else if chat1Type.rawValue > chat2Type.rawValue {
return false
} else {
return chat2.chatInfo.chatTs < chat1.chatInfo.chatTs
}
private func chatComparator(chat1: Chat, chat2: Chat) -> Bool {
let r1 = chatOrderRank(chat1)
let r2 = chatOrderRank(chat2)
return r1 > r2 ? true : r1 < r2 ? false : chat1.chatInfo.chatTs > chat2.chatInfo.chatTs
}
private func filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Bool) -> Bool {
@ -333,7 +309,7 @@ struct ContactsList: View {
}
}
return filteredChats.sorted(by: chatsByTypeComparator)
return filteredChats.sorted(by: chatComparator)
}
}
@ -434,6 +410,7 @@ struct ContactsListSearchBar: View {
private func connect(_ link: String) {
planAndConnect(
link,
theme: theme,
dismiss: true,
cleanup: {
searchText = ""
@ -446,7 +423,6 @@ struct ContactsListSearchBar: View {
struct DeletedChats: View {
@State private var baseContactTypes: [ContactType] = [.chatDeleted]
@State private var searchMode = false
@FocusState var searchFocussed: Bool
@State private var searchText = ""
@ -468,7 +444,7 @@ struct DeletedChats: View {
.frame(maxWidth: .infinity)
ContactsList(
baseContactTypes: $baseContactTypes,
chatPredicate: { chat, _ in chat.chatInfo.contact?.chatDeleted == true },
searchMode: $searchMode,
searchText: $searchText,
searchFocussed: $searchFocussed,

View file

@ -367,7 +367,6 @@ private struct ActiveProfilePicker: View {
.onAppear {
profiles = chatModel.users
.map { $0.user }
.sorted { u, _ in u.activeUser }
}
.onChange(of: incognitoEnabled) { incognito in
if profileSwitchStatus != .switchingIncognito {
@ -425,7 +424,7 @@ private struct ActiveProfilePicker: View {
chatModel.updateContactConnection(conn)
}
do {
try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil )
try await changeActiveUserAsync_(profile.userId, viewPwd: profile.hidden ? trimmedSearchTextOrPassword : nil)
await MainActor.run {
profileSwitchStatus = .idle
dismiss()
@ -559,8 +558,10 @@ private struct ActiveProfilePicker: View {
let activeProfile = filteredProfiles.first { u in u.activeUser }
if let selectedProfile = activeProfile {
let otherProfiles = filteredProfiles.filter { u in u.userId != activeProfile?.userId }
let otherProfiles = filteredProfiles
.filter { u in u.userId != activeProfile?.userId }
.sorted(using: KeyPathComparator<User>(\.activeOrder, order: .reverse))
if incognitoFirst {
incognitoOption
profilerPickerUserOption(selectedProfile)
@ -654,6 +655,7 @@ private struct ConnectView: View {
private func connect(_ link: String) {
planAndConnect(
link,
theme: theme,
dismiss: true
)
}
@ -1003,8 +1005,86 @@ private func showOwnGroupLinkConfirmConnectSheet(
)
}
private func showPrepareContactAlert(
connectionLink: CreatedConnLink,
contactShortLinkData: ContactShortLinkData,
theme: AppTheme,
dismiss: Bool,
cleanup: (() -> Void)?
) {
showOpenChatAlert(
profileName: contactShortLinkData.profile.displayName,
profileFullName: contactShortLinkData.profile.fullName,
profileImage:
ProfileImage(
imageStr: contactShortLinkData.profile.image,
iconName: contactShortLinkData.business ? "briefcase.circle.fill" : "person.crop.circle.fill",
size: alertProfileImageSize
),
theme: theme,
cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"),
confirmTitle: NSLocalizedString("Open chat", comment: "new chat action"),
onCancel: { cleanup?() },
onConfirm: {
Task {
do {
let chat = try await apiPrepareContact(connLink: connectionLink, contactShortLinkData: contactShortLinkData)
await MainActor.run {
ChatModel.shared.addChat(Chat(chat))
openKnownChat(chat.id, dismiss: dismiss, showAlreadyExistsAlert: nil)
cleanup?()
}
} catch let error {
logger.error("showPrepareContactAlert apiPrepareContact error: \(error.localizedDescription)")
showAlert(NSLocalizedString("Error preparing contact", comment: ""), message: responseError(error))
await MainActor.run {
cleanup?()
}
}
}
}
)
}
private func showPrepareGroupAlert(
connectionLink: CreatedConnLink,
groupShortLinkData: GroupShortLinkData,
theme: AppTheme,
dismiss: Bool,
cleanup: (() -> Void)?
) {
showOpenChatAlert(
profileName: groupShortLinkData.groupProfile.displayName,
profileFullName: groupShortLinkData.groupProfile.fullName,
profileImage: ProfileImage(imageStr: groupShortLinkData.groupProfile.image, iconName: "person.2.circle.fill", size: alertProfileImageSize),
theme: theme,
cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"),
confirmTitle: NSLocalizedString("Open group", comment: "new chat action"),
onCancel: { cleanup?() },
onConfirm: {
Task {
do {
let chat = try await apiPrepareGroup(connLink: connectionLink, groupShortLinkData: groupShortLinkData)
await MainActor.run {
ChatModel.shared.addChat(Chat(chat))
openKnownChat(chat.id, dismiss: dismiss, showAlreadyExistsAlert: nil)
cleanup?()
}
} catch let error {
logger.error("showPrepareGroupAlert apiPrepareGroup error: \(error.localizedDescription)")
showAlert(NSLocalizedString("Error preparing group", comment: ""), message: responseError(error))
await MainActor.run {
cleanup?()
}
}
}
}
)
}
func planAndConnect(
_ shortOrFullLink: String,
theme: AppTheme,
dismiss: Bool,
cleanup: (() -> Void)? = nil,
filterKnownContact: ((Contact) -> Void)? = nil,
@ -1016,16 +1096,29 @@ func planAndConnect(
switch connectionPlan {
case let .invitationLink(ilp):
switch ilp {
case .ok:
logger.debug("planAndConnect, .invitationLink, .ok")
await MainActor.run {
showAskCurrentOrIncognitoProfileSheet(
title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"),
connectionLink: connectionLink,
connectionPlan: connectionPlan,
dismiss: dismiss,
cleanup: cleanup
)
case let .ok(contactSLinkData_):
if let contactSLinkData = contactSLinkData_ {
logger.debug("planAndConnect, .invitationLink, .ok, short link data present")
await MainActor.run {
showPrepareContactAlert(
connectionLink: connectionLink,
contactShortLinkData: contactSLinkData,
theme: theme,
dismiss: dismiss,
cleanup: cleanup
)
}
} else {
logger.debug("planAndConnect, .invitationLink, .ok, no short link data")
await MainActor.run {
showAskCurrentOrIncognitoProfileSheet(
title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"),
connectionLink: connectionLink,
connectionPlan: connectionPlan,
dismiss: dismiss,
cleanup: cleanup
)
}
}
case .ownLink:
logger.debug("planAndConnect, .invitationLink, .ownLink")
@ -1064,16 +1157,29 @@ func planAndConnect(
}
case let .contactAddress(cap):
switch cap {
case .ok:
logger.debug("planAndConnect, .contactAddress, .ok")
await MainActor.run {
showAskCurrentOrIncognitoProfileSheet(
title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"),
connectionLink: connectionLink,
connectionPlan: connectionPlan,
dismiss: dismiss,
cleanup: cleanup
)
case let .ok(contactSLinkData_):
if let contactSLinkData = contactSLinkData_ {
logger.debug("planAndConnect, .contactAddress, .ok, short link data present")
await MainActor.run {
showPrepareContactAlert(
connectionLink: connectionLink,
contactShortLinkData: contactSLinkData,
theme: theme,
dismiss: dismiss,
cleanup: cleanup
)
}
} else {
logger.debug("planAndConnect, .contactAddress, .ok, no short link data")
await MainActor.run {
showAskCurrentOrIncognitoProfileSheet(
title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"),
connectionLink: connectionLink,
connectionPlan: connectionPlan,
dismiss: dismiss,
cleanup: cleanup
)
}
}
case .ownLink:
logger.debug("planAndConnect, .contactAddress, .ownLink")
@ -1129,15 +1235,29 @@ func planAndConnect(
}
case let .groupLink(glp):
switch glp {
case .ok:
await MainActor.run {
showAskCurrentOrIncognitoProfileSheet(
title: NSLocalizedString("Join group", comment: "new chat sheet title"),
connectionLink: connectionLink,
connectionPlan: connectionPlan,
dismiss: dismiss,
cleanup: cleanup
)
case let .ok(groupSLinkData_):
if let groupSLinkData = groupSLinkData_ {
logger.debug("planAndConnect, .groupLink, .ok, short link data present")
await MainActor.run {
showPrepareGroupAlert(
connectionLink: connectionLink,
groupShortLinkData: groupSLinkData,
theme: theme,
dismiss: dismiss,
cleanup: cleanup
)
}
} else {
logger.debug("planAndConnect, .groupLink, .ok, no short link data")
await MainActor.run {
showAskCurrentOrIncognitoProfileSheet(
title: NSLocalizedString("Join group", comment: "new chat sheet title"),
connectionLink: connectionLink,
connectionPlan: connectionPlan,
dismiss: dismiss,
cleanup: cleanup
)
}
}
case let .ownLink(groupInfo):
logger.debug("planAndConnect, .groupLink, .ownLink")
@ -1255,36 +1375,28 @@ private func connectViaLink(
}
func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
let m = ChatModel.shared
if let c = m.getContactChat(contact.contactId) {
if dismiss {
dismissAllSheets(animated: true) {
ItemsModel.shared.loadOpenChat(c.id) {
showAlreadyExistsAlert?()
}
}
} else {
ItemsModel.shared.loadOpenChat(c.id) {
showAlreadyExistsAlert?()
}
}
if let c = ChatModel.shared.getContactChat(contact.contactId) {
openKnownChat(c.id, dismiss: dismiss, showAlreadyExistsAlert: showAlreadyExistsAlert)
}
}
func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
let m = ChatModel.shared
if let g = m.getGroupChat(groupInfo.groupId) {
if dismiss {
dismissAllSheets(animated: true) {
ItemsModel.shared.loadOpenChat(g.id) {
showAlreadyExistsAlert?()
}
}
} else {
ItemsModel.shared.loadOpenChat(g.id) {
if let g = ChatModel.shared.getGroupChat(groupInfo.groupId) {
openKnownChat(g.id, dismiss: dismiss, showAlreadyExistsAlert: showAlreadyExistsAlert)
}
}
func openKnownChat(_ chatId: ChatId, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
if dismiss {
dismissAllSheets(animated: true) {
ItemsModel.shared.loadOpenChat(chatId) {
showAlreadyExistsAlert?()
}
}
} else {
ItemsModel.shared.loadOpenChat(chatId) {
showAlreadyExistsAlert?()
}
}
}

View file

@ -77,9 +77,9 @@ struct CreateSimpleXAddress: View {
progressIndicator = true
Task {
do {
let connLinkContact = try await apiCreateUserAddress(short: false)
let connLinkContact = try await apiCreateUserAddress()
DispatchQueue.main.async {
m.userAddress = UserContactLink(connLinkContact: connLinkContact)
m.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: connLinkContact.connShortLink != nil, addressSettings: AddressSettings(businessAddress: false))
}
await MainActor.run { progressIndicator = false }
} catch let error {

View file

@ -20,7 +20,6 @@ struct PrivacySettings: View {
@AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@AppStorage(DEFAULT_PRIVACY_SHORT_LINKS) private var shortSimplexLinks = false
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@State private var currentLAMode = privacyLocalAuthModeDefault.get()
@ -101,11 +100,6 @@ struct PrivacySettings: View {
.onChange(of: simplexLinkMode) { mode in
privacySimplexLinkModeDefault.set(mode)
}
if developerTools {
settingsRow("link.badge.plus", color: theme.colors.secondary) {
Toggle("Use short links (BETA)", isOn: $shortSimplexLinks)
}
}
} header: {
Text("Chats")
.foregroundColor(theme.colors.secondary)

View file

@ -32,7 +32,6 @@ let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" // deprecated, moved t
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews"
let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft"
let DEFAULT_PRIVACY_SHORT_LINKS = "privacyShortLinks"
let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen"
let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet"
let DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS = "privacyMediaBlurRadius"
@ -100,7 +99,6 @@ let appDefaults: [String: Any] = [
DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue,
DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true,
DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true,
DEFAULT_PRIVACY_SHORT_LINKS: false,
DEFAULT_PRIVACY_PROTECT_SCREEN: false,
DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false,
DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS: 0,

View file

@ -17,8 +17,8 @@ struct UserAddressView: View {
@State var shareViaProfile = false
@State var autoCreate = false
@State private var showShortLink = true
@State private var aas = AutoAcceptState()
@State private var savedAAS = AutoAcceptState()
@State private var settings = AddressSettingsState()
@State private var savedSettings = AddressSettingsState()
@State private var showMailView = false
@State private var mailViewResult: Result<MFMailComposeResult, Error>? = nil
@State private var alert: UserAddressAlert?
@ -66,8 +66,8 @@ struct UserAddressView: View {
if let userAddress = chatModel.userAddress {
existingAddressView(userAddress)
.onAppear {
aas = AutoAcceptState(userAddress: userAddress)
savedAAS = aas
settings = AddressSettingsState(settings: userAddress.addressSettings)
savedSettings = AddressSettingsState(settings: userAddress.addressSettings)
}
} else {
Section {
@ -143,23 +143,25 @@ struct UserAddressView: View {
// shareViaEmailButton(userAddress)
// }
settingsRow("briefcase", color: theme.colors.secondary) {
Toggle("Business address", isOn: $aas.business)
.onChange(of: aas.business) { ba in
Toggle("Business address", isOn: $settings.businessAddress)
.onChange(of: settings.businessAddress) { ba in
if ba {
aas.enable = true
aas.incognito = false
settings.autoAccept = true
settings.autoAcceptIncognito = false
}
saveAAS($aas, $savedAAS)
saveAddressSettings(settings, $savedSettings)
}
}
addressSettingsButton(userAddress)
if (userAddress.connLinkContact.connShortLink == nil && UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)) {
if userAddress.connLinkContact.connShortLink == nil {
addShortLinkButton()
} else if !userAddress.shortLinkDataSet {
addProfileToShortLinkButton()
}
} header: {
ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink)
} footer: {
if aas.business {
if settings.businessAddress {
Text("Add your team members to the conversations.")
.foregroundColor(theme.colors.secondary)
}
@ -196,10 +198,9 @@ struct UserAddressView: View {
progressIndicator = true
Task {
do {
let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)
let connLinkContact = try await apiCreateUserAddress(short: short)
let connLinkContact = try await apiCreateUserAddress()
DispatchQueue.main.async {
chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact)
chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: connLinkContact.connShortLink != nil, addressSettings: AddressSettings(businessAddress: false))
alert = .shareOnCreate
progressIndicator = false
}
@ -214,12 +215,34 @@ struct UserAddressView: View {
private func addShortLinkButton() -> some View {
Button {
addShortLink()
showAddShortLinkAlert()
} label: {
Label("Add short link", systemImage: "plus")
settingsRow("plus", color: theme.colors.primary) {
Text("Add short link")
}
}
}
private func addProfileToShortLinkButton() -> some View {
Button {
showAddShortLinkAlert()
} label: {
settingsRow("plus", color: theme.colors.primary) {
Text("Share profile via link")
}
}
}
private func showAddShortLinkAlert() {
showAlert(
title: NSLocalizedString("Share profile via link", comment: "alert title"),
message: NSLocalizedString("Profile will be shared via the address link.", comment: "alert message"),
buttonTitle: NSLocalizedString("Share profile", comment: "alert button"),
buttonAction: { addShortLink() },
cancelButton: true
)
}
private func addShortLink() {
progressIndicator = true
Task {
@ -231,7 +254,7 @@ struct UserAddressView: View {
await MainActor.run { progressIndicator = false }
} catch let error {
logger.error("apiAddMyAddressShortLink: \(responseError(error))")
let a = getErrorAlert(error, "Error creating address")
let a = getErrorAlert(error, "Error adding address short link")
alert = .error(title: a.title, error: a.message)
await MainActor.run { progressIndicator = false }
}
@ -329,7 +352,7 @@ struct ToggleShortLinkHeader: View {
let text: Text
var link: CreatedConnLink
@Binding var short: Bool
var body: some View {
if link.connShortLink == nil {
text.foregroundColor(theme.colors.secondary)
@ -346,45 +369,30 @@ struct ToggleShortLinkHeader: View {
}
}
private struct AutoAcceptState: Equatable {
var enable = false
var incognito = false
var business = false
var welcomeText = ""
struct AddressSettingsState: Equatable {
var businessAddress = false
var welcomeMessage = ""
var autoAccept = false
var autoAcceptIncognito = false
var autoReply = ""
init(enable: Bool = false, incognito: Bool = false, business: Bool = false, welcomeText: String = "") {
self.enable = enable
self.incognito = incognito
self.business = business
self.welcomeText = welcomeText
init() {}
init(settings: AddressSettings) {
self.businessAddress = settings.businessAddress
self.welcomeMessage = settings.welcomeMessage ?? ""
self.autoAccept = settings.autoAccept != nil
self.autoAcceptIncognito = settings.autoAccept?.acceptIncognito == true
self.autoReply = settings.autoReply?.text ?? ""
}
init(userAddress: UserContactLink) {
if let aa = userAddress.autoAccept {
enable = true
incognito = aa.acceptIncognito
business = aa.businessAddress
if let msg = aa.autoReply {
welcomeText = msg.text
} else {
welcomeText = ""
}
} else {
enable = false
incognito = false
business = false
welcomeText = ""
}
}
var autoAccept: AutoAccept? {
if enable {
var autoReply: MsgContent? = nil
let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines)
if s != "" { autoReply = .text(s) }
return AutoAccept(businessAddress: business, acceptIncognito: incognito, autoReply: autoReply)
}
return nil
var addressSettings: AddressSettings {
AddressSettings(
businessAddress: self.businessAddress,
welcomeMessage: self.welcomeMessage.isEmpty ? nil : self.welcomeMessage,
autoAccept: self.autoAccept ? AutoAccept(acceptIncognito: self.autoAcceptIncognito) : nil,
autoReply: self.autoReply.isEmpty ? nil : MsgContent.text(self.autoReply)
)
}
}
@ -409,30 +417,32 @@ struct UserAddressSettingsView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var theme: AppTheme
@Binding var shareViaProfile: Bool
@State private var aas = AutoAcceptState()
@State private var savedAAS = AutoAcceptState()
@State private var settings = AddressSettingsState()
@State private var savedSettings = AddressSettingsState()
@State private var ignoreShareViaProfileChange = false
@State private var progressIndicator = false
@FocusState private var keyboardVisible: Bool
var body: some View {
ZStack {
if let userAddress = ChatModel.shared.userAddress {
userAddressSettingsView()
.onAppear {
aas = AutoAcceptState(userAddress: userAddress)
savedAAS = aas
settings = AddressSettingsState(settings: userAddress.addressSettings)
savedSettings = AddressSettingsState(settings: userAddress.addressSettings)
}
.onChange(of: aas.enable) { aasEnabled in
if !aasEnabled { aas = AutoAcceptState() }
.onChange(of: settings.autoAccept) { autoAccept in
if !autoAccept {
settings.businessAddress = false
settings.autoReply = ""
}
}
.onDisappear {
if savedAAS != aas {
if savedSettings != settings {
showAlert(
title: NSLocalizedString("Auto-accept settings", comment: "alert title"),
title: NSLocalizedString("SimpleX address settings", comment: "alert title"),
message: NSLocalizedString("Settings were changed.", comment: "alert message"),
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
buttonAction: { saveAAS($aas, $savedAAS) },
buttonAction: { saveAddressSettings(settings, $savedSettings) },
cancelButton: true
)
}
@ -450,12 +460,26 @@ struct UserAddressSettingsView: View {
List {
Section {
shareWithContactsButton()
autoAcceptToggle().disabled(aas.business)
autoAcceptToggle().disabled(settings.businessAddress)
}
if aas.enable {
Section {
messageEditor(placeholder: NSLocalizedString("Enter welcome message… (optional)", comment: "placeholder"), text: $settings.welcomeMessage)
} header: {
Text("Welcome message")
.foregroundColor(theme.colors.secondary)
} footer: {
Text("Shown to your contact before connection.")
}
if settings.autoAccept {
autoAcceptSection()
}
Section {
saveAddressSettingsButton()
.disabled(settings == savedSettings)
}
}
}
@ -518,46 +542,45 @@ struct UserAddressSettingsView: View {
private func autoAcceptToggle() -> some View {
settingsRow("checkmark", color: theme.colors.secondary) {
Toggle("Auto-accept", isOn: $aas.enable)
.onChange(of: aas.enable) { _ in
saveAAS($aas, $savedAAS)
Toggle("Auto-accept", isOn: $settings.autoAccept)
.onChange(of: settings.autoAccept) { _ in
saveAddressSettings(settings, $savedSettings)
}
}
}
private func autoAcceptSection() -> some View {
Section {
if !aas.business {
if !ChatModel.shared.addressShortLinkDataSet && !settings.businessAddress {
acceptIncognitoToggle()
}
welcomeMessageEditor()
saveAASButton()
.disabled(aas == savedAAS)
messageEditor(placeholder: NSLocalizedString("Enter auto-reply message… (optional)", comment: "placeholder"), text: $settings.autoReply)
} header: {
Text("Auto-accept")
.foregroundColor(theme.colors.secondary)
} footer: {
Text("Sent to your contact after connection.")
}
}
private func acceptIncognitoToggle() -> some View {
settingsRow(
aas.incognito ? "theatermasks.fill" : "theatermasks",
color: aas.incognito ? .indigo : theme.colors.secondary
settings.autoAcceptIncognito ? "theatermasks.fill" : "theatermasks",
color: settings.autoAcceptIncognito ? .indigo : theme.colors.secondary
) {
Toggle("Accept incognito", isOn: $aas.incognito)
Toggle("Accept incognito", isOn: $settings.autoAcceptIncognito)
}
}
private func welcomeMessageEditor() -> some View {
private func messageEditor(placeholder: String, text: Binding<String>) -> some View {
ZStack {
Group {
if aas.welcomeText.isEmpty {
TextEditor(text: Binding.constant(NSLocalizedString("Enter welcome message… (optional)", comment: "placeholder")))
if text.wrappedValue.isEmpty {
TextEditor(text: Binding.constant(placeholder))
.foregroundColor(theme.colors.secondary)
.disabled(true)
}
TextEditor(text: $aas.welcomeText)
.focused($keyboardVisible)
TextEditor(text: text)
}
.padding(.horizontal, -5)
.padding(.top, -8)
@ -566,27 +589,27 @@ struct UserAddressSettingsView: View {
}
}
private func saveAASButton() -> some View {
private func saveAddressSettingsButton() -> some View {
Button {
keyboardVisible = false
saveAAS($aas, $savedAAS)
hideKeyboard()
saveAddressSettings(settings, $savedSettings)
} label: {
Text("Save")
}
}
}
private func saveAAS(_ aas: Binding<AutoAcceptState>, _ savedAAS: Binding<AutoAcceptState>) {
private func saveAddressSettings(_ settings: AddressSettingsState, _ savedSettings: Binding<AddressSettingsState>) {
Task {
do {
if let address = try await userAddressAutoAccept(aas.wrappedValue.autoAccept) {
if let address = try await apiSetUserAddressSettings(settings.addressSettings) {
await MainActor.run {
ChatModel.shared.userAddress = address
savedAAS.wrappedValue = aas.wrappedValue
savedSettings.wrappedValue = settings
}
}
} catch let error {
logger.error("userAddressAutoAccept error: \(responseError(error))")
logger.error("apiSetUserAddressSettings error: \(responseError(error))")
}
}
}
@ -594,9 +617,13 @@ private func saveAAS(_ aas: Binding<AutoAcceptState>, _ savedAAS: Binding<AutoAc
struct UserAddressView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
chatModel.userAddress = UserContactLink(connLinkContact: CreatedConnLink(connFullLink: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connShortLink: nil))
chatModel.userAddress = UserContactLink(
connLinkContact: CreatedConnLink(connFullLink: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connShortLink: nil),
shortLinkDataSet: false,
addressSettings: AddressSettings(businessAddress: false)
)
return Group {
UserAddressView()
.environmentObject(chatModel)

View file

@ -3618,7 +3618,7 @@ SimpleX servers cannot see your profile.</source>
<source>italic</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -3800,8 +3800,8 @@ SimpleX servers cannot see your profile.</source>
<target state="translated">نعم</target>
<note>pref value</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve" approved="no">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve" approved="no">
<source>You are invited to group</source>
<target state="translated">أنت مدعو إلى المجموعة</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -4587,8 +4587,8 @@ SimpleX servers cannot see your profile.</source>
<source>Ask</source>
<target state="translated">اسأل</target>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve" approved="no">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve" approved="no">
<source>SimpleX address settings</source>
<target state="translated">إعدادات القبول التلقائي</target>
</trans-unit>
<trans-unit id="Better message dates." xml:space="preserve" approved="no">

View file

@ -1139,8 +1139,8 @@ swipe action</note>
<target>Автоматично приемане на изображения</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>Автоматично приемане на настройки</target>
<note>alert title</note>
</trans-unit>
@ -9202,7 +9202,7 @@ pref value</note>
<target>курсив</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>присъединяване като %@</target>
<note>No comment provided by engineer.</note>
@ -9469,8 +9469,8 @@ time to disappear</note>
<target>кодът за сигурност е променен</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>изпрати лично съобщение</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -9620,8 +9620,8 @@ last received msg: %2$@</source>
<source>you accepted this member</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>вие сте поканени в групата</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -4351,7 +4351,7 @@ SimpleX servers cannot see your profile.</source>
<source>italic</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -4548,8 +4548,8 @@ SimpleX servers cannot see your profile.</source>
<source>yes</source>
<note>pref value</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you are observer" xml:space="preserve">

View file

@ -1094,8 +1094,8 @@ swipe action</note>
<target>Automaticky přijímat obrázky</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
@ -8899,7 +8899,7 @@ pref value</note>
<target>kurzíva</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>připojit se jako %@</target>
<note>No comment provided by engineer.</note>
@ -9159,8 +9159,8 @@ time to disappear</note>
<target>bezpečnostní kód změněn</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>odeslat přímou zprávu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -9302,8 +9302,8 @@ last received msg: %2$@</source>
<source>you accepted this member</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>jste pozváni do skupiny</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -1170,8 +1170,8 @@ swipe action</note>
<target>Bilder automatisch akzeptieren</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>Einstellungen automatisch akzeptieren</target>
<note>alert title</note>
</trans-unit>
@ -9747,7 +9747,7 @@ pref value</note>
<target>kursiv</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>beitreten als %@</target>
<note>No comment provided by engineer.</note>
@ -10030,8 +10030,8 @@ time to disappear</note>
<target>Sicherheitscode wurde geändert</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>Direktnachricht senden</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -10189,8 +10189,8 @@ Zuletzt empfangene Nachricht: %2$@</target>
<target>Sie haben dieses Mitglied übernommen</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>Sie sind zu der Gruppe eingeladen</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -3940,7 +3940,7 @@ SimpleX servers cannot see your profile.</source>
<source>italic</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -4117,8 +4117,8 @@ SimpleX servers cannot see your profile.</source>
<source>yes</source>
<note>pref value</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you are observer" xml:space="preserve">

View file

@ -1171,9 +1171,9 @@ swipe action</note>
<target>Auto-accept images</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<target>Auto-accept settings</target>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>SimpleX address settings</target>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
@ -9752,7 +9752,7 @@ pref value</note>
<target>italic</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>join as %@</target>
<note>No comment provided by engineer.</note>
@ -10035,9 +10035,9 @@ time to disappear</note>
<target>security code changed</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<target>send direct message</target>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>send to connect</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="server queue info: %@&#10;&#10;last received msg: %@" xml:space="preserve">
@ -10194,9 +10194,9 @@ last received msg: %2$@</target>
<target>you accepted this member</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<target>you are invited to group</target>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>You are invited to group</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you are observer" xml:space="preserve">

View file

@ -1170,8 +1170,8 @@ swipe action</note>
<target>Aceptar imágenes automáticamente</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>Auto aceptar configuración</target>
<note>alert title</note>
</trans-unit>
@ -9747,7 +9747,7 @@ pref value</note>
<target>cursiva</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>unirte como %@</target>
<note>No comment provided by engineer.</note>
@ -10030,8 +10030,8 @@ time to disappear</note>
<target>código de seguridad cambiado</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>Enviar mensaje directo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -10189,8 +10189,8 @@ last received msg: %2$@</source>
<target>has aceptado al miembro</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>has sido invitado a un grupo</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -1074,8 +1074,8 @@ swipe action</note>
<target>Hyväksy kuvat automaattisesti</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
@ -8869,7 +8869,7 @@ pref value</note>
<target>kursivoitu</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>Liity %@:nä</target>
<note>No comment provided by engineer.</note>
@ -9129,8 +9129,8 @@ time to disappear</note>
<target>turvakoodi on muuttunut</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="server queue info: %@&#10;&#10;last received msg: %@" xml:space="preserve">
@ -9271,8 +9271,8 @@ last received msg: %2$@</source>
<source>you accepted this member</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>sinut on kutsuttu ryhmään</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -1165,8 +1165,8 @@ swipe action</note>
<target>Images auto-acceptées</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>Paramètres de réception automatique</target>
<note>alert title</note>
</trans-unit>
@ -9638,7 +9638,7 @@ pref value</note>
<target>italique</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>rejoindre entant que %@</target>
<note>No comment provided by engineer.</note>
@ -9910,8 +9910,8 @@ time to disappear</note>
<target>code de sécurité modifié</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>envoyer un message direct</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -10068,8 +10068,8 @@ dernier message reçu: %2$@</target>
<source>you accepted this member</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>vous êtes invité·e au groupe</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -4466,7 +4466,7 @@ SimpleX servers cannot see your profile.</source>
<source>italic</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -4643,8 +4643,8 @@ SimpleX servers cannot see your profile.</source>
<source>yes</source>
<note>pref value</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you are observer" xml:space="preserve">

View file

@ -3412,7 +3412,7 @@ SimpleX servers cannot see your profile.</source>
<source>italic</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -3577,8 +3577,8 @@ SimpleX servers cannot see your profile.</source>
<source>yes</source>
<note>pref value</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you changed address" xml:space="preserve">

View file

@ -1170,8 +1170,8 @@ swipe action</note>
<target>Képek automatikus elfogadása</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>Beállítások automatikus elfogadása</target>
<note>alert title</note>
</trans-unit>
@ -9747,7 +9747,7 @@ pref value</note>
<target>dőlt</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>csatlakozás mint %@</target>
<note>No comment provided by engineer.</note>
@ -10030,8 +10030,8 @@ time to disappear</note>
<target>a biztonsági kód módosult</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>közvetlen üzenet küldése</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -10189,8 +10189,8 @@ utoljára fogadott üzenet: %2$@</target>
<target>Ön befogadta ezt a tagot</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>Ön meghívást kapott a csoportba</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -1170,8 +1170,8 @@ swipe action</note>
<target>Auto-accetta le immagini</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>Accetta automaticamente le impostazioni</target>
<note>alert title</note>
</trans-unit>
@ -9747,7 +9747,7 @@ pref value</note>
<target>corsivo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>entra come %@</target>
<note>No comment provided by engineer.</note>
@ -10030,8 +10030,8 @@ time to disappear</note>
<target>codice di sicurezza modificato</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>invia messaggio diretto</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -10189,8 +10189,8 @@ ultimo msg ricevuto: %2$@</target>
<target>hai accettato questo membro</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>sei stato/a invitato/a al gruppo</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -1123,8 +1123,8 @@ swipe action</note>
<target>画像を自動的に受信</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
@ -8940,7 +8940,7 @@ pref value</note>
<target>斜体</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>%@ として参加</target>
<note>No comment provided by engineer.</note>
@ -9200,8 +9200,8 @@ time to disappear</note>
<target>セキュリティコードが変更されました</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="server queue info: %@&#10;&#10;last received msg: %@" xml:space="preserve">
@ -9342,8 +9342,8 @@ last received msg: %2$@</source>
<source>you accepted this member</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>グループ招待が届きました</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -3696,7 +3696,7 @@ SimpleX servers cannot see your profile.</source>
<source>italic</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -3873,8 +3873,8 @@ SimpleX servers cannot see your profile.</source>
<source>yes</source>
<note>pref value</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you are observer" xml:space="preserve">
@ -4641,8 +4641,8 @@ This is your own one-time link!</source>
<source>All new messages from %@ will be hidden!</source>
<target state="translated">%@로부터의 모든 새 메세지가 숨겨집니다!</target>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve" approved="no">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve" approved="no">
<source>SimpleX address settings</source>
<target state="translated">자동-수락 설정</target>
</trans-unit>
<trans-unit id="Archive contacts to chat later." xml:space="preserve" approved="no">

View file

@ -3439,7 +3439,7 @@ SimpleX servers cannot see your profile.</source>
<source>italic</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -3616,8 +3616,8 @@ SimpleX servers cannot see your profile.</source>
<source>yes</source>
<note>pref value</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you are observer" xml:space="preserve">

View file

@ -394,8 +394,8 @@
<source>- connect to [directory service](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
- delivery receipts (up to 20 members).
- faster and more stable.</source>
<target>- verbinding maken met [directory service](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
- ontvangst bevestiging(tot 20 leden).
<target>- verbinding maken met [directory service](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!
- ontvangst bevestiging(tot 20 leden).
- sneller en stabieler.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -1170,8 +1170,8 @@ swipe action</note>
<target>Afbeeldingen automatisch accepteren</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>Instellingen automatisch accepteren</target>
<note>alert title</note>
</trans-unit>
@ -5452,7 +5452,7 @@ Dit is jouw link voor groep %@!</target>
<source>Now admins can:
- delete members' messages.
- disable members ("observer" role)</source>
<target>Nu kunnen beheerders:
<target>Nu kunnen beheerders:
- berichten van leden verwijderen.
- schakel leden uit ("waarnemer" rol)</target>
<note>No comment provided by engineer.</note>
@ -9747,7 +9747,7 @@ pref value</note>
<target>cursief</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>deelnemen als %@</target>
<note>No comment provided by engineer.</note>
@ -10030,8 +10030,8 @@ time to disappear</note>
<target>beveiligingscode gewijzigd</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>stuur een direct bericht</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -10189,8 +10189,8 @@ laatst ontvangen bericht: %2$@</target>
<target>je hebt dit lid geaccepteerd</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>je bent uitgenodigd voor de groep</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -1166,8 +1166,8 @@ swipe action</note>
<target>Automatyczne akceptowanie obrazów</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>Ustawienia automatycznej akceptacji</target>
<note>alert title</note>
</trans-unit>
@ -9504,7 +9504,7 @@ pref value</note>
<target>kursywa</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>dołącz jako %@</target>
<note>No comment provided by engineer.</note>
@ -9775,8 +9775,8 @@ time to disappear</note>
<target>kod bezpieczeństwa zmieniony</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>wyślij wiadomość bezpośrednią</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -9933,8 +9933,8 @@ ostatnia otrzymana wiadomość: %2$@</target>
<source>you accepted this member</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>jesteś zaproszony do grupy</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -3934,7 +3934,7 @@ SimpleX servers cannot see your profile.</source>
<target state="translated">itálico</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -4130,8 +4130,8 @@ SimpleX servers cannot see your profile.</source>
<target state="translated">sim</target>
<note>pref value</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve" approved="no">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve" approved="no">
<source>You are invited to group</source>
<target state="translated">você está convidado para o grupo</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -5565,8 +5565,8 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi
<source>Chat migrated!</source>
<target state="translated">Conversa migrada!</target>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve" approved="no">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve" approved="no">
<source>SimpleX address settings</source>
<target state="translated">Aceitar automaticamente configurações</target>
</trans-unit>
<trans-unit id="App encrypts new local files (except videos)." xml:space="preserve" approved="no">

View file

@ -4043,7 +4043,7 @@ SimpleX servers cannot see your profile.</source>
<source>italic</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -4220,8 +4220,8 @@ SimpleX servers cannot see your profile.</source>
<source>yes</source>
<note>pref value</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="you are observer" xml:space="preserve">

View file

@ -1170,8 +1170,8 @@ swipe action</note>
<target>Автоприем изображений</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>Настройки автоприема</target>
<note>alert title</note>
</trans-unit>
@ -9746,7 +9746,7 @@ pref value</note>
<target>курсив</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>вступить как %@</target>
<note>No comment provided by engineer.</note>
@ -10029,8 +10029,8 @@ time to disappear</note>
<target>код безопасности изменился</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>отправьте сообщение</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -10188,8 +10188,8 @@ last received msg: %2$@</source>
<target>Вы приняли этого члена</target>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>Вы приглашены в группу</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -1066,8 +1066,8 @@ swipe action</note>
<target>ยอมรับภาพอัตโนมัติ</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<note>alert title</note>
</trans-unit>
<trans-unit id="Back" xml:space="preserve">
@ -8836,7 +8836,7 @@ pref value</note>
<target>ตัวเอียง</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>เข้าร่วมเป็น %@</target>
<note>No comment provided by engineer.</note>
@ -9096,8 +9096,8 @@ time to disappear</note>
<target>เปลี่ยนรหัสความปลอดภัยแล้ว</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="server queue info: %@&#10;&#10;last received msg: %@" xml:space="preserve">
@ -9238,8 +9238,8 @@ last received msg: %2$@</source>
<source>you accepted this member</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>คุณได้รับเชิญให้เข้าร่วมกลุ่ม</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -1158,8 +1158,8 @@ swipe action</note>
<target>Fotoğrafları otomatik kabul et</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>Ayarları otomatik olarak kabul et</target>
<note>alert title</note>
</trans-unit>
@ -9528,7 +9528,7 @@ pref value</note>
<target>italik</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>%@ olarak katıl</target>
<note>No comment provided by engineer.</note>
@ -9799,8 +9799,8 @@ time to disappear</note>
<target>güvenlik kodu değiştirildi</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>doğrudan mesaj gönder</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -9957,8 +9957,8 @@ son alınan msj: %2$@</target>
<source>you accepted this member</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>gruba davet edildiniz</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -1170,8 +1170,8 @@ swipe action</note>
<target>Автоматичне прийняття зображень</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>Автоприйняття налаштувань</target>
<note>alert title</note>
</trans-unit>
@ -1860,14 +1860,14 @@ set passcode view</note>
<trans-unit id="Connect to yourself?&#10;This is your own SimpleX address!" xml:space="preserve">
<source>Connect to yourself?
This is your own SimpleX address!</source>
<target>З'єднатися з самим собою?
<target>З'єднатися з самим собою?
Це ваша власна SimpleX-адреса!</target>
<note>new chat sheet title</note>
</trans-unit>
<trans-unit id="Connect to yourself?&#10;This is your own one-time link!" xml:space="preserve">
<source>Connect to yourself?
This is your own one-time link!</source>
<target>Підключитися до себе?
<target>Підключитися до себе?
Це ваше власне одноразове посилання!</target>
<note>new chat sheet title</note>
</trans-unit>
@ -9662,7 +9662,7 @@ pref value</note>
<target>курсив</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>приєднатися як %@</target>
<note>No comment provided by engineer.</note>
@ -9934,8 +9934,8 @@ time to disappear</note>
<target>змінено код безпеки</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>надіслати пряме повідомлення</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -10092,8 +10092,8 @@ last received msg: %2$@</source>
<source>you accepted this member</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>вас запрошують до групи</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -1167,8 +1167,8 @@ swipe action</note>
<target>自动接受图片</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Auto-accept settings" xml:space="preserve">
<source>Auto-accept settings</source>
<trans-unit id="SimpleX address settings" xml:space="preserve">
<source>SimpleX address settings</source>
<target>自动接受设置</target>
<note>alert title</note>
</trans-unit>
@ -9619,7 +9619,7 @@ pref value</note>
<target>斜体</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve">
<trans-unit id="Join as %@" xml:space="preserve">
<source>join as %@</source>
<target>以 %@ 身份加入</target>
<note>No comment provided by engineer.</note>
@ -9890,8 +9890,8 @@ time to disappear</note>
<target>安全密码已更改</target>
<note>chat item text</note>
</trans-unit>
<trans-unit id="send direct message" xml:space="preserve">
<source>send direct message</source>
<trans-unit id="send to connect" xml:space="preserve">
<source>send to connect</source>
<target>发送私信</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -10048,8 +10048,8 @@ last received msg: %2$@</source>
<source>you accepted this member</source>
<note>snd group event chat item</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve">
<source>You are invited to group</source>
<target>您被邀请加入群组</target>
<note>No comment provided by engineer.</note>
</trans-unit>

View file

@ -4000,7 +4000,7 @@ SimpleX 伺服器並不會看到你的個人檔案。</target>
<target state="translated">斜體</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="join as %@" xml:space="preserve" approved="no">
<trans-unit id="Join as %@" xml:space="preserve" approved="no">
<source>join as %@</source>
<target state="translated">以 %@ 身份加入</target>
<note>No comment provided by engineer.</note>
@ -4206,8 +4206,8 @@ SimpleX 伺服器並不會看到你的個人檔案。</target>
<target state="translated">好</target>
<note>pref value</note>
</trans-unit>
<trans-unit id="you are invited to group" xml:space="preserve" approved="no">
<source>you are invited to group</source>
<trans-unit id="You are invited to group" xml:space="preserve" approved="no">
<source>You are invited to group</source>
<target state="translated">你被邀請加入至群組</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@ -5225,7 +5225,7 @@ SimpleX Lock must be enabled.</source>
<trans-unit id="&lt;p&gt;Hi!&lt;/p&gt;&#10;&lt;p&gt;&lt;a href=&quot;%@&quot;&gt;Connect to me via SimpleX Chat&lt;/a&gt;&lt;/p&gt;" xml:space="preserve" approved="no">
<source>&lt;p&gt;Hi!&lt;/p&gt;
&lt;p&gt;&lt;a href="%@"&gt;Connect to me via SimpleX Chat&lt;/a&gt;&lt;/p&gt;</source>
<target state="translated">&lt;p&gt;你好!&lt;/p&gt;
<target state="translated">&lt;p&gt;你好!&lt;/p&gt;
&lt;p&gt;&lt;a href="%@"&gt;來連接我透過SimpleX Chat&lt;/a&gt;&lt;/p&gt;</target>
<note>email text</note>
</trans-unit>

View file

@ -145,7 +145,6 @@
640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CC2B29B8C200CCB412 /* NewChatView.swift */; };
640743612CD360E600158442 /* ChooseServerOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640743602CD360E600158442 /* ChooseServerOperators.swift */; };
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; };
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; };
642BA82D2CE50495005E9412 /* NewServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642BA82C2CE50495005E9412 /* NewServerView.swift */; };
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
@ -179,12 +178,14 @@
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; };
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a */; };
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.a */; };
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; };
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
64E5E3632DF71A4E00A4D530 /* ContextContactRequestActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */; };
64E5E3672DFC16A900A4D530 /* ContextProfilePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */; };
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; };
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
@ -508,7 +509,6 @@
640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; };
640743602CD360E600158442 /* ChooseServerOperators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseServerOperators.swift; sourceTree = "<group>"; };
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
642BA82C2CE50495005E9412 /* NewServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerView.swift; sourceTree = "<group>"; };
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
@ -543,13 +543,15 @@
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a"; sourceTree = "<group>"; };
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.a"; sourceTree = "<group>"; };
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextContactRequestActionsView.swift; sourceTree = "<group>"; };
64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextProfilePickerView.swift; sourceTree = "<group>"; };
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
@ -702,8 +704,8 @@
64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */,
64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */,
64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a in Frameworks */,
64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -788,8 +790,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.a */,
);
path = Libraries;
sourceTree = "<group>";
@ -1085,8 +1087,9 @@
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */,
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */,
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */,
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */,
64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */,
64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */,
64E5E3662DFC16A900A4D530 /* ContextProfilePickerView.swift */,
);
path = ComposeMessage;
sourceTree = "<group>";
@ -1466,7 +1469,6 @@
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */,
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */,
8C74C3EC2C1B92A900039E77 /* Theme.swift in Sources */,
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */,
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */,
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
@ -1514,6 +1516,7 @@
5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */,
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */,
5CC1C99227A6C7F5000D9FF6 /* QRCode.swift in Sources */,
64E5E3672DFC16A900A4D530 /* ContextProfilePickerView.swift in Sources */,
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */,
5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */,
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */,
@ -1580,6 +1583,7 @@
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */,
B70A39732D24090D00E80A5F /* TagListView.swift in Sources */,
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
64E5E3632DF71A4E00A4D530 /* ContextContactRequestActionsView.swift in Sources */,
6440CA00288857A10062C672 /* CIEventView.swift in Sources */,
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,

View file

@ -685,6 +685,7 @@ public enum ChatErrorType: Decodable, Hashable {
case invalidConnReq
case unsupportedConnReq
case invalidChatMessage(connection: Connection, message: String)
case connReqMessageProhibited
case contactNotReady(contact: Contact)
case contactNotActive(contact: Contact)
case contactDisabled(contact: Contact)
@ -757,10 +758,13 @@ public enum StoreError: Decodable, Hashable {
case userContactLinkNotFound
case contactRequestNotFound(contactRequestId: Int64)
case contactRequestNotFoundByName(contactName: ContactName)
case invalidContactRequestEntity(contactRequestId: Int64)
case invalidBusinessChatContactRequest
case groupNotFound(groupId: Int64)
case groupNotFoundByName(groupName: GroupName)
case groupMemberNameNotFound(groupId: Int64, groupMemberName: ContactName)
case groupMemberNotFound(groupMemberId: Int64)
case groupHostMemberNotFound(groupId: Int64)
case groupMemberNotFoundByMemberId(memberId: String)
case memberContactGroupMemberNotFound(contactId: Int64)
case groupWithoutUser
@ -817,7 +821,7 @@ public enum SQLiteError: Decodable, Hashable {
public enum AgentErrorType: Decodable, Hashable {
case CMD(cmdErr: CommandErrorType, errContext: String)
case CONN(connErr: ConnectionErrorType)
case CONN(connErr: ConnectionErrorType, errContext: String)
case SMP(serverAddress: String, smpErr: ProtocolErrorType)
case NTF(ntfErr: ProtocolErrorType)
case XFTP(xftpErr: XFTPErrorType)

View file

@ -1326,7 +1326,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
}
}
}
public var chatDeleted: Bool {
get {
switch self {
@ -1336,14 +1336,24 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
}
}
public var nextConnect: Bool {
get {
switch self {
case let .direct(contact): return contact.sendMsgToConnect
case let .group(groupInfo, _): return groupInfo.nextConnectPrepared
default: return false
}
}
}
public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? {
get {
switch self {
case let .direct(contact):
// TODO [short links] this will have additional statuses for pending contact requests before they are accepted
if contact.nextSendGrpInv { return nil }
if contact.sendMsgToConnect { return nil }
if contact.nextAcceptContactRequest { return ("can't send messages", nil) }
if !contact.active { return ("contact deleted", nil) }
if !contact.sndReady { return ("contact not ready", nil) }
if !contact.sndReady { return (contact.preparedContact?.uiConnLinkType == .con ? "request is sent" : "contact not ready", nil) }
if contact.activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false { return ("not synchronized", nil) }
if contact.activeConn?.connDisabled ?? true { return ("contact disabled", nil) }
return nil
@ -1362,6 +1372,8 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
case .some(.memberSupport(groupMember_: .none)):
return nil
}
} else if groupInfo.nextConnectPrepared {
return nil
} else {
switch groupInfo.membership.memberStatus {
case .memRejected: return ("request to join rejected", nil)
@ -1411,7 +1423,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
default: false
}
}
public var groupInfo: GroupInfo? {
switch self {
case let .group(groupInfo, _): return groupInfo
@ -1521,7 +1533,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
public func ntfsEnabled(chatItem: ChatItem) -> Bool {
ntfsEnabled(chatItem.meta.userMention)
}
public func ntfsEnabled(_ userMention: Bool) -> Bool {
switch self.chatSettings?.enableNtfs {
case .all: true
@ -1537,7 +1549,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
default: return nil
}
}
public var nextNtfMode: MsgFilter? {
self.chatSettings?.enableNtfs.nextMode(mentions: hasMentions)
}
@ -1586,7 +1598,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
case .invalidJSON: return .now
}
}
public func ttl(_ globalTTL: ChatItemTTL) -> ChatTTL {
switch self {
case let .direct(contact):
@ -1634,7 +1646,7 @@ public struct ChatData: Decodable, Identifiable, Hashable, ChatLike {
self.chatItems = chatItems
self.chatStats = chatStats
}
public static func invalidJSON(_ json: Data?) -> ChatData {
ChatData(
chatInfo: .invalidJSON(json: json),
@ -1708,19 +1720,24 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
var createdAt: Date
var updatedAt: Date
var chatTs: Date?
public var preparedContact: PreparedContact?
public var contactRequestId: Int64?
var contactGroupMemberId: Int64?
var contactGrpInvSent: Bool
public var chatTags: [Int64]
public var chatItemTTL: Int64?
public var uiThemes: ThemeModeOverrides?
public var chatDeleted: Bool
public var id: ChatId { get { "@\(contactId)" } }
public var apiId: Int64 { get { contactId } }
public var ready: Bool { get { activeConn?.connStatus == .ready } }
public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } }
public var active: Bool { get { contactStatus == .active } }
public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } }
public var nextConnectPrepared: Bool { preparedContact != nil && (activeConn == nil || activeConn?.connStatus == .prepared) }
public var nextAcceptContactRequest: Bool { contactRequestId != nil && (activeConn == nil || activeConn?.connStatus == .new) }
public var sendMsgToConnect: Bool { nextSendGrpInv || nextConnectPrepared }
public var displayName: String { localAlias == "" ? profile.displayName : localAlias }
public var fullName: String { get { profile.fullName } }
public var image: String? { get { profile.image } }
@ -1736,6 +1753,10 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
}
}
public var isContactCard: Bool {
activeConn == nil && profile.contactLink != nil && active
}
public var contactConnIncognito: Bool {
activeConn?.customUserProfileId != nil
}
@ -1778,6 +1799,16 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
)
}
public struct PreparedContact: Decodable, Hashable {
public var connLinkToConnect: CreatedConnLink
public var uiConnLinkType: ConnectionMode
}
public enum ConnectionMode: String, Decodable, Hashable {
case inv
case con
}
public enum ContactStatus: String, Decodable, Hashable {
case active = "active"
case deleted = "deleted"
@ -1802,7 +1833,7 @@ public struct Connection: Decodable, Hashable {
public var connId: Int64
public var agentConnId: String
public var peerChatVRange: VersionRange
var connStatus: ConnStatus
public var connStatus: ConnStatus
public var connLevel: Int
public var viaGroupLink: Bool
public var customUserProfileId: Int64?
@ -1895,7 +1926,7 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable {
var createdAt: Date
public var updatedAt: Date
public var id: ChatId { get { "<@\(contactRequestId)" } }
public var id: ChatId { get { contactRequestChatId(contactRequestId) } }
public var apiId: Int64 { get { contactRequestId } }
var ready: Bool { get { true } }
public var displayName: String { get { profile.displayName } }
@ -1914,6 +1945,10 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable {
)
}
public func contactRequestChatId(_ contactRequestId: Int64) -> ChatId {
return "<@\(contactRequestId)"
}
public struct PendingContactConnection: Decodable, NamedChat, Hashable {
public var pccConnId: Int64
var pccAgentConnId: String
@ -2052,12 +2087,14 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
var createdAt: Date
var updatedAt: Date
var chatTs: Date?
public var preparedGroup: PreparedGroup?
public var uiThemes: ThemeModeOverrides?
public var membersRequireAttention: Int
public var id: ChatId { get { "#\(groupId)" } }
public var apiId: Int64 { get { groupId } }
public var ready: Bool { get { true } }
public var nextConnectPrepared: Bool { if let preparedGroup { !preparedGroup.connLinkStartedConnection } else { false } }
public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias }
public var fullName: String { get { groupProfile.fullName } }
public var image: String? { get { groupProfile.image } }
@ -2081,6 +2118,14 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
return membership.memberRole >= .moderator && membership.memberActive
}
public var chatIconName: String {
switch businessChat?.chatType {
case .none: "person.2.circle.fill"
case .business: "briefcase.circle.fill"
case .customer: "person.crop.circle.fill"
}
}
public static let sampleData = GroupInfo(
groupId: 1,
localDisplayName: "team",
@ -2096,6 +2141,11 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
)
}
public struct PreparedGroup: Decodable, Hashable {
public var connLinkToConnect: CreatedConnLink
public var connLinkStartedConnection: Bool
}
public struct GroupRef: Decodable, Hashable {
public var groupId: Int64
var localDisplayName: GroupName
@ -2165,6 +2215,16 @@ public enum MemberCriteria: String, Codable, Identifiable, Hashable {
}
}
public struct ContactShortLinkData: Codable, Hashable {
public var profile: Profile
public var message: String?
public var business: Bool
}
public struct GroupShortLinkData: Codable, Hashable {
public var groupProfile: GroupProfile
}
public struct BusinessChatInfo: Decodable, Hashable {
public var chatType: BusinessChatType
public var businessId: String
@ -2243,7 +2303,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
? String.localizedStringWithFormat(NSLocalizedString("Past member %@", comment: "past/unknown group member"), name)
: name
}
public var localAliasAndFullName: String {
get {
let p = memberProfile
@ -2323,7 +2383,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
return memberStatus != .memRemoved && memberStatus != .memLeft && memberRole < .admin
&& userRole >= .admin && userRole >= memberRole && groupInfo.membership.memberActive
}
public var canReceiveReports: Bool {
memberRole >= .moderator && versionRange.maxVersion >= REPORTS_VERSION
}
@ -2335,7 +2395,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
memberChatVRange
}
}
public var memberIncognito: Bool {
memberProfile.profileId != memberContactProfileId
}
@ -2391,7 +2451,7 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Cod
public var id: Self { self }
public static var supportedRoles: [GroupMemberRole] = [.observer, .member, .admin, .owner]
public var text: String {
switch self {
case .observer: return NSLocalizedString("observer", comment: "member role")
@ -2547,7 +2607,7 @@ public enum ConnectionEntity: Decodable, Hashable {
nil
}
}
// public var localDisplayName: String? {
// switch self {
// case let .rcvDirectMsgConnection(conn, contact):
@ -2588,7 +2648,7 @@ public struct NtfMsgInfo: Decodable, Hashable {
public enum RcvNtfMsgInfo: Decodable {
case info(ntfMsgInfo: NtfMsgInfo?)
case error(ntfMsgError: AgentErrorType)
@inline(__always)
public var noMsg: Bool {
if case let .info(msg) = self { msg == nil } else { true }
@ -2648,7 +2708,7 @@ public struct CIMentionMember: Decodable, Hashable {
public struct CIMention: Decodable, Hashable {
public var memberId: String
public var memberRef: CIMentionMember?
public init(groupMember m: GroupMember) {
self.memberId = m.memberId
self.memberRef = CIMentionMember(
@ -2899,7 +2959,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
default: return true
}
}
public var isReport: Bool {
switch content {
case let .sndMsgContent(msgContent), let .rcvMsgContent(msgContent):
@ -2999,14 +3059,14 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
file: nil
)
}
public static func getReportSample(text: String, reason: ReportReason, item: ChatItem, sender: GroupMember? = nil) -> ChatItem {
let chatDir = if let sender = sender {
CIDirection.groupRcv(groupMember: sender)
} else {
CIDirection.groupSnd
}
return ChatItem(
chatDir: chatDir,
meta: CIMeta(
@ -3021,7 +3081,8 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
itemLive: false,
userMention: false,
deletable: false,
editable: false
editable: false,
showGroupAsSender: false
),
content: .sndMsgContent(msgContent: .report(text: text, reason: reason)),
quotedItem: CIQuote.getSample(item.id, item.meta.createdAt, item.text, chatDir: item.chatDir),
@ -3044,7 +3105,8 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
itemLive: false,
userMention: false,
deletable: false,
editable: false
editable: false,
showGroupAsSender: false
),
content: .rcvDeleted(deleteMode: .cidmBroadcast),
quotedItem: nil,
@ -3067,7 +3129,8 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
itemLive: true,
userMention: false,
deletable: false,
editable: false
editable: false,
showGroupAsSender: false
),
content: .sndMsgContent(msgContent: .text("")),
quotedItem: nil,
@ -3117,7 +3180,7 @@ public enum CIDirection: Decodable, Hashable {
}
}
}
public func sameDirection(_ dir: CIDirection) -> Bool {
switch (self, dir) {
case let (.groupRcv(m1), .groupRcv(m2)): m1.groupMemberId == m2.groupMemberId
@ -3142,6 +3205,7 @@ public struct CIMeta: Decodable, Hashable {
public var userMention: Bool
public var deletable: Bool
public var editable: Bool
public var showGroupAsSender: Bool
public var timestampText: Text { Text(formatTimestampMeta(itemTs)) }
public var recent: Bool { updatedAt + 10 > .now }
@ -3166,7 +3230,8 @@ public struct CIMeta: Decodable, Hashable {
itemLive: itemLive,
userMention: false,
deletable: deletable,
editable: editable
editable: editable,
showGroupAsSender: false
)
}
@ -3183,7 +3248,8 @@ public struct CIMeta: Decodable, Hashable {
itemLive: false,
userMention: false,
deletable: false,
editable: false
editable: false,
showGroupAsSender: false
)
}
}
@ -3250,7 +3316,7 @@ public enum CIStatus: Decodable, Hashable {
case .invalid: return "invalid"
}
}
public var sent: Bool {
switch self {
case .sndNew: true
@ -3591,7 +3657,7 @@ public enum CIContent: Decodable, ItemContent, Hashable {
}
private func directE2EEInfoStr(_ e2eeInfo: E2EEInfo) -> String {
e2eeInfo.pqEnabled
e2eeInfo.pqEnabled == true
? NSLocalizedString("This chat is protected by quantum resistant end-to-end encryption.", comment: "E2EE info chat item")
: e2eeInfoNoPQStr
}
@ -3639,6 +3705,14 @@ public enum CIContent: Decodable, ItemContent, Hashable {
}
}
public var hasMsgContent: Bool {
if let mc = msgContent {
!mc.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
} else {
false
}
}
public var showMemberName: Bool {
switch self {
case .rcvMsgContent: return true
@ -4055,7 +4129,7 @@ public enum FileError: Decodable, Equatable, Hashable {
case let .other(fileError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "file error text"), fileError)
}
}
public var moreInfoButton: (label: LocalizedStringKey, link: URL)? {
switch self {
case .blocked: ("How it works", contentModerationPostLink)
@ -4072,6 +4146,7 @@ public enum MsgContent: Equatable, Hashable {
case voice(text: String, duration: Int)
case file(String)
case report(text: String, reason: ReportReason)
case chat(text: String, chatLink: MsgChatLink)
// TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift
case unknown(type: String, text: String)
@ -4084,6 +4159,7 @@ public enum MsgContent: Equatable, Hashable {
case let .voice(text, _): return text
case let .file(text): return text
case let .report(text, _): return text
case let .chat(text, _): return text
case let .unknown(_, text): return text
}
}
@ -4133,6 +4209,7 @@ public enum MsgContent: Equatable, Hashable {
}
}
@inline(__always)
public var cmdString: String {
"json \(encodeJSON(self))"
}
@ -4144,6 +4221,7 @@ public enum MsgContent: Equatable, Hashable {
case image
case duration
case reason
case chatLink
}
public static func == (lhs: MsgContent, rhs: MsgContent) -> Bool {
@ -4155,6 +4233,7 @@ public enum MsgContent: Equatable, Hashable {
case let (.voice(lt, ld), .voice(rt, rd)): return lt == rt && ld == rd
case let (.file(lf), .file(rf)): return lf == rf
case let (.report(lt, lr), .report(rt, rr)): return lt == rt && lr == rr
case let (.chat(lt, ll), .chat(rt, rl)): return lt == rt && ll == rl
case let (.unknown(lType, lt), .unknown(rType, rt)): return lType == rType && lt == rt
default: return false
}
@ -4194,6 +4273,10 @@ extension MsgContent: Decodable {
let text = try container.decode(String.self, forKey: CodingKeys.text)
let reason = try container.decode(ReportReason.self, forKey: CodingKeys.reason)
self = .report(text: text, reason: reason)
case "chat":
let text = try container.decode(String.self, forKey: CodingKeys.text)
let chatLink = try container.decode(MsgChatLink.self, forKey: CodingKeys.chatLink)
self = .chat(text: text, chatLink: chatLink)
default:
let text = try? container.decode(String.self, forKey: CodingKeys.text)
self = .unknown(type: type, text: text ?? "unknown message format")
@ -4235,6 +4318,10 @@ extension MsgContent: Encodable {
try container.encode("report", forKey: .type)
try container.encode(text, forKey: .text)
try container.encode(reason, forKey: .reason)
case let .chat(text, chatLink):
try container.encode("chat", forKey: .type)
try container.encode(text, forKey: .text)
try container.encode(chatLink, forKey: .chatLink)
// TODO use original JSON and type
case let .unknown(_, text):
try container.encode("text", forKey: .type)
@ -4253,6 +4340,12 @@ public enum MsgContentTag: String {
case report
}
public enum MsgChatLink: Codable, Equatable, Hashable {
case contact(connLink: String, profile: Profile, business: Bool)
case invitation(invLink: String, profile: Profile)
case group(connLink: String, groupProfile: GroupProfile)
}
public struct FormattedText: Decodable, Hashable {
public var text: String
public var format: Format?
@ -4338,7 +4431,7 @@ public enum ReportReason: Hashable {
case profile
case other
case unknown(type: String)
public static var supportedReasons: [ReportReason] = [.spam, .illegal, .community, .profile, .other]
public var text: String {
@ -4351,7 +4444,7 @@ public enum ReportReason: Hashable {
case let .unknown(type): return type
}
}
public var attrString: NSAttributedString {
let descr = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
return NSAttributedString(string: text.isEmpty ? self.text : "\(self.text): ", attributes: [
@ -4398,7 +4491,7 @@ public struct LinkPreview: Codable, Equatable, Hashable {
self.description = description
self.image = image
}
public var uri: URL
public var title: String
// TODO remove once optional in haskell
@ -4447,7 +4540,7 @@ public enum NtfTknStatus: String, Decodable, Hashable {
case .expired: NSLocalizedString("Expired", comment: "token status text")
}
}
public func info(register: Bool) -> String {
switch self {
case .new: return NSLocalizedString("Please wait for token to be registered.", comment: "token info")
@ -4555,7 +4648,7 @@ public enum CIGroupInvitationStatus: String, Decodable, Hashable {
}
public struct E2EEInfo: Decodable, Hashable {
public var pqEnabled: Bool
public var pqEnabled: Bool?
}
public enum RcvDirectEvent: Decodable, Hashable {
@ -4825,9 +4918,9 @@ public enum ChatItemTTL: Identifiable, Comparable, Hashable {
public enum ChatTTL: Identifiable, Hashable {
case userDefault(ChatItemTTL)
case chat(ChatItemTTL)
public var id: Self { self }
public var text: String {
switch self {
case let .chat(ttl): return ttl.deleteAfterText
@ -4836,21 +4929,21 @@ public enum ChatTTL: Identifiable, Hashable {
ttl.deleteAfterText)
}
}
public var neverExpires: Bool {
switch self {
case let .chat(ttl): return ttl.seconds == 0
case let .userDefault(ttl): return ttl.seconds == 0
}
}
public var value: Int64? {
switch self {
case let .chat(ttl): return ttl.seconds
case .userDefault: return nil
}
}
public var usingDefault: Bool {
switch self {
case .userDefault: return true
@ -4863,9 +4956,9 @@ public struct ChatTag: Decodable, Hashable {
public var chatTagId: Int64
public var chatTagText: String
public var chatTagEmoji: String?
public var id: Int64 { chatTagId }
public init(chatTagId: Int64, chatTagText: String, chatTagEmoji: String?) {
self.chatTagId = chatTagId
self.chatTagText = chatTagText

View file

@ -82,7 +82,7 @@ public func foundChat(_ chat: ChatLike, _ searchStr: String) -> Bool {
private func canForwardToChat(_ cInfo: ChatInfo) -> Bool {
switch cInfo {
case let .direct(contact): cInfo.sendMsgEnabled && !contact.nextSendGrpInv
case let .direct(contact): cInfo.sendMsgEnabled && !contact.sendMsgToConnect
case .group: cInfo.sendMsgEnabled
case .local: cInfo.sendMsgEnabled
case .contactRequest: false
@ -94,12 +94,7 @@ private func canForwardToChat(_ cInfo: ChatInfo) -> Bool {
public func chatIconName(_ cInfo: ChatInfo) -> String {
switch cInfo {
case .direct: "person.crop.circle.fill"
case let .group(groupInfo, _):
switch groupInfo.businessChat?.chatType {
case .none: "person.2.circle.fill"
case .business: "briefcase.circle.fill"
case .customer: "person.crop.circle.fill"
}
case let .group(groupInfo, _): groupInfo.chatIconName
case .local: "folder.circle.fill"
case .contactRequest: "person.crop.circle.fill"
default: "circle.fill"

View file

@ -644,7 +644,7 @@ swipe action */
"Auto-accept images" = "Автоматично приемане на изображения";
/* alert title */
"Auto-accept settings" = "Автоматично приемане на настройки";
"SimpleX address settings" = "Автоматично приемане на настройки";
/* No comment provided by engineer. */
"Back" = "Назад";
@ -2327,7 +2327,7 @@ snd error text */
"Join" = "Присъединяване";
/* No comment provided by engineer. */
"join as %@" = "присъединяване като %@";
"Join as %@" = "присъединяване като %@";
/* new chat sheet title */
"Join group" = "Влез в групата";
@ -3369,7 +3369,7 @@ chat item action */
"Send delivery receipts to" = "Изпращайте потвърждениe за доставка на";
/* No comment provided by engineer. */
"send direct message" = "изпрати лично съобщение";
"send to connect" = "изпрати лично съобщение";
/* No comment provided by engineer. */
"Send direct message to connect" = "Изпрати лично съобщение за свързване";
@ -4216,7 +4216,7 @@ chat item action */
"You are connected to the server used to receive messages from this contact." = "Вие сте свързани към сървъра, използван за получаване на съобщения от този контакт.";
/* No comment provided by engineer. */
"you are invited to group" = "вие сте поканени в групата";
"You are invited to group" = "вие сте поканени в групата";
/* No comment provided by engineer. */
"You are invited to group" = "Поканени сте в групата";
@ -4427,4 +4427,3 @@ chat item action */
/* No comment provided by engineer. */
"Your SimpleX address" = "Вашият адрес в SimpleX";

View file

@ -1824,7 +1824,7 @@ snd error text */
"Join" = "Připojte se na";
/* No comment provided by engineer. */
"join as %@" = "připojit se jako %@";
"Join as %@" = "připojit se jako %@";
/* new chat sheet title */
"Join group" = "Připojit ke skupině";
@ -2659,7 +2659,7 @@ chat item action */
"Send delivery receipts to" = "Potvrzení o doručení zasílat na";
/* No comment provided by engineer. */
"send direct message" = "odeslat přímou zprávu";
"send to connect" = "odeslat přímou zprávu";
/* No comment provided by engineer. */
"Send direct message to connect" = "Odeslat přímou zprávu pro připojení";
@ -3302,7 +3302,7 @@ chat item action */
"You are connected to the server used to receive messages from this contact." = "Jste připojeni k serveru, který se používá k přijímání zpráv od tohoto kontaktu.";
/* No comment provided by engineer. */
"you are invited to group" = "jste pozváni do skupiny";
"You are invited to group" = "jste pozváni do skupiny";
/* No comment provided by engineer. */
"You are invited to group" = "Jste pozváni do skupiny";
@ -3489,4 +3489,3 @@ chat item action */
/* No comment provided by engineer. */
"Your SimpleX address" = "Vaše SimpleX adresa";

View file

@ -755,7 +755,7 @@ swipe action */
"Auto-accept images" = "Bilder automatisch akzeptieren";
/* alert title */
"Auto-accept settings" = "Einstellungen automatisch akzeptieren";
"SimpleX address settings" = "Einstellungen automatisch akzeptieren";
/* No comment provided by engineer. */
"Back" = "Zurück";
@ -3023,7 +3023,7 @@ snd error text */
"Join" = "Beitreten";
/* No comment provided by engineer. */
"join as %@" = "beitreten als %@";
"Join as %@" = "beitreten als %@";
/* new chat sheet title */
"Join group" = "Treten Sie der Gruppe bei";
@ -4578,7 +4578,7 @@ chat item action */
"Send delivery receipts to" = "Empfangsbestätigungen senden an";
/* No comment provided by engineer. */
"send direct message" = "Direktnachricht senden";
"send to connect" = "Direktnachricht senden";
/* No comment provided by engineer. */
"Send direct message to connect" = "Eine Direktnachricht zum Verbinden senden";
@ -5551,9 +5551,6 @@ report reason */
/* No comment provided by engineer. */
"Use servers" = "Verwende Server";
/* No comment provided by engineer. */
"Use short links (BETA)" = "Kurze Links verwenden (BETA)";
/* No comment provided by engineer. */
"Use SimpleX Chat servers?" = "Verwenden Sie SimpleX-Chat-Server?";
@ -5831,7 +5828,7 @@ report reason */
"You are connected to the server used to receive messages from this contact." = "Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.";
/* No comment provided by engineer. */
"you are invited to group" = "Sie sind zu der Gruppe eingeladen";
"You are invited to group" = "Sie sind zu der Gruppe eingeladen";
/* No comment provided by engineer. */
"You are invited to group" = "Sie sind zu der Gruppe eingeladen";
@ -6093,4 +6090,3 @@ report reason */
/* No comment provided by engineer. */
"Your SimpleX address" = "Ihre SimpleX-Adresse";

View file

@ -755,7 +755,7 @@ swipe action */
"Auto-accept images" = "Aceptar imágenes automáticamente";
/* alert title */
"Auto-accept settings" = "Auto aceptar configuración";
"SimpleX address settings" = "Auto aceptar configuración";
/* No comment provided by engineer. */
"Back" = "Volver";
@ -3023,7 +3023,7 @@ snd error text */
"Join" = "Unirte";
/* No comment provided by engineer. */
"join as %@" = "unirte como %@";
"Join as %@" = "unirte como %@";
/* new chat sheet title */
"Join group" = "Unirte al grupo";
@ -4578,7 +4578,7 @@ chat item action */
"Send delivery receipts to" = "Enviar confirmaciones de entrega a";
/* No comment provided by engineer. */
"send direct message" = "Enviar mensaje directo";
"send to connect" = "Enviar mensaje directo";
/* No comment provided by engineer. */
"Send direct message to connect" = "Envía un mensaje para conectar";
@ -5551,9 +5551,6 @@ report reason */
/* No comment provided by engineer. */
"Use servers" = "Usar servidores";
/* No comment provided by engineer. */
"Use short links (BETA)" = "Usar enlaces cortos (BETA)";
/* No comment provided by engineer. */
"Use SimpleX Chat servers?" = "¿Usar servidores SimpleX Chat?";
@ -5831,7 +5828,7 @@ report reason */
"You are connected to the server used to receive messages from this contact." = "Estás conectado al servidor usado para recibir mensajes de este contacto.";
/* No comment provided by engineer. */
"you are invited to group" = "has sido invitado a un grupo";
"You are invited to group" = "has sido invitado a un grupo";
/* No comment provided by engineer. */
"You are invited to group" = "Has sido invitado a un grupo";
@ -6093,4 +6090,3 @@ report reason */
/* No comment provided by engineer. */
"Your SimpleX address" = "Mi dirección SimpleX";

View file

@ -1755,7 +1755,7 @@ snd error text */
"Join" = "Liity";
/* No comment provided by engineer. */
"join as %@" = "Liity %@:nä";
"Join as %@" = "Liity %@:nä";
/* new chat sheet title */
"Join group" = "Liity ryhmään";
@ -3215,7 +3215,7 @@ chat item action */
"You are connected to the server used to receive messages from this contact." = "Olet yhteydessä palvelimeen, jota käytetään vastaanottamaan viestejä tältä kontaktilta.";
/* No comment provided by engineer. */
"you are invited to group" = "sinut on kutsuttu ryhmään";
"You are invited to group" = "sinut on kutsuttu ryhmään";
/* No comment provided by engineer. */
"You are invited to group" = "Sinut on kutsuttu ryhmään";
@ -3402,4 +3402,3 @@ chat item action */
/* No comment provided by engineer. */
"Your SimpleX address" = "SimpleX-osoitteesi";

View file

@ -728,7 +728,7 @@ swipe action */
"Auto-accept images" = "Images auto-acceptées";
/* alert title */
"Auto-accept settings" = "Paramètres de réception automatique";
"SimpleX address settings" = "Paramètres de réception automatique";
/* No comment provided by engineer. */
"Back" = "Retour";
@ -2924,7 +2924,7 @@ snd error text */
"Join" = "Rejoindre";
/* No comment provided by engineer. */
"join as %@" = "rejoindre entant que %@";
"Join as %@" = "rejoindre entant que %@";
/* new chat sheet title */
"Join group" = "Rejoindre le groupe";
@ -4284,7 +4284,7 @@ chat item action */
"Send delivery receipts to" = "Envoyer les accusés de réception à";
/* No comment provided by engineer. */
"send direct message" = "envoyer un message direct";
"send to connect" = "envoyer un message direct";
/* No comment provided by engineer. */
"Send direct message to connect" = "Envoyer un message direct pour vous connecter";
@ -5473,7 +5473,7 @@ chat item action */
"You are connected to the server used to receive messages from this contact." = "Vous êtes connecté·e au serveur utilisé pour recevoir les messages de ce contact.";
/* No comment provided by engineer. */
"you are invited to group" = "vous êtes invité·e au groupe";
"You are invited to group" = "vous êtes invité·e au groupe";
/* No comment provided by engineer. */
"You are invited to group" = "Vous êtes invité·e au groupe";
@ -5729,4 +5729,3 @@ chat item action */
/* No comment provided by engineer. */
"Your SimpleX address" = "Votre adresse SimpleX";

View file

@ -755,7 +755,7 @@ swipe action */
"Auto-accept images" = "Képek automatikus elfogadása";
/* alert title */
"Auto-accept settings" = "Beállítások automatikus elfogadása";
"SimpleX address settings" = "Beállítások automatikus elfogadása";
/* No comment provided by engineer. */
"Back" = "Vissza";
@ -3023,7 +3023,7 @@ snd error text */
"Join" = "Csatlakozás";
/* No comment provided by engineer. */
"join as %@" = "csatlakozás mint %@";
"Join as %@" = "csatlakozás mint %@";
/* new chat sheet title */
"Join group" = "Csatlakozás csoporthoz";
@ -4578,7 +4578,7 @@ chat item action */
"Send delivery receipts to" = "A kézbesítési jelentéseket a következő címre kell küldeni";
/* No comment provided by engineer. */
"send direct message" = "közvetlen üzenet küldése";
"send to connect" = "közvetlen üzenet küldése";
/* No comment provided by engineer. */
"Send direct message to connect" = "Közvetlen üzenet küldése a kapcsolódáshoz";
@ -5551,9 +5551,6 @@ report reason */
/* No comment provided by engineer. */
"Use servers" = "Kiszolgálók használata";
/* No comment provided by engineer. */
"Use short links (BETA)" = "Rövid hivatkozások használata (béta)";
/* No comment provided by engineer. */
"Use SimpleX Chat servers?" = "SimpleX Chat kiszolgálók használata?";
@ -5831,7 +5828,7 @@ report reason */
"You are connected to the server used to receive messages from this contact." = "Ön már kapcsolódott ahhoz a kiszolgálóhoz, amely az adott partnerétől érkező üzenetek fogadására szolgál.";
/* No comment provided by engineer. */
"you are invited to group" = "Ön meghívást kapott a csoportba";
"You are invited to group" = "Ön meghívást kapott a csoportba";
/* No comment provided by engineer. */
"You are invited to group" = "Ön meghívást kapott a csoportba";
@ -6093,4 +6090,3 @@ report reason */
/* No comment provided by engineer. */
"Your SimpleX address" = "Profil SimpleX-címe";

View file

@ -755,7 +755,7 @@ swipe action */
"Auto-accept images" = "Auto-accetta le immagini";
/* alert title */
"Auto-accept settings" = "Accetta automaticamente le impostazioni";
"SimpleX address settings" = "Accetta automaticamente le impostazioni";
/* No comment provided by engineer. */
"Back" = "Indietro";
@ -3023,7 +3023,7 @@ snd error text */
"Join" = "Entra";
/* No comment provided by engineer. */
"join as %@" = "entra come %@";
"Join as %@" = "entra come %@";
/* new chat sheet title */
"Join group" = "Entra nel gruppo";
@ -4578,7 +4578,7 @@ chat item action */
"Send delivery receipts to" = "Invia ricevute di consegna a";
/* No comment provided by engineer. */
"send direct message" = "invia messaggio diretto";
"send to connect" = "invia messaggio diretto";
/* No comment provided by engineer. */
"Send direct message to connect" = "Invia messaggio diretto per connetterti";
@ -5551,9 +5551,6 @@ report reason */
/* No comment provided by engineer. */
"Use servers" = "Usa i server";
/* No comment provided by engineer. */
"Use short links (BETA)" = "Usa link brevi (BETA)";
/* No comment provided by engineer. */
"Use SimpleX Chat servers?" = "Usare i server di SimpleX Chat?";
@ -5831,7 +5828,7 @@ report reason */
"You are connected to the server used to receive messages from this contact." = "Sei connesso/a al server usato per ricevere messaggi da questo contatto.";
/* No comment provided by engineer. */
"you are invited to group" = "sei stato/a invitato/a al gruppo";
"You are invited to group" = "sei stato/a invitato/a al gruppo";
/* No comment provided by engineer. */
"You are invited to group" = "Sei stato/a invitato/a al gruppo";
@ -6093,4 +6090,3 @@ report reason */
/* No comment provided by engineer. */
"Your SimpleX address" = "Il tuo indirizzo SimpleX";

View file

@ -1962,7 +1962,7 @@ snd error text */
"Join" = "参加";
/* No comment provided by engineer. */
"join as %@" = "%@ として参加";
"Join as %@" = "%@ として参加";
/* new chat sheet title */
"Join group" = "グループに参加";
@ -3413,7 +3413,7 @@ chat item action */
"You are connected to the server used to receive messages from this contact." = "この連絡先から受信するメッセージのサーバに既に接続してます。";
/* No comment provided by engineer. */
"you are invited to group" = "グループ招待が届きました";
"You are invited to group" = "グループ招待が届きました";
/* No comment provided by engineer. */
"You are invited to group" = "グループ招待が届きました";
@ -3603,4 +3603,3 @@ chat item action */
/* No comment provided by engineer. */
"Your SimpleX address" = "あなたのSimpleXアドレス";

View file

@ -755,7 +755,7 @@ swipe action */
"Auto-accept images" = "Afbeeldingen automatisch accepteren";
/* alert title */
"Auto-accept settings" = "Instellingen automatisch accepteren";
"SimpleX address settings" = "Instellingen automatisch accepteren";
/* No comment provided by engineer. */
"Back" = "Terug";
@ -3023,7 +3023,7 @@ snd error text */
"Join" = "Word lid";
/* No comment provided by engineer. */
"join as %@" = "deelnemen als %@";
"Join as %@" = "deelnemen als %@";
/* new chat sheet title */
"Join group" = "Word lid van groep";
@ -4578,7 +4578,7 @@ chat item action */
"Send delivery receipts to" = "Stuur ontvangstbewijzen naar";
/* No comment provided by engineer. */
"send direct message" = "stuur een direct bericht";
"send to connect" = "stuur een direct bericht";
/* No comment provided by engineer. */
"Send direct message to connect" = "Stuur een direct bericht om verbinding te maken";
@ -5551,9 +5551,6 @@ report reason */
/* No comment provided by engineer. */
"Use servers" = "Gebruik servers";
/* No comment provided by engineer. */
"Use short links (BETA)" = "Gebruik korte links (BETA)";
/* No comment provided by engineer. */
"Use SimpleX Chat servers?" = "SimpleX Chat servers gebruiken?";
@ -5831,7 +5828,7 @@ report reason */
"You are connected to the server used to receive messages from this contact." = "U bent verbonden met de server die wordt gebruikt om berichten van dit contact te ontvangen.";
/* No comment provided by engineer. */
"you are invited to group" = "je bent uitgenodigd voor de groep";
"You are invited to group" = "je bent uitgenodigd voor de groep";
/* No comment provided by engineer. */
"You are invited to group" = "Je bent uitgenodigd voor de groep";
@ -6093,4 +6090,3 @@ report reason */
/* No comment provided by engineer. */
"Your SimpleX address" = "Uw SimpleX adres";

View file

@ -728,7 +728,7 @@ swipe action */
"Auto-accept images" = "Automatyczne akceptowanie obrazów";
/* alert title */
"Auto-accept settings" = "Ustawienia automatycznej akceptacji";
"SimpleX address settings" = "Ustawienia automatycznej akceptacji";
/* No comment provided by engineer. */
"Back" = "Wstecz";
@ -2729,7 +2729,7 @@ snd error text */
"Join" = "Dołącz";
/* No comment provided by engineer. */
"join as %@" = "dołącz jako %@";
"Join as %@" = "dołącz jako %@";
/* new chat sheet title */
"Join group" = "Dołącz do grupy";
@ -4008,7 +4008,7 @@ chat item action */
"Send delivery receipts to" = "Wyślij potwierdzenia dostawy do";
/* No comment provided by engineer. */
"send direct message" = "wyślij wiadomość bezpośrednią";
"send to connect" = "wyślij wiadomość bezpośrednią";
/* No comment provided by engineer. */
"Send direct message to connect" = "Wyślij wiadomość bezpośrednią aby połączyć";
@ -5095,7 +5095,7 @@ chat item action */
"You are connected to the server used to receive messages from this contact." = "Jesteś połączony z serwerem używanym do odbierania wiadomości od tego kontaktu.";
/* No comment provided by engineer. */
"you are invited to group" = "jesteś zaproszony do grupy";
"You are invited to group" = "jesteś zaproszony do grupy";
/* No comment provided by engineer. */
"You are invited to group" = "Jesteś zaproszony do grupy";
@ -5342,4 +5342,3 @@ chat item action */
/* No comment provided by engineer. */
"Your SimpleX address" = "Twój adres SimpleX";

View file

@ -403,6 +403,9 @@ swipe action */
/* No comment provided by engineer. */
"Add list" = "Добавить список";
/* No comment provided by engineer. */
"Add message" = "Добавить cообщение";
/* No comment provided by engineer. */
"Add profile" = "Добавить профиль";
@ -755,7 +758,7 @@ swipe action */
"Auto-accept images" = "Автоприем изображений";
/* alert title */
"Auto-accept settings" = "Настройки автоприема";
"SimpleX address settings" = "Настройки автоприема";
/* No comment provided by engineer. */
"Back" = "Назад";
@ -3023,7 +3026,7 @@ snd error text */
"Join" = "Вступить";
/* No comment provided by engineer. */
"join as %@" = "вступить как %@";
"Join as %@" = "вступить как %@";
/* new chat sheet title */
"Join group" = "Вступить в группу";
@ -4574,11 +4577,14 @@ chat item action */
/* No comment provided by engineer. */
"Send a live message - it will update for the recipient(s) as you type it" = "Отправить живое сообщение — оно будет обновляться для получателей по мере того, как Вы его вводите";
/* No comment provided by engineer. */
"Send contact request?" = "Отправить запрос на соединение?";
/* No comment provided by engineer. */
"Send delivery receipts to" = "Отправка отчётов о доставке";
/* No comment provided by engineer. */
"send direct message" = "отправьте сообщение";
"send to connect" = "отправьте сообщение";
/* No comment provided by engineer. */
"Send direct message to connect" = "Отправьте сообщение чтобы соединиться";
@ -4616,6 +4622,12 @@ chat item action */
/* No comment provided by engineer. */
"Send receipts" = "Отправлять отчёты о доставке";
/* No comment provided by engineer. */
"Send request" = "Отправить запрос";
/* No comment provided by engineer. */
"Send request without message" = "Отправить запрос без сообщения";
/* No comment provided by engineer. */
"Send them from gallery or custom keyboards." = "Отправьте из галереи или из дополнительных клавиатур.";
@ -5551,9 +5563,6 @@ report reason */
/* No comment provided by engineer. */
"Use servers" = "Использовать серверы";
/* No comment provided by engineer. */
"Use short links (BETA)" = "Короткие ссылки (БЕТА)";
/* No comment provided by engineer. */
"Use SimpleX Chat servers?" = "Использовать серверы предосталенные SimpleX Chat?";
@ -5831,7 +5840,7 @@ report reason */
"You are connected to the server used to receive messages from this contact." = "Установлено соединение с сервером, через который Вы получаете сообщения от этого контакта.";
/* No comment provided by engineer. */
"you are invited to group" = "Вы приглашены в группу";
"You are invited to group" = "Вы приглашены в группу";
/* No comment provided by engineer. */
"You are invited to group" = "Вы приглашены в группу";
@ -6093,4 +6102,3 @@ report reason */
/* No comment provided by engineer. */
"Your SimpleX address" = "Ваш адрес SimpleX";

View file

@ -1701,7 +1701,7 @@ snd error text */
"Join" = "เข้าร่วม";
/* No comment provided by engineer. */
"join as %@" = "เข้าร่วมเป็น %@";
"Join as %@" = "เข้าร่วมเป็น %@";
/* new chat sheet title */
"Join group" = "เข้าร่วมกลุ่ม";
@ -3125,7 +3125,7 @@ chat item action */
"You are connected to the server used to receive messages from this contact." = "คุณเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้";
/* No comment provided by engineer. */
"you are invited to group" = "คุณได้รับเชิญให้เข้าร่วมกลุ่ม";
"You are invited to group" = "คุณได้รับเชิญให้เข้าร่วมกลุ่ม";
/* No comment provided by engineer. */
"You are invited to group" = "คุณได้รับเชิญให้เข้าร่วมกลุ่ม";
@ -3306,4 +3306,3 @@ chat item action */
/* No comment provided by engineer. */
"Your SimpleX address" = "ที่อยู่ SimpleX ของคุณ";

View file

@ -704,7 +704,7 @@ swipe action */
"Auto-accept images" = "Fotoğrafları otomatik kabul et";
/* alert title */
"Auto-accept settings" = "Ayarları otomatik olarak kabul et";
"SimpleX address settings" = "Ayarları otomatik olarak kabul et";
/* No comment provided by engineer. */
"Back" = "Geri";
@ -2783,7 +2783,7 @@ snd error text */
"Join" = "Katıl";
/* No comment provided by engineer. */
"join as %@" = "%@ olarak katıl";
"Join as %@" = "%@ olarak katıl";
/* new chat sheet title */
"Join group" = "Gruba katıl";
@ -4071,7 +4071,7 @@ chat item action */
"Send delivery receipts to" = "Görüldü bilgilerini şuraya gönder";
/* No comment provided by engineer. */
"send direct message" = "doğrudan mesaj gönder";
"send to connect" = "doğrudan mesaj gönder";
/* No comment provided by engineer. */
"Send direct message to connect" = "Bağlanmak için doğrudan mesaj gönder";
@ -5167,7 +5167,7 @@ chat item action */
"You are connected to the server used to receive messages from this contact." = "Bu kişiden mesaj almak için kullanılan sunucuya bağlısınız.";
/* No comment provided by engineer. */
"you are invited to group" = "gruba davet edildiniz";
"You are invited to group" = "gruba davet edildiniz";
/* No comment provided by engineer. */
"You are invited to group" = "Gruba davet edildiniz";
@ -5411,4 +5411,3 @@ chat item action */
/* No comment provided by engineer. */
"Your SimpleX address" = "SimpleX adresin";

View file

@ -743,7 +743,7 @@ swipe action */
"Auto-accept images" = "Автоматичне прийняття зображень";
/* alert title */
"Auto-accept settings" = "Автоприйняття налаштувань";
"SimpleX address settings" = "Автоприйняття налаштувань";
/* No comment provided by engineer. */
"Back" = "Назад";
@ -2993,7 +2993,7 @@ snd error text */
"Join" = "Приєднуйтесь";
/* No comment provided by engineer. */
"join as %@" = "приєднатися як %@";
"Join as %@" = "приєднатися як %@";
/* new chat sheet title */
"Join group" = "Приєднуйтесь до групи";
@ -4359,7 +4359,7 @@ chat item action */
"Send delivery receipts to" = "Надсилання звітів про доставку";
/* No comment provided by engineer. */
"send direct message" = "надіслати пряме повідомлення";
"send to connect" = "надіслати пряме повідомлення";
/* No comment provided by engineer. */
"Send direct message to connect" = "Надішліть пряме повідомлення, щоб підключитися";
@ -5548,7 +5548,7 @@ chat item action */
"You are connected to the server used to receive messages from this contact." = "Ви підключені до сервера, який використовується для отримання повідомлень від цього контакту.";
/* No comment provided by engineer. */
"you are invited to group" = "вас запрошують до групи";
"You are invited to group" = "вас запрошують до групи";
/* No comment provided by engineer. */
"You are invited to group" = "Запрошуємо вас до групи";
@ -5804,4 +5804,3 @@ chat item action */
/* No comment provided by engineer. */
"Your SimpleX address" = "Ваша адреса SimpleX";

View file

@ -731,7 +731,7 @@ swipe action */
"Auto-accept images" = "自动接受图片";
/* alert title */
"Auto-accept settings" = "自动接受设置";
"SimpleX address settings" = "自动接受设置";
/* No comment provided by engineer. */
"Back" = "返回";
@ -2963,7 +2963,7 @@ snd error text */
"Join" = "加入";
/* No comment provided by engineer. */
"join as %@" = "以 %@ 身份加入";
"Join as %@" = "以 %@ 身份加入";
/* new chat sheet title */
"Join group" = "加入群组";
@ -4374,7 +4374,7 @@ chat item action */
"Send delivery receipts to" = "将送达回执发送给";
/* No comment provided by engineer. */
"send direct message" = "发送私信";
"send to connect" = "发送私信";
/* No comment provided by engineer. */
"Send direct message to connect" = "发送私信来连接";
@ -5446,7 +5446,7 @@ chat item action */
"You are connected to the server used to receive messages from this contact." = "您已连接到用于接收该联系人消息的服务器。";
/* No comment provided by engineer. */
"you are invited to group" = "您被邀请加入群组";
"You are invited to group" = "您被邀请加入群组";
/* No comment provided by engineer. */
"You are invited to group" = "您被邀请加入群组";
@ -5678,4 +5678,3 @@ chat item action */
/* No comment provided by engineer. */
"Your SimpleX address" = "您的 SimpleX 地址";

View file

@ -136,7 +136,6 @@ object NtfManager {
val actionPendingIntent: PendingIntent = PendingIntent.getBroadcast(SimplexApp.context, 0, actionIntent, flags)
val actionButton = when (action) {
NotificationAction.ACCEPT_CONTACT_REQUEST -> generalGetString(MR.strings.accept)
NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO -> generalGetString(MR.strings.accept_contact_incognito_button)
}
builder.addAction(0, actionButton, actionPendingIntent)
}
@ -316,7 +315,6 @@ object NtfManager {
val m = SimplexApp.context.chatModel
when (intent.action) {
NotificationAction.ACCEPT_CONTACT_REQUEST.name -> ntfManager.acceptContactRequestAction(userId, incognito = false, chatId)
NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO.name -> ntfManager.acceptContactRequestAction(userId, incognito = true, chatId)
RejectCallAction -> {
val invitation = m.callInvitations[chatId]
if (invitation != null) {

View file

@ -171,6 +171,8 @@ object ChatModel {
// return true if you handled the click
var centerPanelBackgroundClickHandler: (() -> Boolean)? = null
var addressShortLinkDataSet: Boolean = userAddress.value?.shortLinkDataSet ?: true
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
currentUser.value
} else {
@ -396,8 +398,19 @@ object ChatModel {
}
}
fun updateChats(newChats: List<Chat>) {
chats.replaceAll(newChats)
fun updateChats(newChats: List<Chat>, keepingChatId: String? = null) {
if (keepingChatId != null) {
val chatToKeep = getChat(keepingChatId)
val indexToRemove = newChats.indexOfFirst { it.id == keepingChatId }
if (chatToKeep != null && indexToRemove != -1) {
val remainingNewChats = newChats.toMutableList().apply { removeAt(indexToRemove) }
chats.replaceAll(listOf(chatToKeep) + remainingNewChats)
} else {
chats.replaceAll(newChats)
}
} else {
chats.replaceAll(newChats)
}
popChatCollector.clear()
val cId = chatId.value
@ -436,14 +449,16 @@ object ChatModel {
chatState.itemsRemoved(listOf(removed), chatItems.value)
}
suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) {
suspend fun addChatItem(rhId: Long?, chatInfo: ChatInfo, cItem: ChatItem) {
// updates membersRequireAttention
updateChatInfo(rhId, cInfo)
// mark chat non deleted
if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) {
val updatedContact = cInfo.contact.copy(chatDeleted = false)
updateContact(rhId, updatedContact)
val cInfo = if (chatInfo is ChatInfo.Direct && chatInfo.chatDeleted) {
// mark chat non deleted
val updatedContact = chatInfo.contact.copy(chatDeleted = false)
ChatInfo.Direct(updatedContact)
} else {
chatInfo
}
updateChatInfo(rhId, cInfo)
// update chat list
val i = getChatIndex(rhId, cInfo.id)
val chat: Chat
@ -1272,6 +1287,7 @@ interface SomeChat {
val apiId: Long
val ready: Boolean
val chatDeleted: Boolean
val nextConnect: Boolean
val incognito: Boolean
fun featureEnabled(feature: ChatFeature): Boolean
val timedMessagesTTL: Int?
@ -1351,6 +1367,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val apiId get() = contact.apiId
override val ready get() = contact.ready
override val chatDeleted get() = contact.chatDeleted
override val nextConnect get() = contact.nextConnect
override val incognito get() = contact.incognito
override fun featureEnabled(feature: ChatFeature) = contact.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = contact.timedMessagesTTL
@ -1375,6 +1392,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val apiId get() = groupInfo.apiId
override val ready get() = groupInfo.ready
override val chatDeleted get() = groupInfo.chatDeleted
override val nextConnect get() = groupInfo.nextConnect
override val incognito get() = groupInfo.incognito
override fun featureEnabled(feature: ChatFeature) = groupInfo.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = groupInfo.timedMessagesTTL
@ -1398,6 +1416,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val apiId get() = noteFolder.apiId
override val ready get() = noteFolder.ready
override val chatDeleted get() = noteFolder.chatDeleted
override val nextConnect get() = noteFolder.nextConnect
override val incognito get() = noteFolder.incognito
override fun featureEnabled(feature: ChatFeature) = noteFolder.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = noteFolder.timedMessagesTTL
@ -1421,6 +1440,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val apiId get() = contactRequest.apiId
override val ready get() = contactRequest.ready
override val chatDeleted get() = contactRequest.chatDeleted
override val nextConnect get() = contactRequest.nextConnect
override val incognito get() = contactRequest.incognito
override fun featureEnabled(feature: ChatFeature) = contactRequest.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = contactRequest.timedMessagesTTL
@ -1444,6 +1464,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val apiId get() = contactConnection.apiId
override val ready get() = contactConnection.ready
override val chatDeleted get() = contactConnection.chatDeleted
override val nextConnect get() = contactConnection.nextConnect
override val incognito get() = contactConnection.incognito
override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL
@ -1472,6 +1493,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val id get() = "?$apiId"
override val ready get() = false
override val chatDeleted get() = false
override val nextConnect get() = false
override val incognito get() = false
override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null
@ -1490,10 +1512,16 @@ sealed class ChatInfo: SomeChat, NamedChat {
get() {
when (this) {
is Direct -> {
// TODO [short links] this will have additional statuses for pending contact requests before they are accepted
if (contact.nextSendGrpInv) return null
if (contact.sendMsgToConnect) return null
if (contact.nextAcceptContactRequest) { return generalGetString(MR.strings.cant_send_message_generic) to null }
if (!contact.active) return generalGetString(MR.strings.cant_send_message_contact_deleted) to null
if (!contact.sndReady) return generalGetString(MR.strings.cant_send_message_contact_not_ready) to null
if (!contact.sndReady) {
return if (contact.preparedContact?.uiConnLinkType == ConnectionMode.Con) {
generalGetString(MR.strings.cant_send_message_request_is_sent) to null
} else {
generalGetString(MR.strings.cant_send_message_contact_not_ready) to null
}
}
if (contact.activeConn?.connectionStats?.ratchetSyncSendProhibited == true) return generalGetString(MR.strings.cant_send_message_contact_not_synchronized) to null
if (contact.activeConn?.connDisabled == true) return generalGetString(MR.strings.cant_send_message_contact_disabled) to null
return null
@ -1523,6 +1551,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
return null
}
}
} else if (groupInfo.nextConnectPrepared) {
return null
} else {
return when (groupInfo.membership.memberStatus) {
GroupMemberStatus.MemRejected -> generalGetString(MR.strings.cant_send_message_rejected) to null
@ -1641,6 +1671,8 @@ data class Contact(
override val createdAt: Instant,
override val updatedAt: Instant,
val chatTs: Instant?,
val preparedContact: PreparedContact?,
val contactRequestId: Long?,
val contactGroupMemberId: Long? = null,
val contactGrpInvSent: Boolean,
val chatTags: List<Long>,
@ -1654,7 +1686,11 @@ data class Contact(
override val ready get() = activeConn?.connStatus == ConnStatus.Ready
val sndReady get() = ready || activeConn?.connStatus == ConnStatus.SndReady
val active get() = contactStatus == ContactStatus.Active
override val nextConnect get() = sendMsgToConnect
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
val nextConnectPrepared get() = preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared)
val nextAcceptContactRequest get() = contactRequestId != null && (activeConn == null || activeConn.connStatus == ConnStatus.New)
val sendMsgToConnect get() = nextSendGrpInv || nextConnectPrepared
override val incognito get() = contactConnIncognito
override fun featureEnabled(feature: ChatFeature) = when (feature) {
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser
@ -1684,6 +1720,9 @@ data class Contact(
true
}
val isContactCard: Boolean =
activeConn == null && profile.contactLink != null && active
val contactConnIncognito =
activeConn?.customUserProfileId != null
@ -1717,6 +1756,8 @@ data class Contact(
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
chatTs = Clock.System.now(),
preparedContact = null,
contactRequestId = null,
contactGrpInvSent = false,
chatDeleted = false,
uiThemes = null,
@ -1732,6 +1773,18 @@ data class NavigationInfo(
val afterTotal: Int = 0
)
@Serializable
data class PreparedContact (
val connLinkToConnect: CreatedConnLink,
val uiConnLinkType: ConnectionMode
)
@Serializable
enum class ConnectionMode {
@SerialName("inv") Inv,
@SerialName("con") Con
}
@Serializable
enum class ContactStatus {
@SerialName("active") Active,
@ -1877,6 +1930,7 @@ data class GroupInfo (
override val createdAt: Instant,
override val updatedAt: Instant,
val chatTs: Instant?,
val preparedGroup: PreparedGroup?,
val uiThemes: ThemeModeOverrides? = null,
val membersRequireAttention: Int,
val chatTags: List<Long>,
@ -1887,6 +1941,8 @@ data class GroupInfo (
override val id get() = "#$groupId"
override val apiId get() = groupId
override val ready get() = membership.memberActive
override val nextConnect get() = nextConnectPrepared
val nextConnectPrepared = if (preparedGroup != null) !preparedGroup.connLinkStartedConnection else false
override val chatDeleted get() = false
override val incognito get() = membership.memberIncognito
override fun featureEnabled(feature: ChatFeature) = when (feature) {
@ -1913,6 +1969,13 @@ data class GroupInfo (
val canModerate: Boolean
get() = membership.memberRole >= GroupMemberRole.Moderator && membership.memberActive
val chatIconName: ImageResource
get() = when (businessChat?.chatType) {
null -> MR.images.ic_supervised_user_circle_filled
BusinessChatType.Business -> MR.images.ic_work_filled_padded
BusinessChatType.Customer -> MR.images.ic_account_circle_filled
}
fun groupFeatureEnabled(feature: GroupFeature): Boolean {
val p = fullGroupPreferences
return when (feature) {
@ -1939,6 +2002,7 @@ data class GroupInfo (
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
chatTs = Clock.System.now(),
preparedGroup = null,
uiThemes = null,
membersRequireAttention = 0,
chatTags = emptyList(),
@ -1948,6 +2012,12 @@ data class GroupInfo (
}
}
@Serializable
data class PreparedGroup (
val connLinkToConnect: CreatedConnLink,
val connLinkStartedConnection: Boolean
)
@Serializable
data class GroupRef(val groupId: Long, val localDisplayName: String)
@ -1990,6 +2060,18 @@ enum class MemberCriteria {
}
}
@Serializable
data class ContactShortLinkData (
val profile: Profile,
val message: String?,
val business: Boolean
)
@Serializable
data class GroupShortLinkData (
val groupProfile: GroupProfile
)
@Serializable
data class BusinessChatInfo (
val chatType: BusinessChatType,
@ -2313,6 +2395,7 @@ class NoteFolder(
override val apiId get() = noteFolderId
override val chatDeleted get() = false
override val ready get() = true
override val nextConnect get() = false
override val incognito get() = false
override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice
override val timedMessagesTTL: Int? get() = null
@ -2344,10 +2427,11 @@ class UserContactRequest (
override val updatedAt: Instant
): SomeChat, NamedChat {
override val chatType get() = ChatType.ContactRequest
override val id get() = "<@$contactRequestId"
override val id get() = contactRequestChatId(contactRequestId)
override val apiId get() = contactRequestId
override val chatDeleted get() = false
override val ready get() = true
override val nextConnect get() = false
override val incognito get() = false
override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null
@ -2368,6 +2452,8 @@ class UserContactRequest (
}
}
fun contactRequestChatId(contactRequestId: Long): String = "<@$contactRequestId"
@Serializable
class PendingContactConnection(
val pccConnId: Long,
@ -2386,6 +2472,7 @@ class PendingContactConnection(
override val apiId get() = pccConnId
override val chatDeleted get() = false
override val ready get() = false
override val nextConnect get() = false
override val incognito get() = customUserProfileId != null
override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null
@ -2834,6 +2921,7 @@ data class ChatItem (
deletable = false,
editable = false,
userMention = false,
showGroupAsSender = false,
),
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
quotedItem = null,
@ -2859,6 +2947,7 @@ data class ChatItem (
deletable = false,
editable = false,
userMention = false,
showGroupAsSender = false
),
content = CIContent.SndMsgContent(MsgContent.MCText("")),
quotedItem = null,
@ -3004,7 +3093,8 @@ data class CIMeta (
val itemLive: Boolean?,
val userMention: Boolean,
val deletable: Boolean,
val editable: Boolean
val editable: Boolean,
val showGroupAsSender: Boolean
) {
val timestampText: String get() = getTimestampText(itemTs, true)
@ -3043,6 +3133,7 @@ data class CIMeta (
deletable = deletable,
editable = editable,
userMention = false,
showGroupAsSender = false
)
fun invalidJSON(): CIMeta =
@ -3062,7 +3153,8 @@ data class CIMeta (
itemLive = false,
deletable = false,
editable = false,
userMention = false
userMention = false,
showGroupAsSender = false
)
}
}
@ -3417,6 +3509,13 @@ sealed class CIContent: ItemContent {
is InvalidJSON -> "invalid data"
}
val hasMsgContent: Boolean get() =
if (msgContent != null) {
(msgContent as MsgContent).text.trim().isNotEmpty()
} else {
false
}
val showMemberName: Boolean get() =
when (this) {
is RcvMsgContent -> true
@ -3433,7 +3532,7 @@ sealed class CIContent: ItemContent {
companion object {
fun directE2EEInfoStr(e2EEInfo: E2EEInfo): String =
if (e2EEInfo.pqEnabled) {
if (e2EEInfo.pqEnabled == true) {
generalGetString(MR.strings.e2ee_info_pq_short)
} else {
e2eeInfoNoPQStr
@ -3912,6 +4011,7 @@ sealed class MsgContent {
@Serializable(with = MsgContentSerializer::class) class MCVoice(override val text: String, val duration: Int): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCReport(override val text: String, val reason: ReportReason): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCChat(override val text: String, val chatLink: MsgChatLink): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
val isVoice: Boolean get() =
@ -3965,7 +4065,7 @@ enum class CIGroupInvitationStatus {
}
@Serializable
class E2EEInfo (val pqEnabled: Boolean) {}
class E2EEInfo (val pqEnabled: Boolean?) {}
object MsgContentSerializer : KSerializer<MsgContent> {
override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) {
@ -3992,6 +4092,10 @@ object MsgContentSerializer : KSerializer<MsgContent> {
element<String>("text")
element<ReportReason>("reason")
})
element("MCChat", buildClassSerialDescriptor("MCChat") {
element<String>("text")
element<MsgChatLink>("chatLink")
})
element("MCUnknown", buildClassSerialDescriptor("MCUnknown"))
}
@ -4026,6 +4130,10 @@ object MsgContentSerializer : KSerializer<MsgContent> {
val reason = Json.decodeFromString<ReportReason>(json["reason"].toString())
MsgContent.MCReport(text, reason)
}
"chat" -> {
val chatLink = Json.decodeFromString<MsgChatLink>(json["chatLink"].toString())
MsgContent.MCChat(text, chatLink)
}
else -> MsgContent.MCUnknown(t, text, json)
}
} else {
@ -4080,6 +4188,12 @@ object MsgContentSerializer : KSerializer<MsgContent> {
put("text", value.text)
put("reason", json.encodeToJsonElement(value.reason))
}
is MsgContent.MCChat ->
buildJsonObject {
put("type", "chat")
put("text", value.text)
put("chatLink", json.encodeToJsonElement(value.chatLink))
}
is MsgContent.MCUnknown -> value.json
}
encoder.encodeJsonElement(json)
@ -4095,6 +4209,14 @@ enum class MsgContentTag {
@SerialName("voice") Voice,
@SerialName("file") File,
@SerialName("report") Report,
@SerialName("chat") Chat,
}
@Serializable
sealed class MsgChatLink {
@Serializable @SerialName("contact") data class Contact(val connLink: String, val profile: Profile, val business: Boolean) : MsgChatLink()
@Serializable @SerialName("invitation") data class Invitation(val invLink: String, val profile: Profile) : MsgChatLink()
@Serializable @SerialName("group") data class Group(val connLink: String, val groupProfile: GroupProfile) : MsgChatLink()
}
@Serializable

View file

@ -107,7 +107,6 @@ class AppPreferences {
val simplexLinkMode: SharedPreference<SimplexLinkMode> = mkSafeEnumPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default)
val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true)
val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true)
val privacyShortLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SHORT_LINKS, false)
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
@ -370,7 +369,6 @@ class AppPreferences {
private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode"
private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews"
private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft"
private const val SHARED_PREFS_PRIVACY_SHORT_LINKS = "PrivacyShortLinks"
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
@ -593,7 +591,7 @@ object ChatController {
}
}
suspend fun changeActiveUser_(rhId: Long?, toUserId: Long?, viewPwd: String?) {
suspend fun changeActiveUser_(rhId: Long?, toUserId: Long?, viewPwd: String?, keepingChatId: String? = null) {
val prevActiveUser = chatModel.currentUser.value
val currentUser = changingActiveUserMutex.withLock {
(if (toUserId != null) apiSetActiveUser(rhId, toUserId, viewPwd) else apiGetActiveUser(rhId)).also {
@ -606,20 +604,20 @@ object ChatController {
val users = listUsers(rhId)
chatModel.users.clear()
chatModel.users.addAll(users)
getUserChatData(rhId)
getUserChatData(rhId, keepingChatId = keepingChatId)
val invitation = chatModel.callInvitations.values.firstOrNull { inv -> inv.user.userId == toUserId }
if (invitation != null && currentUser != null) {
chatModel.callManager.reportNewIncomingCall(invitation.copy(user = currentUser))
}
}
suspend fun getUserChatData(rhId: Long?) {
suspend fun getUserChatData(rhId: Long?, keepingChatId: String? = null) {
val hasUser = chatModel.currentUser.value != null
chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null
chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None
withContext(Dispatchers.Main) {
val chats = apiGetChats(rhId)
chatModel.chatsContext.updateChats(chats)
chatModel.chatsContext.updateChats(chats, keepingChatId = keepingChatId)
}
chatModel.userTags.value = apiGetChatTags(rhId).takeIf { hasUser } ?: emptyList()
chatModel.activeChatTagFilter.value = null
@ -795,7 +793,7 @@ object ChatController {
}
suspend fun apiStartChat(ctrl: ChatCtrl? = null): Boolean {
val r = sendCmd(null, CC.StartChat(mainApp = true), ctrl)
val r = sendCmd(null, CC.StartChat(mainApp = true, largeLinkData = true), ctrl)
when (r.result) {
is CR.ChatStarted -> return true
is CR.ChatRunning -> return false
@ -1295,8 +1293,7 @@ object ChatController {
suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair<Pair<CreatedConnLink, PendingContactConnection>?, (() -> Unit)?> {
val userId = try { currentUserId("apiAddContact") } catch (e: Exception) { return null to null }
val short = appPrefs.privacyShortLinks.get()
val r = sendCmd(rh, CC.APIAddContact(userId, short = short, incognito = incognito))
val r = sendCmd(rh, CC.APIAddContact(userId, incognito = incognito))
return when {
r is API.Result && r.res is CR.Invitation -> (r.res.connLinkInvitation to r.res.connection) to null
!(networkErrorAlert(r)) -> null to { apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) }
@ -1394,6 +1391,60 @@ object ChatController {
}
}
suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Chat? {
val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null }
val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData))
if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat
Log.e(TAG, "apiPrepareContact bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_contact), "${r.responseType}: ${r.details}")
return null
}
suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, groupShortLinkData: GroupShortLinkData): Chat? {
val userId = try { currentUserId("apiPrepareGroup") } catch (e: Exception) { return null }
val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, groupShortLinkData))
if (r is API.Result && r.res is CR.NewPreparedChat) return r.res.chat
Log.e(TAG, "apiPrepareGroup bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_group), "${r.responseType}: ${r.details}")
return null
}
suspend fun apiChangePreparedContactUser(rh: Long?, contactId: Long, newUserId: Long): Contact? {
val r = sendCmd(rh, CC.APIChangePreparedContactUser(contactId, newUserId))
if (r is API.Result && r.res is CR.ContactUserChanged) return r.res.toContact
Log.e(TAG, "apiChangePreparedContactUser bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_contact_user), "${r.responseType}: ${r.details}")
return null
}
suspend fun apiChangePreparedGroupUser(rh: Long?, groupId: Long, newUserId: Long): GroupInfo? {
val r = sendCmd(rh, CC.APIChangePreparedGroupUser(groupId, newUserId))
if (r is API.Result && r.res is CR.GroupUserChanged) return r.res.toGroup
Log.e(TAG, "apiChangePreparedGroupUser bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_group_user), "${r.responseType}: ${r.details}")
return null
}
suspend fun apiConnectPreparedContact(rh: Long?, contactId: Long, incognito: Boolean, msg: MsgContent?): Contact? {
val r = sendCmd(rh, CC.APIConnectPreparedContact(contactId, incognito, msg))
if (r is API.Result && r.res is CR.StartedConnectionToContact) return r.res.contact
Log.e(TAG, "apiConnectPreparedContact bad response: ${r.responseType} ${r.details}")
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiConnectPreparedContact", generalGetString(MR.strings.connection_error), r)
}
return null
}
suspend fun apiConnectPreparedGroup(rh: Long?, groupId: Long, incognito: Boolean, msg: MsgContent?): GroupInfo? {
val r = sendCmd(rh, CC.APIConnectPreparedGroup(groupId, incognito, msg))
if (r is API.Result && r.res is CR.StartedConnectionToGroup) return r.res.groupInfo
Log.e(TAG, "apiConnectPreparedGroup bad response: ${r.responseType} ${r.details}")
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiConnectPreparedGroup", generalGetString(MR.strings.connection_error), r)
}
return null
}
suspend fun apiConnectContactViaAddress(rh: Long?, incognito: Boolean, contactId: Long): Contact? {
val userId = try { currentUserId("apiConnectContactViaAddress") } catch (e: Exception) { return null }
val r = sendCmd(rh, CC.ApiConnectContactViaAddress(userId, incognito, contactId))
@ -1539,9 +1590,9 @@ object ChatController {
return false
}
suspend fun apiCreateUserAddress(rh: Long?, short: Boolean): CreatedConnLink? {
suspend fun apiCreateUserAddress(rh: Long?): CreatedConnLink? {
val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null }
val r = sendCmd(rh, CC.ApiCreateMyAddress(userId, short))
val r = sendCmd(rh, CC.ApiCreateMyAddress(userId))
if (r is API.Result && r.res is CR.UserContactLinkCreated) return r.res.connLinkContact
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiCreateUserAddress", generalGetString(MR.strings.error_creating_address), r)
@ -1580,9 +1631,9 @@ object ChatController {
return null
}
suspend fun userAddressAutoAccept(rh: Long?, autoAccept: AutoAccept?): UserContactLinkRec? {
val userId = kotlin.runCatching { currentUserId("userAddressAutoAccept") }.getOrElse { return null }
val r = sendCmd(rh, CC.ApiAddressAutoAccept(userId, autoAccept))
suspend fun apiSetUserAddressSettings(rh: Long?, settings: AddressSettings): UserContactLinkRec? {
val userId = kotlin.runCatching { currentUserId("apiSetUserAddressSettings") }.getOrElse { return null }
val r = sendCmd(rh, CC.ApiSetAddressSettings(userId, settings))
if (r is API.Result && r.res is CR.UserContactLinkUpdated) return r.res.contactLink
if (r is API.Error && r.err is ChatError.ChatErrorStore
&& r.err.storeError is StoreError.UserContactLinkNotFound
@ -1615,11 +1666,14 @@ object ChatController {
}
}
suspend fun apiRejectContactRequest(rh: Long?, contactReqId: Long): Boolean {
suspend fun apiRejectContactRequest(rh: Long?, contactReqId: Long): Contact? {
val r = sendCmd(rh, CC.ApiRejectContact(contactReqId))
if (r is API.Result && r.res is CR.ContactRequestRejected) return true
if (r is API.Result && r.res is CR.ContactRequestRejected) return r.res.contact_
Log.e(TAG, "apiRejectContactRequest bad response: ${r.responseType} ${r.details}")
return false
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiRejectContactRequest", generalGetString(MR.strings.error_rejecting_contact_request), r)
}
return null
}
suspend fun apiGetCallInvitations(rh: Long?): List<RcvCallInvitation> {
@ -1991,19 +2045,18 @@ object ChatController {
}
}
suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<CreatedConnLink, GroupMemberRole>? {
val short = appPrefs.privacyShortLinks.get()
val r = sendCmd(rh, CC.APICreateGroupLink(groupId, memberRole, short))
if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.connLinkContact to r.res.memberRole
suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? {
val r = sendCmd(rh, CC.APICreateGroupLink(groupId, memberRole))
if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.groupLink
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r)
}
return null
}
suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<CreatedConnLink, GroupMemberRole>? {
suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? {
val r = sendCmd(rh, CC.APIGroupLinkMemberRole(groupId, memberRole))
if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole
if (r is API.Result && r.res is CR.CRGroupLink) return r.res.groupLink
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiGroupLinkMemberRole", generalGetString(MR.strings.error_updating_link_for_group), r)
}
@ -2019,16 +2072,16 @@ object ChatController {
return false
}
suspend fun apiGetGroupLink(rh: Long?, groupId: Long): Pair<CreatedConnLink, GroupMemberRole>? {
suspend fun apiGetGroupLink(rh: Long?, groupId: Long): GroupLink? {
val r = sendCmd(rh, CC.APIGetGroupLink(groupId))
if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole
if (r is API.Result && r.res is CR.CRGroupLink) return r.res.groupLink
Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun apiAddGroupShortLink(rh: Long?, groupId: Long): Pair<CreatedConnLink, GroupMemberRole>? {
suspend fun apiAddGroupShortLink(rh: Long?, groupId: Long): GroupLink? {
val r = sendCmd(rh, CC.ApiAddGroupShortLink(groupId))
if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole
if (r is API.Result && r.res is CR.CRGroupLink) return r.res.groupLink
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiAddGroupShortLink", generalGetString(MR.strings.error_creating_link_for_group), r)
}
@ -2380,17 +2433,26 @@ object ChatController {
}
is CR.ReceivedContactRequest -> {
val contactRequest = r.contactRequest
val cInfo = ChatInfo.ContactRequest(contactRequest)
if (active(r.user)) {
withContext(Dispatchers.Main) {
if (chatModel.chatsContext.hasChat(rhId, contactRequest.id)) {
chatModel.chatsContext.updateChatInfo(rhId, cInfo)
if (r.contact_ != null) { // means contact request was created with contact, so we need to add/update contact chat
val contact = r.contact_
if (chatModel.chatsContext.hasChat(rhId, contact.id)) {
chatModel.chatsContext.updateContact(rhId, contact)
} else {
chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Direct(contact), chatItems = listOf()))
}
} else {
chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf()))
val cInfo = ChatInfo.ContactRequest(contactRequest)
if (chatModel.chatsContext.hasChat(rhId, contactRequest.id)) {
chatModel.chatsContext.updateChatInfo(rhId, cInfo)
} else {
chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf()))
}
}
}
}
ntfManager.notifyContactRequestReceived(r.user, cInfo)
ntfManager.notifyContactRequestReceived(r.user, ChatInfo.ContactRequest(contactRequest))
}
is CR.ContactUpdated -> {
if (active(r.user) && chatModel.chatsContext.hasChat(rhId, r.toContact.id)) {
@ -3338,7 +3400,7 @@ sealed class CC {
class ApiMuteUser(val userId: Long): CC()
class ApiUnmuteUser(val userId: Long): CC()
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
class StartChat(val mainApp: Boolean): CC()
class StartChat(val mainApp: Boolean, val largeLinkData: Boolean): CC()
class CheckChatRunning: CC()
class ApiStopChat: CC()
@Serializable
@ -3383,7 +3445,7 @@ sealed class CC {
class ApiLeaveGroup(val groupId: Long): CC()
class ApiListMembers(val groupId: Long): CC()
class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC()
class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole, val short: Boolean): CC()
class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole): CC()
class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC()
class APIDeleteGroupLink(val groupId: Long): CC()
class APIGetGroupLink(val groupId: Long): CC()
@ -3423,10 +3485,16 @@ sealed class CC {
class APIGetGroupMemberCode(val groupId: Long, val groupMemberId: Long): CC()
class APIVerifyContact(val contactId: Long, val connectionCode: String?): CC()
class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC()
class APIAddContact(val userId: Long, val short: Boolean, val incognito: Boolean): CC()
class APIAddContact(val userId: Long, val incognito: Boolean): CC()
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC()
class APIConnectPlan(val userId: Long, val connLink: String): CC()
class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData): CC()
class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val groupShortLinkData: GroupShortLinkData): CC()
class APIChangePreparedContactUser(val contactId: Long, val newUserId: Long): CC()
class APIChangePreparedGroupUser(val groupId: Long, val newUserId: Long): CC()
class APIConnectPreparedContact(val contactId: Long, val incognito: Boolean, val msg: MsgContent?): CC()
class APIConnectPreparedGroup(val groupId: Long, val incognito: Boolean, val msg: MsgContent?): CC()
class APIConnect(val userId: Long, val incognito: Boolean, val connLink: CreatedConnLink): CC()
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC()
@ -3439,12 +3507,12 @@ sealed class CC {
class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC()
class ApiSetUserUIThemes(val userId: Long, val themes: ThemeModeOverrides?): CC()
class ApiSetChatUIThemes(val chatId: String, val themes: ThemeModeOverrides?): CC()
class ApiCreateMyAddress(val userId: Long, val short: Boolean): CC()
class ApiCreateMyAddress(val userId: Long): CC()
class ApiDeleteMyAddress(val userId: Long): CC()
class ApiShowMyAddress(val userId: Long): CC()
class ApiAddMyAddressShortLink(val userId: Long): CC()
class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC()
class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC()
class ApiSetAddressSettings(val userId: Long, val addressSettings: AddressSettings): CC()
class ApiGetCallInvitations: CC()
class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC()
class ApiRejectCall(val contact: Contact): CC()
@ -3509,7 +3577,7 @@ sealed class CC {
is ApiMuteUser -> "/_mute user $userId"
is ApiUnmuteUser -> "/_unmute user $userId"
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
is StartChat -> "/_start main=${onOff(mainApp)}"
is StartChat -> "/_start main=${onOff(mainApp)} large_link_data=${onOff(largeLinkData)}"
is CheckChatRunning -> "/_check running"
is ApiStopChat -> "/_stop"
is ApiSetAppFilePaths -> "/set file paths ${json.encodeToString(this)}"
@ -3572,7 +3640,7 @@ sealed class CC {
is ApiLeaveGroup -> "/_leave #$groupId"
is ApiListMembers -> "/_members #$groupId"
is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}"
is APICreateGroupLink -> "/_create link #$groupId ${memberRole.name.lowercase()} short=${onOff(short)}"
is APICreateGroupLink -> "/_create link #$groupId ${memberRole.name.lowercase()}"
is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}"
is APIDeleteGroupLink -> "/_delete link #$groupId"
is APIGetGroupLink -> "/_get link #$groupId"
@ -3612,10 +3680,16 @@ sealed class CC {
is APIGetGroupMemberCode -> "/_get code #$groupId $groupMemberId"
is APIVerifyContact -> "/_verify code @$contactId" + if (connectionCode != null) " $connectionCode" else ""
is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else ""
is APIAddContact -> "/_connect $userId short=${onOff(short)} incognito=${onOff(incognito)}"
is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}"
is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}"
is ApiChangeConnectionUser -> "/_set conn user :$connId $userId"
is APIConnectPlan -> "/_connect plan $userId $connLink"
is APIPrepareContact -> "/_prepare contact $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(contactShortLinkData)}"
is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(groupShortLinkData)}"
is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId"
is APIChangePreparedGroupUser -> "/_set group user #$groupId $newUserId"
is APIConnectPreparedContact -> "/_connect contact @$contactId incognito=${onOff(incognito)} ${maybeContent(msg)}"
is APIConnectPreparedGroup -> "/_connect group #$groupId incognito=${onOff(incognito)} ${maybeContent(msg)}"
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} ${connLink.connFullLink} ${connLink.connShortLink ?: ""}"
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
is ApiDeleteChat -> "/_delete ${chatRef(type, id, scope = null)} ${chatDeleteMode.cmdString}"
@ -3628,12 +3702,12 @@ sealed class CC {
is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}"
is ApiSetUserUIThemes -> "/_set theme user $userId ${if (themes != null) json.encodeToString(themes) else ""}"
is ApiSetChatUIThemes -> "/_set theme $chatId ${if (themes != null) json.encodeToString(themes) else ""}"
is ApiCreateMyAddress -> "/_address $userId short=${onOff(short)}"
is ApiCreateMyAddress -> "/_address $userId"
is ApiDeleteMyAddress -> "/_delete_address $userId"
is ApiShowMyAddress -> "/_show_address $userId"
is ApiAddMyAddressShortLink -> "/_short_link_address $userId"
is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}"
is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}"
is ApiSetAddressSettings -> "/_address_settings $userId ${json.encodeToString(addressSettings)}"
is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId"
is ApiRejectContact -> "/_reject $contactReqId"
is ApiGetCallInvitations -> "/_call get"
@ -3783,6 +3857,12 @@ sealed class CC {
is ApiSetConnectionIncognito -> "apiSetConnectionIncognito"
is ApiChangeConnectionUser -> "apiChangeConnectionUser"
is APIConnectPlan -> "apiConnectPlan"
is APIPrepareContact -> "apiPrepareContact"
is APIPrepareGroup -> "apiPrepareGroup"
is APIChangePreparedContactUser -> "apiChangePreparedContactUser"
is APIChangePreparedGroupUser -> "apiChangePreparedGroupUser"
is APIConnectPreparedContact -> "apiConnectPreparedContact"
is APIConnectPreparedGroup -> "apiConnectPreparedGroup"
is APIConnect -> "apiConnect"
is ApiConnectContactViaAddress -> "apiConnectContactViaAddress"
is ApiDeleteChat -> "apiDeleteChat"
@ -3800,7 +3880,7 @@ sealed class CC {
is ApiShowMyAddress -> "apiShowMyAddress"
is ApiAddMyAddressShortLink -> "apiAddMyAddressShortLink"
is ApiSetProfileAddress -> "apiSetProfileAddress"
is ApiAddressAutoAccept -> "apiAddressAutoAccept"
is ApiSetAddressSettings -> "apiSetAddressSettings"
is ApiAcceptContact -> "apiAcceptContact"
is ApiRejectContact -> "apiRejectContact"
is ApiGetCallInvitations -> "apiGetCallInvitations"
@ -3870,6 +3950,14 @@ sealed class CC {
private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd)
private fun maybeContent(mc: MsgContent?): String {
return when {
mc is MsgContent.MCText && mc.text.isEmpty() -> ""
mc != null -> " " + mc.cmdString
else -> ""
}
}
companion object {
fun chatRef(chatType: ChatType, id: Long, scope: GroupChatScope?) = when (scope) {
null -> "${chatType.type}${id}"
@ -5833,8 +5921,13 @@ sealed class CR {
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
@Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR()
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connLink: CreatedConnLink, val connectionPlan: ConnectionPlan): CR()
@Serializable @SerialName("newPreparedChat") class NewPreparedChat(val user: UserRef, val chat: Chat): CR()
@Serializable @SerialName("contactUserChanged") class ContactUserChanged(val user: UserRef, val fromContact: Contact, val newUser: UserRef, val toContact: Contact): CR()
@Serializable @SerialName("groupUserChanged") class GroupUserChanged(val user: UserRef, val fromGroup: GroupInfo, val newUser: UserRef, val toGroup: GroupInfo): CR()
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR()
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR()
@Serializable @SerialName("startedConnectionToContact") class StartedConnectionToContact(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("startedConnectionToGroup") class StartedConnectionToGroup(val user: UserRef, val groupInfo: GroupInfo): CR()
@Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR()
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR()
@ -5855,9 +5948,9 @@ sealed class CR {
@Serializable @SerialName("contactConnected") class ContactConnected(val user: UserRef, val contact: Contact, val userCustomProfile: Profile? = null): CR()
@Serializable @SerialName("contactConnecting") class ContactConnecting(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("contactSndReady") class ContactSndReady(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val user: UserRef, val contactRequest: UserContactRequest): CR()
@Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val user: UserRef, val contactRequest: UserContactRequest, val contact_: Contact?): CR()
@Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef): CR()
@Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef, val contactRequest: UserContactRequest, val contact_: Contact?): CR()
@Serializable @SerialName("contactUpdated") class ContactUpdated(val user: UserRef, val toContact: Contact): CR()
@Serializable @SerialName("groupMemberUpdated") class GroupMemberUpdated(val user: UserRef, val groupInfo: GroupInfo, val fromMember: GroupMember, val toMember: GroupMember): CR()
// TODO remove below
@ -5904,8 +5997,8 @@ sealed class CR {
@Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR()
@Serializable @SerialName("groupUpdated") class GroupUpdated(val user: UserRef, val toGroup: GroupInfo): CR()
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val connLinkContact: CreatedConnLink, val memberRole: GroupMemberRole): CR()
@Serializable @SerialName("groupLink") class GroupLink(val user: UserRef, val groupInfo: GroupInfo, val connLinkContact: CreatedConnLink, val memberRole: GroupMemberRole): CR()
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink): CR()
@Serializable @SerialName("groupLink") class CRGroupLink(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink): CR()
@Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR()
@Serializable @SerialName("newMemberContact") class NewMemberContact(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("newMemberContactSentInv") class NewMemberContactSentInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR()
@ -6015,8 +6108,13 @@ sealed class CR {
is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated"
is ConnectionUserChanged -> "ConnectionUserChanged"
is CRConnectionPlan -> "connectionPlan"
is NewPreparedChat -> "newPreparedChat"
is ContactUserChanged -> "contactUserChanged"
is GroupUserChanged -> "groupUserChanged"
is SentConfirmation -> "sentConfirmation"
is SentInvitation -> "sentInvitation"
is StartedConnectionToContact -> "startedConnectionToContact"
is StartedConnectionToGroup -> "startedConnectionToGroup"
is SentInvitationToContact -> "sentInvitationToContact"
is ContactAlreadyExists -> "contactAlreadyExists"
is ContactDeleted -> "contactDeleted"
@ -6084,7 +6182,7 @@ sealed class CR {
is ConnectedToGroupMember -> "connectedToGroupMember"
is GroupUpdated -> "groupUpdated"
is GroupLinkCreated -> "groupLinkCreated"
is GroupLink -> "groupLink"
is CRGroupLink -> "groupLink"
is GroupLinkDeleted -> "groupLinkDeleted"
is NewMemberContact -> "newMemberContact"
is NewMemberContactSentInv -> "newMemberContactSentInv"
@ -6187,8 +6285,13 @@ sealed class CR {
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" )
is CRConnectionPlan -> withUser(user, "connLink: ${json.encodeToString(connLink)}\nconnectionPlan: ${json.encodeToString(connectionPlan)}")
is NewPreparedChat -> withUser(user, json.encodeToString(chat))
is ContactUserChanged -> withUser(user, "fromContact: ${json.encodeToString(fromContact)}\nnewUserId: ${json.encodeToString(newUser.userId)}\ntoContact: ${json.encodeToString(toContact)}")
is GroupUserChanged -> withUser(user, "fromGroup: ${json.encodeToString(fromGroup)}\nnewUserId: ${json.encodeToString(newUser.userId)}\ntoGroup: ${json.encodeToString(toGroup)}")
is SentConfirmation -> withUser(user, json.encodeToString(connection))
is SentInvitation -> withUser(user, json.encodeToString(connection))
is StartedConnectionToContact -> withUser(user, json.encodeToString(contact))
is StartedConnectionToGroup -> withUser(user, json.encodeToString(groupInfo))
is SentInvitationToContact -> withUser(user, json.encodeToString(contact))
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
is ContactDeleted -> withUser(user, json.encodeToString(contact))
@ -6202,16 +6305,16 @@ sealed class CR {
is GroupAliasUpdated -> withUser(user, json.encodeToString(toGroup))
is ConnectionAliasUpdated -> withUser(user, json.encodeToString(toConnection))
is ContactPrefsUpdated -> withUser(user, "fromContact: $fromContact\ntoContact: \n${json.encodeToString(toContact)}")
is UserContactLink -> withUser(user, contactLink.responseDetails)
is UserContactLinkUpdated -> withUser(user, contactLink.responseDetails)
is UserContactLink -> withUser(user, json.encodeToString(contactLink))
is UserContactLinkUpdated -> withUser(user, json.encodeToString(contactLink))
is UserContactLinkCreated -> withUser(user, json.encodeToString(connLinkContact))
is UserContactLinkDeleted -> withUser(user, noDetails())
is ContactConnected -> withUser(user, json.encodeToString(contact))
is ContactConnecting -> withUser(user, json.encodeToString(contact))
is ContactSndReady -> withUser(user, json.encodeToString(contact))
is ReceivedContactRequest -> withUser(user, json.encodeToString(contactRequest))
is ReceivedContactRequest -> withUser(user, "contactRequest: ${json.encodeToString(contactRequest)}\ncontact_: ${json.encodeToString(contact_)}")
is AcceptingContactRequest -> withUser(user, json.encodeToString(contact))
is ContactRequestRejected -> withUser(user, noDetails())
is ContactRequestRejected -> withUser(user, "contactRequest: ${json.encodeToString(contactRequest)}\ncontact_: ${json.encodeToString(contact_)}")
is ContactUpdated -> withUser(user, json.encodeToString(toContact))
is GroupMemberUpdated -> withUser(user, "groupInfo: $groupInfo\nfromMember: $fromMember\ntoMember: $toMember")
is ContactsSubscribed -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}"
@ -6255,8 +6358,8 @@ sealed class CR {
is JoinedGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact")
is GroupUpdated -> withUser(user, json.encodeToString(toGroup))
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnLinkContact: $connLinkContact\nmemberRole: $memberRole")
is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnLinkContact: $connLinkContact\nmemberRole: $memberRole")
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink")
is CRGroupLink -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink")
is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo))
is NewMemberContact -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
@ -6392,7 +6495,7 @@ sealed class ConnectionPlan {
@Serializable
sealed class InvitationLinkPlan {
@Serializable @SerialName("ok") object Ok: InvitationLinkPlan()
@Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): InvitationLinkPlan()
@Serializable @SerialName("ownLink") object OwnLink: InvitationLinkPlan()
@Serializable @SerialName("connecting") class Connecting(val contact_: Contact? = null): InvitationLinkPlan()
@Serializable @SerialName("known") class Known(val contact: Contact): InvitationLinkPlan()
@ -6400,7 +6503,7 @@ sealed class InvitationLinkPlan {
@Serializable
sealed class ContactAddressPlan {
@Serializable @SerialName("ok") object Ok: ContactAddressPlan()
@Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): ContactAddressPlan()
@Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan()
@Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan()
@Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan()
@ -6410,7 +6513,7 @@ sealed class ContactAddressPlan {
@Serializable
sealed class GroupLinkPlan {
@Serializable @SerialName("ok") object Ok: GroupLinkPlan()
@Serializable @SerialName("ok") class Ok(val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan()
@Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan()
@Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan()
@Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan()
@ -6520,26 +6623,30 @@ enum class RatchetSyncState {
}
@Serializable
class UserContactLinkRec(val connLinkContact: CreatedConnLink, val autoAccept: AutoAccept? = null) {
val responseDetails: String get() = "connLinkContact: ${connLinkContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}"
}
data class UserContactLinkRec(
val connLinkContact: CreatedConnLink,
val shortLinkDataSet: Boolean,
val addressSettings: AddressSettings
)
@Serializable
class AutoAccept(val businessAddress: Boolean, val acceptIncognito: Boolean, val autoReply: MsgContent?) {
companion object {
fun cmdString(autoAccept: AutoAccept?): String {
if (autoAccept == null) return "off"
var s = "on"
if (autoAccept.acceptIncognito) {
s += " incognito=on"
} else if (autoAccept.businessAddress) {
s += " business"
}
val msg = autoAccept.autoReply ?: return s
return s + " " + msg.cmdString
}
}
}
data class AddressSettings(
val businessAddress: Boolean,
val autoAccept: AutoAccept?,
val autoReply: MsgContent?
)
@Serializable
data class AutoAccept(val acceptIncognito: Boolean)
@Serializable
data class GroupLink(
val userContactLinkId: Long,
val connLinkContact: CreatedConnLink,
val shortLinkDataSet: Boolean,
val groupLinkId: String,
val acceptMemberRole: GroupMemberRole
)
@Serializable
data class CoreVersionInfo(
@ -6625,6 +6732,7 @@ sealed class ChatErrorType {
is InvalidConnReq -> "invalidConnReq"
is UnsupportedConnReq -> "unsupportedConnReq"
is InvalidChatMessage -> "invalidChatMessage"
is ConnReqMessageProhibited -> "connReqMessageProhibited"
is ContactNotReady -> "contactNotReady"
is ContactNotActive -> "contactNotActive"
is ContactDisabled -> "contactDisabled"
@ -6640,6 +6748,7 @@ sealed class ChatErrorType {
is GroupMemberNotActive -> "groupMemberNotActive"
is GroupMemberUserRemoved -> "groupMemberUserRemoved"
is GroupMemberNotFound -> "groupMemberNotFound"
is GroupHostMemberNotFound -> "groupHostMemberNotFound"
is GroupCantResendInvitation -> "groupCantResendInvitation"
is GroupInternal -> "groupInternal"
is FileNotFound -> "fileNotFound"
@ -6704,6 +6813,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType()
@Serializable @SerialName("unsupportedConnReq") object UnsupportedConnReq: ChatErrorType()
@Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType()
@Serializable @SerialName("connReqMessageProhibited") object ConnReqMessageProhibited: ChatErrorType()
@Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType()
@Serializable @SerialName("contactNotActive") class ContactNotActive(val contact: Contact): ChatErrorType()
@Serializable @SerialName("contactDisabled") class ContactDisabled(val contact: Contact): ChatErrorType()
@ -6719,6 +6829,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("groupMemberNotActive") object GroupMemberNotActive: ChatErrorType()
@Serializable @SerialName("groupMemberUserRemoved") object GroupMemberUserRemoved: ChatErrorType()
@Serializable @SerialName("groupMemberNotFound") object GroupMemberNotFound: ChatErrorType()
@Serializable @SerialName("groupHostMemberNotFound") class GroupHostMemberNotFound(val groupId: Long): ChatErrorType()
@Serializable @SerialName("groupCantResendInvitation") class GroupCantResendInvitation(val groupInfo: GroupInfo, val contactName: String): ChatErrorType()
@Serializable @SerialName("groupInternal") class GroupInternal(val message: String): ChatErrorType()
@Serializable @SerialName("fileNotFound") class FileNotFound(val message: String): ChatErrorType()
@ -6779,6 +6890,8 @@ sealed class StoreError {
is UserContactLinkNotFound -> "userContactLinkNotFound"
is ContactRequestNotFound -> "contactRequestNotFound $contactRequestId"
is ContactRequestNotFoundByName -> "contactRequestNotFoundByName $contactName"
is InvalidContactRequestEntity -> "invalidContactRequestEntity $contactRequestId"
is InvalidBusinessChatContactRequest -> "invalidBusinessChatContactRequest"
is GroupNotFound -> "groupNotFound $groupId"
is GroupNotFoundByName -> "groupNotFoundByName $groupName"
is GroupMemberNameNotFound -> "groupMemberNameNotFound $groupId $groupMemberName"
@ -6855,6 +6968,8 @@ sealed class StoreError {
@Serializable @SerialName("userContactLinkNotFound") object UserContactLinkNotFound: StoreError()
@Serializable @SerialName("contactRequestNotFound") class ContactRequestNotFound(val contactRequestId: Long): StoreError()
@Serializable @SerialName("contactRequestNotFoundByName") class ContactRequestNotFoundByName(val contactName: String): StoreError()
@Serializable @SerialName("invalidContactRequestEntity") class InvalidContactRequestEntity(val contactRequestId: Long): StoreError()
@Serializable @SerialName("invalidBusinessChatContactRequest") object InvalidBusinessChatContactRequest: StoreError()
@Serializable @SerialName("groupNotFound") class GroupNotFound(val groupId: Long): StoreError()
@Serializable @SerialName("groupNotFoundByName") class GroupNotFoundByName(val groupName: String): StoreError()
@Serializable @SerialName("groupMemberNameNotFound") class GroupMemberNameNotFound(val groupId: Long, val groupMemberName: String): StoreError()
@ -6942,7 +7057,7 @@ sealed class SQLiteError {
sealed class AgentErrorType {
val string: String get() = when (this) {
is CMD -> "CMD ${cmdErr.string} $errContext"
is CONN -> "CONN ${connErr.string}"
is CONN -> "CONN ${connErr.string} $errContext"
is SMP -> "SMP ${smpErr.string}"
// is NTF -> "NTF ${ntfErr.string}"
is XFTP -> "XFTP ${xftpErr.string}"
@ -6955,7 +7070,7 @@ sealed class AgentErrorType {
is INACTIVE -> "INACTIVE"
}
@Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType, val errContext: String): AgentErrorType()
@Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType()
@Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType, val errContext: String): AgentErrorType()
@Serializable @SerialName("SMP") class SMP(val serverAddress: String, val smpErr: SMPErrorType): AgentErrorType()
// @Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType()
@Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType()

View file

@ -10,8 +10,7 @@ import chat.simplex.res.MR
import kotlinx.coroutines.delay
enum class NotificationAction {
ACCEPT_CONTACT_REQUEST,
ACCEPT_CONTACT_REQUEST_INCOGNITO
ACCEPT_CONTACT_REQUEST
}
lateinit var ntfManager: NtfManager
@ -31,8 +30,7 @@ abstract class NtfManager {
msgText = generalGetString(MR.strings.notification_new_contact_request),
image = cInfo.image,
listOf(
NotificationAction.ACCEPT_CONTACT_REQUEST to { acceptContactRequestAction(user.userId, incognito = false, cInfo.id) },
NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO to { acceptContactRequestAction(user.userId, incognito = true, cInfo.id) }
NotificationAction.ACCEPT_CONTACT_REQUEST to { acceptContactRequestAction(user.userId, incognito = false, cInfo.id) }
)
)
@ -51,14 +49,9 @@ abstract class NtfManager {
fun acceptContactRequestAction(userId: Long?, incognito: Boolean, chatId: ChatId) {
val isCurrentUser = ChatModel.currentUser.value?.userId == userId
val cInfo: ChatInfo.ContactRequest? = if (isCurrentUser) {
(ChatModel.getChat(chatId)?.chatInfo as? ChatInfo.ContactRequest) ?: return
} else {
null
}
val apiId = chatId.replace("<@", "").toLongOrNull() ?: return
// TODO include remote host in notification
acceptContactRequest(null, incognito, apiId, cInfo, isCurrentUser, ChatModel)
acceptContactRequest(null, incognito, apiId, isCurrentUser, ChatModel)
cancelNotificationsForChat(chatId)
}

View file

@ -934,7 +934,7 @@ fun CallButton(
}
}
} }
contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } }
contact.sendMsgToConnect -> { { showCantCallContactSendMessageAlert() } }
!contact.active -> { { showCantCallContactDeletedAlert() } }
!contact.ready -> { { showCantCallContactConnectingAlert() } }
needToAllowCallsToContact -> { { showNeedToAllowCallsAlert(onConfirm = { allowCallsToContact(chat) }) } }

View file

@ -177,14 +177,10 @@ fun ChatView(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (
chatInfo is ChatInfo.Direct
&& !chatInfo.contact.sndReady
&& chatInfo.contact.active
&& !chatInfo.contact.nextSendGrpInv
) {
val connectingText = connectingText(chatInfo)
if (connectingText != null) {
Text(
generalGetString(MR.strings.contact_connection_pending),
connectingText,
Modifier.padding(top = 4.dp),
fontSize = 14.sp,
color = MaterialTheme.colors.secondary
@ -287,7 +283,7 @@ fun ChatView(
// The idea is to preload information before showing a modal because large groups can take time to load all members
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
var preloadedCode: String? = null
var preloadedLink: Pair<CreatedConnLink, GroupMemberRole>? = null
var preloadedLink: GroupLink? = null
if (chatInfo is ChatInfo.Direct) {
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second
@ -315,13 +311,13 @@ fun ChatView(
showSearch.value = true
}
} else if (chatInfo is ChatInfo.Group) {
var link: Pair<CreatedConnLink, GroupMemberRole>? by remember(chatInfo.id) { mutableStateOf(preloadedLink) }
var link: GroupLink? by remember(chatInfo.id) { mutableStateOf(preloadedLink) }
KeyChangeEffect(chatInfo.id) {
setGroupMembers(chatRh, chatInfo.groupInfo, chatModel)
link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId)
preloadedLink = link
}
GroupChatInfoView(chatsCtx, chatRh, chatInfo.id, link?.first, link?.second, selectedItems, appBar, scrollToItemId, {
GroupChatInfoView(chatsCtx, chatRh, chatInfo.id, link, selectedItems, appBar, scrollToItemId, {
link = it
preloadedLink = it
}, close, { showSearch.value = true })
@ -713,6 +709,34 @@ fun ChatView(
}
}
private fun connectingText(chatInfo: ChatInfo): String? {
return when (chatInfo) {
is ChatInfo.Direct ->
if (
!chatInfo.contact.sndReady
&& chatInfo.contact.active
&& !chatInfo.contact.sendMsgToConnect
&& !chatInfo.contact.nextAcceptContactRequest
) {
if (chatInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con) {
generalGetString(MR.strings.contact_should_accept)
} else {
generalGetString(MR.strings.contact_connection_pending)
}
} else {
null
}
is ChatInfo.Group ->
when (chatInfo.groupInfo.membership.memberStatus) {
GroupMemberStatus.MemAccepted -> generalGetString(MR.strings.group_connection_pending) // TODO [short links] add member status to show transition from prepared group to started connection earlier?
else -> null
}
else -> null
}
}
fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) {
withBGApi {
if (chatInfo is ChatInfo.Direct) {
@ -1704,7 +1728,7 @@ fun BoxScope.ChatItemsList(
} else {
null
}
val showAvatar = shouldShowAvatar(item, listItem.nextItem)
val showAvatar = shouldShowAvatar(item, merged.oldest().nextItem)
val isRevealed = remember { derivedStateOf { revealedItems.value.contains(item.id) } }
val itemSeparation: ItemSeparation
val prevItemSeparationLargeGap: Boolean
@ -2509,7 +2533,7 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (
val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId)
close?.invoke()
ModalManager.end.showModalCloseable(true) {
GroupLinkView(chatModel, rhId, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null)
GroupLinkView(chatModel, rhId, groupInfo, link, onGroupLinkUpdated = null)
}
}
}

View file

@ -0,0 +1,126 @@
package chat.simplex.common.views.chat
import SectionItemView
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chatlist.acceptContactRequest
import chat.simplex.common.views.chatlist.rejectContactRequest
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource
@Composable
fun ComposeContextContactRequestActionsView(
rhId: Long?,
contactRequestId: Long
) {
Column(
Modifier
.height(60.dp)
.background(MaterialTheme.colors.surface)
) {
Divider()
Row(
Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Column(
Modifier
.fillMaxWidth()
.fillMaxHeight()
.weight(1F)
.clickable {
showRejectRequestAlert(rhId, contactRequestId)
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(MR.strings.reject_contact_button), color = Color.Red)
}
Column(
Modifier
.fillMaxWidth()
.fillMaxHeight()
.weight(1F)
.clickable {
showAcceptRequestAlert(rhId, contactRequestId)
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(MR.strings.accept_contact_button), color = MaterialTheme.colors.primary)
}
}
}
}
fun showRejectRequestAlert(rhId: Long?, contactRequestId: Long) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.reject_contact_request),
text = generalGetString(MR.strings.the_sender_will_not_be_notified),
confirmText = generalGetString(MR.strings.reject_contact_button),
onConfirm = {
AlertManager.shared.hideAlert()
rejectContactRequest(rhId, contactRequestId, chatModel, dismissToChatList = true)
},
destructive = true,
hostDevice = hostDevice(rhId),
)
}
fun showAcceptRequestAlert(rhId: Long?, contactRequestId: Long) {
// Show 2 buttons in a row
if (chatModel.addressShortLinkDataSet) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.accept_contact_request),
confirmText = generalGetString(MR.strings.accept_contact_button),
onConfirm = {
AlertManager.shared.hideAlert()
acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel)
},
hostDevice = hostDevice(rhId),
)
// Show 3 buttons in a column
} else {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.accept_contact_request),
buttons = {
Column {
// Accept
SectionItemView({
AlertManager.shared.hideAlert()
acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel)
}) {
Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
// Accept incognito
SectionItemView({
AlertManager.shared.hideAlert()
acceptContactRequest(rhId, incognito = true, contactRequestId, isCurrentUser = true, chatModel)
}) {
Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
// Cancel
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
},
hostDevice = hostDevice(rhId),
)
}
}

View file

@ -0,0 +1,281 @@
package chat.simplex.common.views.chat
import TextIconSpaced
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.IncognitoOptionImage
import chat.simplex.common.views.usersettings.IncognitoView
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
private val USER_ROW_AVATAR_SIZE = 42.dp
private val USER_ROW_VERTICAL_PADDING = 8.dp
private val USER_PICKER_ROW_SIZE = USER_ROW_AVATAR_SIZE + (USER_ROW_VERTICAL_PADDING * 2f)
private val MAX_USER_PICKER_HEIGHT = (USER_PICKER_ROW_SIZE * 4) + (USER_ROW_AVATAR_SIZE + USER_ROW_VERTICAL_PADDING - 4.dp)
@Composable
fun ComposeContextProfilePickerView(
rhId: Long?,
chat: Chat,
currentUser: User
) {
val selectedUser = remember { mutableStateOf(currentUser) }
val incognitoDefault = chatModel.controller.appPrefs.incognito.get()
val users = chatModel.users.map { it.user }.filter { u -> u.activeUser || !u.hidden }
val listExpanded = remember { mutableStateOf(false) }
val maxHeightInPx = with(LocalDensity.current) { windowHeight().toPx() }
val isVisible = remember { mutableStateOf(false) }
val offsetY = remember { Animatable(maxHeightInPx) }
LaunchedEffect(isVisible.value) {
if (isVisible.value) {
offsetY.animateTo(
targetValue = 0f,
animationSpec = contextUserPickerAnimSpec()
)
}
}
@Composable
fun ExpandCollapseChevron() {
if (listExpanded.value) {
Icon(
painterResource(
MR.images.ic_chevron_down
),
contentDescription = null,
Modifier.size(20.dp),
tint = MaterialTheme.colors.secondary,
)
} else {
Icon(
painterResource(
MR.images.ic_chevron_up
),
contentDescription = null,
Modifier.size(20.dp),
tint = MaterialTheme.colors.secondary,
)
}
}
fun changeProfile(newUser: User) {
withApi {
if (chat.chatInfo is ChatInfo.Direct) {
val updatedContact = chatModel.controller.apiChangePreparedContactUser(rhId, chat.chatInfo.contact.contactId, newUser.userId)
if (updatedContact != null) {
selectedUser.value = newUser
chatModel.controller.appPrefs.incognito.set(false)
listExpanded.value = false
chatModel.chatsContext.updateContact(rhId, updatedContact)
}
} else if (chat.chatInfo is ChatInfo.Group) {
val updatedGroup = chatModel.controller.apiChangePreparedGroupUser(rhId, chat.chatInfo.groupInfo.groupId, newUser.userId)
if (updatedGroup != null) {
selectedUser.value = newUser
chatModel.controller.appPrefs.incognito.set(false)
listExpanded.value = false
chatModel.chatsContext.updateGroup(rhId, updatedGroup)
}
}
chatModel.controller.changeActiveUser_(
rhId = newUser.remoteHostId,
toUserId = newUser.userId,
viewPwd = null,
keepingChatId = chat.id
)
if (chatModel.currentUser.value?.userId != newUser.userId) {
AlertManager.shared.showAlertMsg(generalGetString(
MR.strings.switching_profile_error_title),
String.format(generalGetString(MR.strings.switching_profile_error_message), newUser.chatViewName)
)
}
}
}
@Composable
fun ProfilePickerUserOption(user: User) {
Row(
Modifier
.fillMaxWidth()
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp)
.clickable(onClick = {
if (selectedUser.value.userId == user.userId) {
if (!incognitoDefault) {
listExpanded.value = !listExpanded.value
} else {
chatModel.controller.appPrefs.incognito.set(false)
listExpanded.value = false
}
} else {
changeProfile(user)
}
})
.padding(horizontal = DEFAULT_PADDING_HALF, vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
ProfileImage(size = USER_ROW_AVATAR_SIZE, image = user.image)
TextIconSpaced(false)
Text(
user.chatViewName,
modifier = Modifier.align(Alignment.CenterVertically),
fontWeight = if (selectedUser.value.userId == user.userId && !incognitoDefault) FontWeight.Medium else FontWeight.Normal
)
Spacer(Modifier.weight(1f))
if (selectedUser.value.userId == user.userId && !incognitoDefault) {
ExpandCollapseChevron()
}
}
}
@Composable
fun IncognitoOption() {
Row(
Modifier
.fillMaxWidth()
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp)
.clickable(onClick = {
if (incognitoDefault) {
listExpanded.value = !listExpanded.value
} else {
chatModel.controller.appPrefs.incognito.set(true)
listExpanded.value = false
}
})
.padding(horizontal = DEFAULT_PADDING_HALF, vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
IncognitoOptionImage()
TextIconSpaced(false)
Text(
stringResource(MR.strings.incognito),
modifier = Modifier.align(Alignment.CenterVertically),
fontWeight = if (incognitoDefault) FontWeight.Medium else FontWeight.Normal
)
Spacer(Modifier.padding(6.dp))
Column(Modifier
.size(48.dp)
.clip(CircleShape)
.clickable(
onClick = {
if (ModalManager.end.isLastModalOpen(ModalViewId.CONTEXT_USER_PICKER_INCOGNITO)) {
ModalManager.end.closeModal()
} else {
ModalManager.end.showModal(id = ModalViewId.CONTEXT_USER_PICKER_INCOGNITO) { IncognitoView() }
}
}
),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
painterResource(MR.images.ic_info),
stringResource(MR.strings.incognito),
tint = MaterialTheme.colors.primary
)
}
Spacer(Modifier.weight(1f))
if (incognitoDefault) {
ExpandCollapseChevron()
}
}
}
@Composable
fun ProfilePicker() {
LazyColumnWithScrollBarNoAppBar(
Modifier
.heightIn(max = MAX_USER_PICKER_HEIGHT)
.background(MaterialTheme.colors.surface),
reverseLayout = true,
maxHeight = remember { mutableStateOf(MAX_USER_PICKER_HEIGHT) },
containerAlignment = Alignment.BottomEnd
) {
val otherUsers = users.filter { u -> u.userId != selectedUser.value.userId }.sortedByDescending { it.activeOrder }
if (incognitoDefault) {
item {
IncognitoOption()
Divider(
Modifier.padding(
start = DEFAULT_PADDING_HALF,
end = DEFAULT_PADDING_HALF,
)
)
ProfilePickerUserOption(selectedUser.value)
}
} else {
item {
ProfilePickerUserOption(selectedUser.value)
Divider(
Modifier.padding(
start = DEFAULT_PADDING_HALF,
end = DEFAULT_PADDING_HALF,
)
)
IncognitoOption()
}
}
items(otherUsers, key = { it.userId }) { user ->
Divider(
Modifier.padding(
start = DEFAULT_PADDING_HALF,
end = DEFAULT_PADDING_HALF,
)
)
ProfilePickerUserOption(user)
}
}
}
@Composable
fun CurrentSelection() {
Column(
Modifier
.background(MaterialTheme.colors.surface),
) {
Text(
generalGetString(MR.strings.context_user_picker_your_profile),
Modifier.padding(horizontal = 14.dp).padding(top = 8.dp),
color = MaterialTheme.colors.secondary
)
if (incognitoDefault) {
IncognitoOption()
} else {
ProfilePickerUserOption(selectedUser.value)
}
}
}
if (listExpanded.value) {
ProfilePicker()
} else {
CurrentSelection()
}
}

View file

@ -245,6 +245,7 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview {
is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = fileName, mc.duration / 1000, true)
is MsgContent.MCFile -> ComposePreview.FilePreview(fileName, getAppFileUri(fileName))
is MsgContent.MCReport -> ComposePreview.NoPreview
is MsgContent.MCChat -> ComposePreview.NoPreview
is MsgContent.MCUnknown, null -> ComposePreview.NoPreview
}
}
@ -485,6 +486,91 @@ fun ComposeView(
return null
}
fun checkLinkPreview(): MsgContent {
val msgText = composeState.value.message.text
return when (val composePreview = composeState.value.preview) {
is ComposePreview.CLinkPreview -> {
val parsedMsg = parseToMarkdown(msgText)
val url = getSimplexLink(parsedMsg).first
val lp = composePreview.linkPreview
if (lp != null && url == lp.uri) {
MsgContent.MCLink(msgText, preview = lp)
} else {
MsgContent.MCText(msgText)
}
}
else -> MsgContent.MCText(msgText)
}
}
suspend fun sendMemberContactInvitation() {
val mc = checkLinkPreview()
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc)
if (contact != null) {
withContext(Dispatchers.Main) {
chatsCtx.updateContact(chat.remoteHostId, contact)
clearState()
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
}
}
}
suspend fun sendConnectPreparedContact() {
val mc = checkLinkPreview()
val contact = chatModel.controller.apiConnectPreparedContact(
rh = chat.remoteHostId,
contactId = chat.chatInfo.apiId,
incognito = chatModel.controller.appPrefs.incognito.get(),
msg = mc
)
if (contact != null) {
withContext(Dispatchers.Main) {
chatsCtx.updateContact(chat.remoteHostId, contact)
clearState()
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
}
}
}
suspend fun sendConnectPreparedGroup() {
val mc = checkLinkPreview()
val groupInfo = chatModel.controller.apiConnectPreparedGroup(
rh = chat.remoteHostId,
groupId = chat.chatInfo.apiId,
incognito = chatModel.controller.appPrefs.incognito.get(),
msg = mc
)
if (groupInfo != null) {
withContext(Dispatchers.Main) {
chatsCtx.updateGroup(chat.remoteHostId, groupInfo)
clearState()
}
}
}
// TODO [short links] next connect button design, rework compose to not show send button, align with Swift
@Composable
fun NextConnectPreparedButton() {
TextButton(onClick = {
withBGApi {
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.nextSendGrpInv) {
sendMemberContactInvitation()
} else if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.nextConnectPrepared) {
sendConnectPreparedContact()
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared) {
sendConnectPreparedGroup()
}
}
}) {
if (chat.chatInfo is ChatInfo.Group) {
Text("Join")
} else {
Text("Connect")
}
}
}
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List<ChatItem>? {
val cInfo = chat.chatInfo
val cs = composeState.value
@ -525,23 +611,6 @@ fun ComposeView(
return chatItems
}
fun checkLinkPreview(): MsgContent {
return when (val composePreview = cs.preview) {
is ComposePreview.CLinkPreview -> {
val parsedMsg = parseToMarkdown(msgText)
val url = getSimplexLink(parsedMsg).first
val lp = composePreview.linkPreview
if (lp != null && url == lp.uri) {
MsgContent.MCLink(msgText, preview = lp)
} else {
MsgContent.MCText(msgText)
}
}
else -> MsgContent.MCText(msgText)
}
}
fun constructFailedMessage(cs: ComposeState): ComposeState {
val preview = when (cs.preview) {
is ComposePreview.MediaPreview -> {
@ -564,6 +633,8 @@ fun ComposeView(
is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration)
is MsgContent.MCFile -> MsgContent.MCFile(msgText)
is MsgContent.MCReport -> MsgContent.MCReport(msgText, reason = msgContent.reason)
// TODO [short links] update chat link
is MsgContent.MCChat -> MsgContent.MCChat(msgText, chatLink = msgContent.chatLink)
is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json)
}
}
@ -586,16 +657,6 @@ fun ComposeView(
return cItems?.map { it.chatItem }
}
suspend fun sendMemberContactInvitation() {
val mc = checkLinkPreview()
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc)
if (contact != null) {
withContext(Dispatchers.Main) {
chatsCtx.updateContact(chat.remoteHostId, contact)
}
}
}
suspend fun updateMessage(ei: ChatItem, chat: Chat, live: Boolean): ChatItem? {
val cInfo = chat.chatInfo
val oldMsgContent = ei.content.msgContent
@ -1015,6 +1076,37 @@ fun ComposeView(
val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv)
Column {
// TODO [short links] move button to the right of send field, rework SendMsgView to not show send button, align with Swift
if (chat.chatInfo.nextConnect) {
NextConnectPreparedButton()
}
// TODO ^^^ (this shouldn't be here)
val currentUser = chatModel.currentUser.value
if ((
(chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.nextConnectPrepared)
|| (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared)
)
&& currentUser != null
) {
ComposeContextProfilePickerView(
rhId = rhId,
chat = chat,
currentUser = currentUser
)
}
if (
chat.chatInfo is ChatInfo.Direct
&& chat.chatInfo.contact.nextAcceptContactRequest
&& chat.chatInfo.contact.contactRequestId != null
) {
ComposeContextContactRequestActionsView(
rhId = rhId,
contactRequestId = chat.chatInfo.contact.contactRequestId
)
}
if (
chat.chatInfo is ChatInfo.Group
&& chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext
@ -1030,13 +1122,16 @@ fun ComposeView(
member = chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_
)
}
if (nextSendGrpInv.value) {
ComposeContextInvitingContactMemberView()
}
val ctx = composeState.value.contextItem
if (ctx is ComposeContextItem.ReportedItem) {
ReportReasonView(ctx.reason)
}
val simplexLinkProhibited = chatsCtx.secondaryContextFilter == null && hasSimplexLink.value && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)
val fileProhibited = chatsCtx.secondaryContextFilter == null && composeState.value.attachmentPreview && !chat.groupFeatureEnabled(GroupFeature.Files)
val voiceProhibited = composeState.value.preview is ComposePreview.VoicePreview && !chat.chatInfo.featureEnabled(ChatFeature.Voice)

View file

@ -105,7 +105,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
.filterIsInstance<ChatInfo.Direct>()
.filter { it.sendMsgEnabled }
.map { it.contact }
.filter { c -> !c.nextSendGrpInv && c.contactId !in memberContactIds && c.anyNameContains(s)
.filter { c -> !c.sendMsgToConnect && c.contactId !in memberContactIds && c.anyNameContains(s)
}
.sortedBy { it.displayName.lowercase() }
.toList()

View file

@ -54,12 +54,11 @@ fun ModalData.GroupChatInfoView(
chatsCtx: ChatModel.ChatsContext,
rhId: Long?,
chatId: String,
groupLink: CreatedConnLink?,
groupLinkMemberRole: GroupMemberRole?,
groupLink: GroupLink?,
selectedItems: MutableState<Set<Long>?>,
appBar: MutableState<@Composable (BoxScope.() -> Unit)?>,
scrollToItemId: MutableState<Long?>,
onGroupLinkUpdated: (Pair<CreatedConnLink, GroupMemberRole>?) -> Unit,
onGroupLinkUpdated: (GroupLink?) -> Unit,
close: () -> Unit,
onSearchClicked: () -> Unit
) {
@ -166,7 +165,7 @@ fun ModalData.GroupChatInfoView(
clearChat = { clearChatDialog(chat, close) },
leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) },
manageGroupLink = {
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated) }
},
onSearchClicked = onSearchClicked,
deletingItems = deletingItems
@ -375,7 +374,7 @@ fun ModalData.GroupChatInfoLayout(
activeSortedMembers: List<GroupMember>,
developerTools: Boolean,
onLocalAliasChanged: (String) -> Unit,
groupLink: CreatedConnLink?,
groupLink: GroupLink?,
selectedItems: MutableState<Set<Long>?>,
appBar: MutableState<@Composable (BoxScope.() -> Unit)?>,
scrollToItemId: MutableState<Long?>,
@ -537,59 +536,65 @@ fun ModalData.GroupChatInfoLayout(
}
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true)
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) {
if (groupInfo.canAddMembers) {
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
val addMembersTitleId = when (groupInfo.businessChat?.chatType) {
BusinessChatType.Customer -> MR.strings.button_add_team_members
BusinessChatType.Business -> MR.strings.button_add_friends
null -> MR.strings.button_add_members
if (!groupInfo.nextConnectPrepared) {
SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) {
if (groupInfo.canAddMembers) {
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
val addMembersTitleId = when (groupInfo.businessChat?.chatType) {
BusinessChatType.Customer -> MR.strings.button_add_team_members
BusinessChatType.Business -> MR.strings.button_add_friends
null -> MR.strings.button_add_members
}
AddMembersButton(addMembersTitleId, tint, onAddMembersClick)
}
AddMembersButton(addMembersTitleId, tint, onAddMembersClick)
}
if (activeSortedMembers.size > 8) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
MemberListSearchRowView(searchText)
if (activeSortedMembers.size > 8) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
MemberListSearchRowView(searchText)
}
}
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
MemberRow(groupInfo.membership, user = true)
}
}
SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
MemberRow(groupInfo.membership, user = true)
}
}
}
items(filteredMembers.value, key = { it.groupMemberId }) { member ->
Divider()
val showMenu = remember { mutableStateOf(false) }
val canBeSelected = groupInfo.membership.memberRole >= member.memberRole && member.memberRole < GroupMemberRole.Moderator
SectionItemViewLongClickable(
click = {
if (selectedItems.value != null) {
if (canBeSelected) {
toggleItemSelection(member.groupMemberId, selectedItems)
if (!groupInfo.nextConnectPrepared) {
items(filteredMembers.value, key = { it.groupMemberId }) { member ->
Divider()
val showMenu = remember { mutableStateOf(false) }
val canBeSelected = groupInfo.membership.memberRole >= member.memberRole && member.memberRole < GroupMemberRole.Moderator
SectionItemViewLongClickable(
click = {
if (selectedItems.value != null) {
if (canBeSelected) {
toggleItemSelection(member.groupMemberId, selectedItems)
}
} else {
showMemberInfo(member)
}
},
longClick = { showMenu.value = true },
minHeight = 54.dp,
padding = PaddingValues(horizontal = DEFAULT_PADDING)
) {
Box(contentAlignment = Alignment.CenterStart) {
androidx.compose.animation.AnimatedVisibility(selectedItems.value != null, enter = fadeIn(), exit = fadeOut()) {
SelectedListItem(Modifier.alpha(if (canBeSelected) 1f else 0f).padding(start = 2.dp), member.groupMemberId, selectedItems)
}
val selectionOffset by animateDpAsState(if (selectedItems.value != null) 20.dp + 22.dp * fontSizeMultiplier else 0.dp)
DropDownMenuForMember(chat.remoteHostId, member, groupInfo, selectedItems, showMenu)
Box(Modifier.padding(start = selectionOffset)) {
MemberRow(member)
}
} else {
showMemberInfo(member)
}
},
longClick = { showMenu.value = true },
minHeight = 54.dp,
padding = PaddingValues(horizontal = DEFAULT_PADDING)
) {
Box(contentAlignment = Alignment.CenterStart) {
androidx.compose.animation.AnimatedVisibility(selectedItems.value != null, enter = fadeIn(), exit = fadeOut()) {
SelectedListItem(Modifier.alpha(if (canBeSelected) 1f else 0f).padding(start = 2.dp), member.groupMemberId, selectedItems)
}
val selectionOffset by animateDpAsState(if (selectedItems.value != null) 20.dp + 22.dp * fontSizeMultiplier else 0.dp)
DropDownMenuForMember(chat.remoteHostId, member, groupInfo, selectedItems, showMenu)
Box(Modifier.padding(start = selectionOffset)) {
MemberRow(member)
}
}
}
}
item {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
if (!groupInfo.nextConnectPrepared) {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
}
SectionView {
ClearChatButton(clearChat)
if (groupInfo.canDelete) {

View file

@ -28,22 +28,21 @@ fun GroupLinkView(
chatModel: ChatModel,
rhId: Long?,
groupInfo: GroupInfo,
connLinkContact: CreatedConnLink?,
memberRole: GroupMemberRole?,
onGroupLinkUpdated: ((Pair<CreatedConnLink, GroupMemberRole>?) -> Unit)?,
groupLink: GroupLink?,
onGroupLinkUpdated: ((GroupLink?) -> Unit)?,
creatingGroup: Boolean = false,
close: (() -> Unit)? = null
) {
var groupLink by rememberSaveable(stateSaver = CreatedConnLink.nullableStateSaver) { mutableStateOf(connLinkContact) }
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
var groupLinkVar by rememberSaveable { mutableStateOf(groupLink) }
val groupLinkMemberRole = rememberSaveable { mutableStateOf(groupLink?.acceptMemberRole) }
var creatingLink by rememberSaveable { mutableStateOf(false) }
fun createLink() {
creatingLink = true
withBGApi {
val link = chatModel.controller.apiCreateGroupLink(rhId, groupInfo.groupId)
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
groupLinkVar = link
groupLinkMemberRole.value = link.acceptMemberRole
onGroupLinkUpdated?.invoke(link)
}
creatingLink = false
@ -54,8 +53,8 @@ fun GroupLinkView(
withBGApi {
val link = chatModel.controller.apiAddGroupShortLink(rhId, groupInfo.groupId)
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
groupLinkVar = link
groupLinkMemberRole.value = link.acceptMemberRole
onGroupLinkUpdated?.invoke(link)
}
creatingLink = false
@ -67,7 +66,7 @@ fun GroupLinkView(
}
}
GroupLinkLayout(
groupLink = groupLink,
groupLink = groupLinkVar,
groupInfo,
groupLinkMemberRole,
creatingLink,
@ -79,8 +78,8 @@ fun GroupLinkView(
withBGApi {
val link = chatModel.controller.apiGroupLinkMemberRole(rhId, groupInfo.groupId, role)
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
groupLinkVar = link
groupLinkMemberRole.value = link.acceptMemberRole
onGroupLinkUpdated?.invoke(link)
}
}
@ -95,7 +94,7 @@ fun GroupLinkView(
withBGApi {
val r = chatModel.controller.apiDeleteGroupLink(rhId, groupInfo.groupId)
if (r) {
groupLink = null
groupLinkVar = null
onGroupLinkUpdated?.invoke(null)
}
}
@ -113,7 +112,7 @@ fun GroupLinkView(
@Composable
fun GroupLinkLayout(
groupLink: CreatedConnLink?,
groupLink: GroupLink?,
groupInfo: GroupInfo,
groupLinkMemberRole: MutableState<GroupMemberRole?>,
creatingLink: Boolean,
@ -167,11 +166,11 @@ fun GroupLinkLayout(
}
val showShortLink = remember { mutableStateOf(true) }
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
if (groupLink.connShortLink == null) {
SimpleXCreatedLinkQRCode(groupLink, short = false)
if (groupLink.connLinkContact.connShortLink == null) {
SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = false)
} else {
SectionViewWithButton(titleButton = { ToggleShortLinkButton(showShortLink) }) {
SimpleXCreatedLinkQRCode(groupLink, short = showShortLink.value)
SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = showShortLink.value)
}
}
Row(
@ -183,7 +182,7 @@ fun GroupLinkLayout(
SimpleButton(
stringResource(MR.strings.share_link),
icon = painterResource(MR.images.ic_share),
click = { clipboard.shareText(groupLink.simplexChatUri(short = showShortLink.value)) }
click = { clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) }
)
if (creatingGroup && close != null) {
ContinueButton(close)
@ -196,8 +195,10 @@ fun GroupLinkLayout(
)
}
}
if (groupLink.connShortLink == null && appPreferences.privacyShortLinks.get()) {
AddShortLinkButton(addShortLink)
if (groupLink.connLinkContact.connShortLink == null) {
AddShortLinkButton(text = stringResource(MR.strings.add_short_link), addShortLink)
} else if (!groupLink.shortLinkDataSet) {
AddShortLinkButton(text = stringResource(MR.strings.share_group_profile_via_link), addShortLink)
}
}
}
@ -206,10 +207,10 @@ fun GroupLinkLayout(
}
@Composable
private fun AddShortLinkButton(onClick: () -> Unit) {
private fun AddShortLinkButton(text: String, onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_add),
stringResource(MR.strings.add_short_link),
text,
onClick,
iconColor = MaterialTheme.colors.primary,
textColor = MaterialTheme.colors.primary,

View file

@ -788,7 +788,7 @@ fun updateMembersRoleDialog(
fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) {
try {
withBGApi {
planAndConnect(rhId, connReqUri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
planAndConnect(rhId, connReqUri, close = { ModalManager.closeAllModalsEverywhere() })
}
} catch (e: RuntimeException) {
AlertManager.shared.showAlertMsg(

View file

@ -33,6 +33,7 @@ import chat.simplex.common.views.chatlist.openChat
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.datetime.Clock
import kotlin.math.*
@ -667,26 +668,30 @@ fun ChatItemView(
}
@Composable
fun E2EEInfoNoPQText() {
fun e2eeInfoText(sId: StringResource) {
Text(
buildAnnotatedString {
withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_no_pq)) }
withStyle(chatEventStyle) { append(annotatedStringResource(sId)) }
},
Modifier.padding(horizontal = 6.dp, vertical = 6.dp)
)
}
@Composable
fun E2EEInfoNoPQText() {
e2eeInfoText(MR.strings.e2ee_info_no_pq)
}
@Composable
fun DirectE2EEInfoText(e2EEInfo: E2EEInfo) {
if (e2EEInfo.pqEnabled) {
Text(
buildAnnotatedString {
withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_pq)) }
},
Modifier.padding(horizontal = 6.dp, vertical = 6.dp)
)
if (e2EEInfo.pqEnabled != null) {
if (e2EEInfo.pqEnabled) {
e2eeInfoText(MR.strings.e2ee_info_pq)
} else {
E2EEInfoNoPQText()
}
} else {
E2EEInfoNoPQText()
e2eeInfoText(MR.strings.e2ee_info_e2ee)
}
}

View file

@ -132,7 +132,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) { onRequestAccepted(it) } },
dropdownMenuItems = {
tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) {
ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu)
ContactRequestMenuItems(chat.remoteHostId, contactRequestId = chat.chatInfo.apiId, chatModel, showMenu)
}
},
showMenu,
@ -271,18 +271,22 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo
@Composable
fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMenu: MutableState<Boolean>, showMarkRead: Boolean) {
if (contact.activeConn != null) {
if (showMarkRead) {
MarkReadChatAction(chat, showMenu)
} else {
MarkUnreadChatAction(chat, chatModel, showMenu)
if (contact.nextAcceptContactRequest && contact.contactRequestId != null) {
ContactRequestMenuItems(chat.remoteHostId, contactRequestId = contact.contactRequestId, chatModel, showMenu)
} else {
if (contact.activeConn != null) {
if (showMarkRead) {
MarkReadChatAction(chat, showMenu)
} else {
MarkUnreadChatAction(chat, chatModel, showMenu)
}
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
ToggleNotificationsChatAction(chat, chatModel, contact.chatSettings.enableNtfs.nextMode(false), showMenu)
TagListAction(chat, showMenu)
ClearChatAction(chat, showMenu)
}
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
ToggleNotificationsChatAction(chat, chatModel, contact.chatSettings.enableNtfs.nextMode(false), showMenu)
TagListAction(chat, showMenu)
ClearChatAction(chat, showMenu)
DeleteContactAction(chat, chatModel, showMenu)
}
DeleteContactAction(chat, chatModel, showMenu)
}
@Composable
@ -509,30 +513,32 @@ fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, sh
}
@Composable
fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>, onSuccess: ((chat: Chat) -> Unit)? = null) {
fun ContactRequestMenuItems(rhId: Long?, contactRequestId: Long, chatModel: ChatModel, showMenu: MutableState<Boolean>, onSuccess: ((chat: Chat) -> Unit)? = null) {
ItemAction(
stringResource(MR.strings.accept_contact_button),
painterResource(MR.images.ic_check),
color = MaterialTheme.colors.onBackground,
onClick = {
acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel, onSuccess)
showMenu.value = false
}
)
ItemAction(
stringResource(MR.strings.accept_contact_incognito_button),
painterResource(MR.images.ic_theater_comedy),
color = MaterialTheme.colors.onBackground,
onClick = {
acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel, onSuccess)
acceptContactRequest(rhId, incognito = false, contactRequestId, true, chatModel, onSuccess)
showMenu.value = false
}
)
if (!chatModel.addressShortLinkDataSet) {
ItemAction(
stringResource(MR.strings.accept_contact_incognito_button),
painterResource(MR.images.ic_theater_comedy),
color = MaterialTheme.colors.onBackground,
onClick = {
acceptContactRequest(rhId, incognito = true, contactRequestId, true, chatModel, onSuccess)
showMenu.value = false
}
)
}
ItemAction(
stringResource(MR.strings.reject_contact_button),
painterResource(MR.images.ic_close),
onClick = {
rejectContactRequest(rhId, chatInfo, chatModel)
rejectContactRequest(rhId, contactRequestId, chatModel)
showMenu.value = false
},
color = Color.Red
@ -665,19 +671,21 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
Column {
SectionItemView({
AlertManager.shared.hideAlert()
acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel, onSucess)
acceptContactRequest(rhId, incognito = false, contactRequest.apiId, true, chatModel, onSucess)
}) {
Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel, onSucess)
}) {
Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
if (!chatModel.addressShortLinkDataSet) {
SectionItemView({
AlertManager.shared.hideAlert()
acceptContactRequest(rhId, incognito = true, contactRequest.apiId, true, chatModel, onSucess)
}) {
Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
SectionItemView({
AlertManager.shared.hideAlert()
rejectContactRequest(rhId, contactRequest, chatModel)
rejectContactRequest(rhId, contactRequest.apiId, chatModel)
}) {
Text(generalGetString(MR.strings.reject_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
@ -687,13 +695,17 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque
)
}
fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel, close: ((chat: Chat) -> Unit)? = null ) {
fun acceptContactRequest(rhId: Long?, incognito: Boolean, contactRequestId: Long, isCurrentUser: Boolean, chatModel: ChatModel, close: ((chat: Chat) -> Unit)? = null ) {
withBGApi {
val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId)
if (contact != null && isCurrentUser && contactRequest != null) {
val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, contactRequestId)
if (contact != null && isCurrentUser) {
val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf())
withContext(Dispatchers.Main) {
chatModel.chatsContext.replaceChat(rhId, contactRequest.id, chat)
if (contact.contactRequestId != null) { // means contact request was initially created with contact, so we don't need to replace it
chatModel.chatsContext.updateContact(rhId, contact)
} else {
chatModel.chatsContext.replaceChat(rhId, contactRequestChatId(contactRequestId), chat)
}
}
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
close?.invoke(chat)
@ -701,11 +713,18 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe
}
}
fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
fun rejectContactRequest(rhId: Long?, contactRequestId: Long, chatModel: ChatModel, dismissToChatList: Boolean = false) {
withBGApi {
chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId)
val contact_ = chatModel.controller.apiRejectContactRequest(rhId, contactRequestId)
withContext(Dispatchers.Main) {
chatModel.chatsContext.removeChat(rhId, contactRequest.id)
if (contact_ != null) { // means contact request was initially created with contact, so we need to remove contact chat
chatModel.chatsContext.removeChat(rhId, contact_.id)
} else {
chatModel.chatsContext.removeChat(rhId, contactRequestChatId(contactRequestId))
}
if (dismissToChatList) {
chatModel.chatId.value = null
}
}
}
}

View file

@ -590,7 +590,7 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) {
chatModel.appOpenUrl.value = rhId to uri
} else {
withBGApi {
planAndConnect(rhId, uri, incognito = null, close = null)
planAndConnect(rhId, uri, close = null)
}
}
}
@ -677,7 +677,6 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<
planAndConnect(
chatModel.remoteHostId(),
link,
incognito = null,
filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id },
filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id },
close = null,

View file

@ -137,42 +137,82 @@ fun ChatPreviewView(
fun chatPreviewTitle() {
val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) }
when (cInfo) {
is ChatInfo.Direct ->
Row(verticalAlignment = Alignment.CenterVertically) {
if (cInfo.contact.verified) {
VerifiedIcon()
is ChatInfo.Direct -> {
if (cInfo.contact.verified) {
VerifiedIcon()
}
val color = if (deleting)
MaterialTheme.colors.secondary
else if (cInfo.contact.nextAcceptContactRequest || cInfo.contact.sendMsgToConnect) {
MaterialTheme.colors.primary
} else if (!cInfo.contact.sndReady) {
MaterialTheme.colors.secondary
} else {
Color.Unspecified
}
chatPreviewTitleText(color = color)
}
is ChatInfo.Group -> {
val color = if (deleting) {
MaterialTheme.colors.secondary
} else {
when (cInfo.groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary
GroupMemberStatus.MemAccepted, GroupMemberStatus.MemRejected -> MaterialTheme.colors.secondary
else -> if (cInfo.groupInfo.nextConnectPrepared) MaterialTheme.colors.primary else Color.Unspecified
}
chatPreviewTitleText(
if (deleting)
MaterialTheme.colors.secondary
else
Color.Unspecified
)
}
is ChatInfo.Group ->
when (cInfo.groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> chatPreviewTitleText(
if (inProgress || deleting)
MaterialTheme.colors.secondary
else
if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary
)
GroupMemberStatus.MemAccepted, GroupMemberStatus.MemRejected -> chatPreviewTitleText(MaterialTheme.colors.secondary)
else -> chatPreviewTitleText(
if (deleting)
MaterialTheme.colors.secondary
else
Color.Unspecified
)
}
chatPreviewTitleText(color = color)
}
else -> chatPreviewTitleText()
}
}
@Composable
fun chatPreviewInfoText(): Pair<String, Color>? {
return when (cInfo) {
is ChatInfo.Direct ->
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null && cInfo.contact.active) {
stringResource(MR.strings.contact_tap_to_connect) to MaterialTheme.colors.primary
} else if (cInfo.contact.sendMsgToConnect) {
stringResource(MR.strings.open_to_connect) to Color.Unspecified
} else if (cInfo.contact.nextAcceptContactRequest) {
stringResource(MR.strings.open_to_accept) to Color.Unspecified
} else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null && cInfo.contact.active) {
if (cInfo.contact.preparedContact?.uiConnLinkType == ConnectionMode.Con) {
stringResource(MR.strings.contact_should_accept) to Color.Unspecified
} else {
stringResource(MR.strings.contact_connection_pending) to Color.Unspecified
}
} else {
null
}
is ChatInfo.Group ->
if (cInfo.groupInfo.nextConnectPrepared) {
stringResource(MR.strings.group_preview_open_to_join) to Color.Unspecified
} else {
when (cInfo.groupInfo.membership.memberStatus) {
GroupMemberStatus.MemRejected -> stringResource(MR.strings.group_preview_rejected) to Color.Unspecified
GroupMemberStatus.MemInvited -> groupInvitationPreviewText(currentUserProfileDisplayName, cInfo.groupInfo) to Color.Unspecified
GroupMemberStatus.MemAccepted -> stringResource(MR.strings.group_connection_pending) to Color.Unspecified
GroupMemberStatus.MemPendingReview, GroupMemberStatus.MemPendingApproval ->
stringResource(MR.strings.reviewed_by_admins) to MaterialTheme.colors.secondary
else -> null
}
}
else -> null
}
}
@Composable
fun chatPreviewText() {
val previewText = chatPreviewInfoText()
val ci = chat.chatItems.lastOrNull()
if (ci != null) {
if (ci?.content?.hasMsgContent != true && previewText != null) {
Text(previewText.first, color = previewText.second)
} else if (ci != null) {
if (showChatPreviews || (chatModelDraftChatId == chat.id && chatModelDraft != null)) {
val sp20 = with(LocalDensity.current) { 20.sp.toDp() }
val (text: CharSequence, inlineTextContent) = when {
@ -192,6 +232,7 @@ fun ChatPreviewView(
append(if (text.isEmpty()) mc.reason.text else "${mc.reason.text}: ")
}
}
else -> null
}
@ -200,7 +241,7 @@ fun ChatPreviewView(
formattedText,
sender = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName
cInfo is ChatInfo.Group && !ci.chatDir.sent && !ci.meta.showGroupAsSender -> ci.memberDisplayName
else -> null
},
mentions = ci.mentions,
@ -224,29 +265,6 @@ fun ChatPreviewView(
prefix = prefix
)
}
} else {
when (cInfo) {
is ChatInfo.Direct ->
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null && cInfo.contact.active) {
Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary)
} else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null) {
if (cInfo.contact.nextSendGrpInv) {
Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary)
} else if (cInfo.contact.active) {
Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary)
}
}
is ChatInfo.Group ->
when (cInfo.groupInfo.membership.memberStatus) {
GroupMemberStatus.MemRejected -> Text(stringResource(MR.strings.group_preview_rejected))
GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(currentUserProfileDisplayName, cInfo.groupInfo))
GroupMemberStatus.MemAccepted -> Text(stringResource(MR.strings.group_connection_pending), color = MaterialTheme.colors.secondary)
GroupMemberStatus.MemPendingReview, GroupMemberStatus.MemPendingApproval ->
Text(stringResource(MR.strings.reviewed_by_admins), color = MaterialTheme.colors.secondary)
else -> {}
}
else -> {}
}
}
}
@ -339,7 +357,10 @@ fun ChatPreviewView(
@Composable
fun chatStatusImage() {
if (cInfo is ChatInfo.Direct) {
if (cInfo.contact.active && cInfo.contact.activeConn != null) {
if (
cInfo.contact.active &&
(cInfo.contact.activeConn?.connStatus == ConnStatus.Ready || cInfo.contact.activeConn?.connStatus == ConnStatus.SndReady)
) {
val descr = contactNetworkStatus?.statusString
when (contactNetworkStatus) {
is NetworkStatus.Connected ->

View file

@ -53,13 +53,7 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, showDel
click = {
hideKeyboard(view)
when (contactType) {
ContactType.RECENT -> {
withApi {
openChat(secondaryChatsCtx = null, rhId, chat.chatInfo)
ModalManager.start.closeModals()
}
}
ContactType.CHAT_DELETED -> {
ContactType.RECENT, ContactType.CONTACT_WITH_REQUEST, ContactType.CHAT_DELETED -> {
withApi {
openChat(secondaryChatsCtx = null, rhId, chat.chatInfo)
ModalManager.start.closeModals()
@ -79,7 +73,17 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, showDel
},
dropdownMenuItems = {
tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) {
DeleteContactAction(chat, chatModel, showMenu)
if (contactType == ContactType.CONTACT_WITH_REQUEST && chat.chatInfo.contact.contactRequestId != null) {
ContactRequestMenuItems(
rhId = chat.remoteHostId,
contactRequestId = chat.chatInfo.contact.contactRequestId,
chatModel = chatModel,
showMenu = showMenu,
onSuccess = { onRequestAccepted(it) }
)
} else {
DeleteContactAction(chat, chatModel, showMenu)
}
}
},
showMenu,
@ -108,7 +112,7 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, showDel
tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) {
ContactRequestMenuItems(
rhId = chat.remoteHostId,
chatInfo = chat.chatInfo,
contactRequestId = chat.chatInfo.apiId,
chatModel = chatModel,
showMenu = showMenu,
onSuccess = { onRequestAccepted(it) }

View file

@ -38,8 +38,9 @@ fun ContactPreviewView(
val textColor = when {
deleting -> MaterialTheme.colors.secondary
contactType == ContactType.CARD -> MaterialTheme.colors.primary
contactType == ContactType.CONTACT_WITH_REQUEST -> MaterialTheme.colors.primary
contactType == ContactType.REQUEST -> MaterialTheme.colors.primary
contactType == ContactType.RECENT && chat.chatInfo.incognito -> Indigo
contactType == ContactType.RECENT -> if (chat.chatInfo.nextConnect) MaterialTheme.colors.primary else Color.Unspecified
else -> Color.Unspecified
}
@ -85,7 +86,7 @@ fun ContactPreviewView(
Spacer(Modifier.fillMaxWidth().weight(1f))
if (chat.chatInfo is ChatInfo.ContactRequest) {
if (chat.chatInfo is ChatInfo.ContactRequest || contactType == ContactType.CONTACT_WITH_REQUEST) {
Icon(
painterResource(MR.images.ic_check),
contentDescription = null,

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import chat.simplex.common.model.ChatModel
@ -266,6 +267,78 @@ class AlertManager {
hostDevice: Pair<Long?, String>? = null,
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), onConfirm, hostDevice)
fun showOpenChatAlert(
profileName: String,
profileImage: @Composable () -> Unit,
confirmText: String = generalGetString(MR.strings.connect_plan_open_chat),
onConfirm: () -> Unit,
dismissText: String = generalGetString(MR.strings.cancel_verb),
onDismiss: (() -> Unit)?,
) {
showAlert {
AlertDialog(
onDismissRequest = {
onDismiss?.invoke()
hideAlert()
},
buttons = {
AlertContent(text = null as String?, null) {
Column(
Modifier
.width(360.dp)
.padding(top = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceEvenly
) {
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
profileImage()
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
Text(
profileName,
fontWeight = FontWeight.SemiBold,
maxLines = 2
)
}
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
delay(200)
focusRequester.requestFocus()
}
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) {
Text(dismissText)
}
Spacer(Modifier.width(0.dp))
TextButton(onClick = {
onConfirm.invoke()
hideAlert()
}, Modifier.focusRequester(focusRequester)) {
Text(confirmText)
}
}
}
}
}
)
}
}
@Composable
fun showInView() {
alertViews.collectAsState().value.lastOrNull()?.invoke()

View file

@ -11,3 +11,5 @@ fun <T> audioProgressBarAnimationSpec() = tween<T>(durationMillis = 30, easing =
fun <T> userPickerAnimSpec() = tween<T>(256, 0, FastOutSlowInEasing)
fun <T> mentionPickerAnimSpec() = tween<T>(256, 0, FastOutSlowInEasing)
fun <T> contextUserPickerAnimSpec() = tween<T>(256, 0, FastOutSlowInEasing)

View file

@ -31,12 +31,7 @@ import kotlin.math.max
fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, shadow: Boolean = false) {
val icon =
when (chatInfo) {
is ChatInfo.Group ->
when (chatInfo.groupInfo.businessChat?.chatType) {
BusinessChatType.Business -> MR.images.ic_work_filled_padded
BusinessChatType.Customer -> MR.images.ic_account_circle_filled
null -> MR.images.ic_supervised_user_circle_filled
}
is ChatInfo.Group -> chatInfo.groupInfo.chatIconName
is ChatInfo.Local -> MR.images.ic_folder_filled
else -> MR.images.ic_account_circle_filled
}

Some files were not shown because too many files have changed in this diff Show more