mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
Merge e31e8a6def
into 8f9bb4dc5b
This commit is contained in:
commit
51b1c6aa30
161 changed files with 7373 additions and 2982 deletions
|
@ -437,6 +437,7 @@ struct ContentView: View {
|
|||
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
|
||||
planAndConnect(
|
||||
link,
|
||||
theme: theme,
|
||||
dismiss: false
|
||||
)
|
||||
} else {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)" } }
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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: []
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -345,6 +345,7 @@ struct GroupMemberInfoView: View {
|
|||
Button {
|
||||
planAndConnect(
|
||||
contactLink,
|
||||
theme: theme,
|
||||
dismiss: true
|
||||
)
|
||||
} label: {
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -668,6 +668,7 @@ struct ChatListSearchBar: View {
|
|||
private func connect(_ link: String) {
|
||||
planAndConnect(
|
||||
link,
|
||||
theme: theme,
|
||||
dismiss: false,
|
||||
cleanup: {
|
||||
searchText = ""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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: %@ 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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: %@ 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: %@ 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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -394,8 +394,8 @@
|
|||
<source>- connect to [directory service](simplex:/contact#/?v=1-4&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&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&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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: %@ 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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? 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? 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve" approved="no">
|
||||
<source><p>Hi!</p>
|
||||
<p><a href="%@">Connect to me via SimpleX Chat</a></p></source>
|
||||
<target state="translated"><p>你好!</p>
|
||||
<target state="translated"><p>你好!</p>
|
||||
<p><a href="%@">來連接我透過SimpleX Chat</a></p></target>
|
||||
<note>email text</note>
|
||||
</trans-unit>
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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アドレス";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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 ของคุณ";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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 地址";
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -934,7 +934,7 @@ fun CallButton(
|
|||
}
|
||||
}
|
||||
} }
|
||||
contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } }
|
||||
contact.sendMsgToConnect -> { { showCantCallContactSendMessageAlert() } }
|
||||
!contact.active -> { { showCantCallContactDeletedAlert() } }
|
||||
!contact.ready -> { { showCantCallContactConnectingAlert() } }
|
||||
needToAllowCallsToContact -> { { showNeedToAllowCallsAlert(onConfirm = { allowCallsToContact(chat) }) } }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue