ios: better error handling when connecting via links, improve alerts with chat information (#6012)

* simplexmq

* ios: error handling

* better new chat alerts

* vertical buttons in alert when they dont fit

* allow messages in prepared groups
This commit is contained in:
Evgeny
2025-06-26 12:10:06 +01:00
committed by GitHub
parent cc643e5aeb
commit 8b770ca03e
15 changed files with 245 additions and 185 deletions
+8 -5
View File
@@ -581,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
+17 -17
View File
@@ -899,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)? {
@@ -933,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",
@@ -974,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)
}
}
@@ -1027,16 +1025,18 @@ func apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) async throws -
throw r.unexpected
}
func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) async throws -> Contact {
let r: ChatResponse1 = try await chatSendCmd(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg))
if case let .startedConnectionToContact(_, contact) = r { return contact }
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 throws -> GroupInfo {
let r: ChatResponse1 = try await chatSendCmd(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg))
if case let .startedConnectionToGroup(_, groupInfo) = r { return groupInfo }
throw r.unexpected
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?) {
@@ -405,14 +405,15 @@ struct ComposeView: View {
}
if chat.chatInfo.groupInfo?.nextConnectPrepared == true {
Button(action: connectPreparedGroup) {
if chat.chatInfo.groupInfo?.businessChat == nil {
if chat.chatInfo.groupInfo?.businessChat == nil {
Button(action: connectPreparedGroup) {
Label("Join group", systemImage: "person.2.fill")
} else {
Label("Connect", systemImage: "briefcase.fill")
}
.frame(height: 60)
.disabled(composeState.inProgress)
} else {
sendContactRequestView(disableSendButton, icon: "briefcase.fill", sendRequest: connectPreparedGroup)
}
.frame(height: 60)
} else if contact?.nextSendGrpInv == true {
contextSendMessageToConnect("Send direct message to connect")
Divider()
@@ -428,24 +429,9 @@ struct ComposeView: View {
Label("Connect", systemImage: "person.fill.badge.plus")
}
.frame(height: 60)
.disabled(composeState.inProgress)
case .con:
HStack (alignment: .center) {
sendMessageView(
disableSendButton,
placeholder: NSLocalizedString("Add message", comment: "placeholder for sending contact request"),
sendToConnect: sendConnectPreparedContactRequest
)
if composeState.whitespaceOnly {
Button(action: sendConnectPreparedContactRequest) {
HStack {
Text("Connect").fontWeight(.medium)
Image(systemName: "person.fill.badge.plus")
}
}
.padding(.horizontal, 8)
}
}
.padding(.horizontal, 12)
sendContactRequestView(disableSendButton, icon: "person.fill.badge.plus", sendRequest: sendConnectPreparedContactRequest)
}
} else if contact?.nextAcceptContactRequest == true, let crId = contact?.contactRequestId {
ContextContactRequestActionsView(contactRequestId: crId)
@@ -620,6 +606,27 @@ 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(
@@ -695,6 +702,7 @@ struct ComposeView: View {
Task {
do {
if let mc = connectCheckLinkPreview() {
await sending()
let contact = try await apiSendMemberContactInvitation(chat.chatInfo.apiId, mc)
await MainActor.run {
self.chatModel.updateContact(contact)
@@ -704,6 +712,7 @@ struct ComposeView: View {
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))")
}
@@ -730,32 +739,30 @@ struct ComposeView: View {
private func sendConnectPreparedContact() {
Task {
do {
let mc = connectCheckLinkPreview()
let contact = try await apiConnectPreparedContact(contactId: chat.chatInfo.apiId, incognito: incognitoGroupDefault.get(), msg: mc)
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)
clearState()
}
} catch {
logger.error("ChatView.sendConnectPreparedContact error: \(error.localizedDescription)")
AlertManager.shared.showAlertMsg(title: "Error connecting with contact", message: "Error: \(responseError(error))")
} else {
await MainActor.run { composeState.inProgress = false }
}
}
}
private func connectPreparedGroup() {
Task {
do {
let mc = connectCheckLinkPreview()
let groupInfo = try await apiConnectPreparedGroup(groupId: chat.chatInfo.apiId, incognito: incognitoGroupDefault.get(), msg: mc)
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()
}
} catch {
logger.error("ChatView.connectPreparedGroup error: \(error.localizedDescription)")
AlertManager.shared.showAlertMsg(title: "Error joining group", message: "Error: \(responseError(error))")
} else {
await MainActor.run { composeState.inProgress = false }
}
}
}
@@ -1094,10 +1101,6 @@ struct ComposeView: View {
}
}
func sending() async {
await MainActor.run { composeState.inProgress = true }
}
func updateMessage(_ ei: ChatItem, live: Bool) async -> ChatItem? {
if let oldMsgContent = ei.content.msgContent {
do {
@@ -1270,6 +1273,10 @@ struct ComposeView: View {
}
}
func sending() async {
await MainActor.run { composeState.inProgress = true }
}
private func startVoiceMessageRecording() async {
startingRecording = true
let fileName = generateNewFileName("voice", "m4a")
@@ -13,60 +13,66 @@ 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) {
Label("Reject", systemImage: "multiply")
.foregroundColor(.red)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.onTapGesture {
showRejectRequestAlert(contactRequestId)
Button(role: .destructive, action: showRejectRequestAlert) {
Label("Reject", systemImage: "multiply")
}
.frame(maxWidth: .infinity, minHeight: 60)
Label("Accept", systemImage: "checkmark").foregroundColor(theme.colors.primary)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.onTapGesture {
Button {
if ChatModel.shared.addressShortLinkDataSet {
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) }
acceptRequest()
} else {
showAcceptRequestAlert(contactRequestId)
showAcceptRequestAlert()
}
} label: {
Label("Accept", systemImage: "checkmark")
}
.frame(maxWidth: .infinity, minHeight: 60)
}
.frame(minHeight: 60)
.disabled(inProgress)
.frame(maxWidth: .infinity)
.background(ToolbarMaterial.material(toolbarMaterial))
}
}
func showRejectRequestAlert(_ contactRequestId: Int64) {
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 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
]}
)
}
func showAcceptRequestAlert(_ contactRequestId: Int64) {
showAlert(
NSLocalizedString("Accept contact request", comment: "alert title"),
actions: {[
UIAlertAction(title: NSLocalizedString("Accept", comment: "alert action"), style: .default) { _ in
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) }
},
UIAlertAction(title: NSLocalizedString("Accept incognito", comment: "alert action"), style: .default) { _ in
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) }
},
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 {
@@ -887,7 +887,6 @@ struct GroupPreferencesButton: View {
}
}
}
}
+72 -34
View File
@@ -90,8 +90,15 @@ let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert
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
@@ -100,6 +107,7 @@ class OpenChatAlertViewController: UIViewController {
init(
profileName: String,
profileFullName: String,
profileImage: UIView,
cancelTitle: String = "Cancel",
confirmTitle: String = "Open",
@@ -107,6 +115,7 @@ class OpenChatAlertViewController: UIViewController {
onConfirm: @escaping () -> Void
) {
self.profileName = profileName
self.profileFullName = profileFullName
self.profileImage = profileImage
self.cancelTitle = cancelTitle
self.confirmTitle = confirmTitle
@@ -135,54 +144,72 @@ class OpenChatAlertViewController: UIViewController {
// Profile image sizing
profileImage.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
profileImage.widthAnchor.constraint(equalToConstant: 60),
profileImage.heightAnchor.constraint(equalToConstant: 60)
profileImage.widthAnchor.constraint(equalToConstant: alertProfileImageSize),
profileImage.heightAnchor.constraint(equalToConstant: alertProfileImageSize)
])
// Name label
let nameLabel = UILabel()
nameLabel.text = profileName
nameLabel.font = UIFont.systemFont(ofSize: 18, weight: .semibold)
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 hStack = UIStackView(arrangedSubviews: [profileImage, nameLabel])
hStack.axis = .horizontal
hStack.spacing = 12
hStack.alignment = .center
hStack.translatesAutoresizingMaskIntoConstraints = false
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(hStack)
topRowContainer.addSubview(stack)
NSLayoutConstraint.activate([
hStack.topAnchor.constraint(equalTo: topRowContainer.topAnchor),
hStack.bottomAnchor.constraint(equalTo: topRowContainer.bottomAnchor),
hStack.leadingAnchor.constraint(equalTo: topRowContainer.leadingAnchor, constant: 20),
hStack.trailingAnchor.constraint(equalTo: topRowContainer.trailingAnchor, constant: -20)
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)
cancelButton.titleLabel?.font = UIFont.systemFont(ofSize: 15)
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.systemFont(ofSize: 15, weight: .semibold)
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: [cancelButton, confirmButton])
buttonStack.axis = .horizontal
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: 50).isActive = true
buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: alertButtonHeight * (verticalButtons ? 2 : 1)).isActive = true
// Vertical stack containing hStack and buttonStack
let vStack = UIStackView(arrangedSubviews: [topRowContainer, buttonStack])
@@ -195,23 +222,38 @@ class OpenChatAlertViewController: UIViewController {
// Add horizontal divider above buttons
let horizontalDivider = UIView()
horizontalDivider.backgroundColor = UIColor(white: 0.85, alpha: 1)
horizontalDivider.backgroundColor = UIColor.separator
horizontalDivider.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(horizontalDivider)
// Add vertical divider between buttons
let verticalDivider = UIView()
verticalDivider.backgroundColor = UIColor(white: 0.85, alpha: 1)
verticalDivider.translatesAutoresizingMaskIntoConstraints = false
buttonStack.addSubview(verticalDivider)
// 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: 280),
containerView.widthAnchor.constraint(equalToConstant: alertWidth),
// Vertical stack padding inside containerView
vStack.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20),
@@ -220,20 +262,14 @@ class OpenChatAlertViewController: UIViewController {
vStack.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0),
// Center hStack horizontally inside vStack's padded width
hStack.centerXAnchor.constraint(equalTo: vStack.centerXAnchor),
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),
// Vertical divider between buttons
verticalDivider.widthAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale),
verticalDivider.topAnchor.constraint(equalTo: buttonStack.topAnchor),
verticalDivider.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
verticalDivider.centerXAnchor.constraint(equalTo: buttonStack.centerXAnchor)
])
horizontalDivider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale)
] + buttonDividerConstraints)
}
@objc private func cancelTapped() {
@@ -252,6 +288,7 @@ class OpenChatAlertViewController: UIViewController {
func showOpenChatAlert<Content: View>(
profileName: String,
profileFullName: String,
profileImage: Content,
theme: AppTheme,
cancelTitle: String = "Cancel",
@@ -267,6 +304,7 @@ func showOpenChatAlert<Content: View>(
if let topVC = getTopViewController() {
let alertVC = OpenChatAlertViewController(
profileName: profileName,
profileFullName: profileFullName,
profileImage: hostedView,
cancelTitle: cancelTitle,
confirmTitle: confirmTitle,
@@ -1014,11 +1014,12 @@ private func showPrepareContactAlert(
) {
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: 60
size: alertProfileImageSize
),
theme: theme,
cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"),
@@ -1054,10 +1055,11 @@ private func showPrepareGroupAlert(
) {
showOpenChatAlert(
profileName: groupShortLinkData.groupProfile.displayName,
profileImage: ProfileImage(imageStr: groupShortLinkData.groupProfile.image, iconName: "person.2.circle.fill", size: 60),
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 chat", comment: "new chat action"),
confirmTitle: NSLocalizedString("Open group", comment: "new chat action"),
onCancel: { cleanup?() },
onConfirm: {
Task {
+8 -8
View File
@@ -178,8 +178,8 @@
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-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a */; };
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.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 */; };
@@ -543,8 +543,8 @@
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-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a"; sourceTree = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.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>"; };
@@ -704,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-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a in Frameworks */,
64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.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;
@@ -790,8 +790,8 @@
64C829992D54AEEE006B9E89 /* libffi.a */,
64C829982D54AEED006B9E89 /* libgmp.a */,
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */,
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a */,
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.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>";
+1 -1
View File
@@ -819,7 +819,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)
+38 -33
View File
@@ -1372,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)
@@ -1421,7 +1423,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
default: false
}
}
public var groupInfo: GroupInfo? {
switch self {
case let .group(groupInfo, _): return groupInfo
@@ -1531,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
@@ -1547,7 +1549,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
default: return nil
}
}
public var nextNtfMode: MsgFilter? {
self.chatSettings?.enableNtfs.nextMode(mentions: hasMentions)
}
@@ -1596,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):
@@ -1644,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),
@@ -2085,15 +2087,14 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
var createdAt: Date
var updatedAt: Date
var chatTs: Date?
public var connLinkToConnect: CreatedConnLink?
public var connLinkStartedConnection: Bool
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 { connLinkToConnect != nil && !connLinkStartedConnection }
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 } }
@@ -2134,13 +2135,17 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable {
chatSettings: ChatSettings.defaults,
createdAt: .now,
updatedAt: .now,
connLinkStartedConnection: false,
membersRequireAttention: 0,
chatTags: [],
localAlias: ""
)
}
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
@@ -2298,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
@@ -2378,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
}
@@ -2390,7 +2395,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
memberChatVRange
}
}
public var memberIncognito: Bool {
memberProfile.profileId != memberContactProfileId
}
@@ -2446,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")
@@ -2602,7 +2607,7 @@ public enum ConnectionEntity: Decodable, Hashable {
nil
}
}
// public var localDisplayName: String? {
// switch self {
// case let .rcvDirectMsgConnection(conn, contact):
@@ -2643,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 }
@@ -2703,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(
@@ -2954,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):
@@ -3054,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(
@@ -3175,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
@@ -3311,7 +3316,7 @@ public enum CIStatus: Decodable, Hashable {
case .invalid: return "invalid"
}
}
public var sent: Bool {
switch self {
case .sndNew: true
@@ -4124,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)
@@ -4426,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 {
@@ -4439,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: [
@@ -4486,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
@@ -4535,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")
@@ -4913,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
@@ -4924,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
@@ -4951,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
@@ -7055,7 +7055,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}"
@@ -7068,7 +7068,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()
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 976bd3a389aaded78b4285541e6dddd6b2766149
tag: b4bcfd325b43caefd9b649653c1b3ee6920bad61
source-repository-package
type: git
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."976bd3a389aaded78b4285541e6dddd6b2766149" = "06mijsfnb9q9wa0lj49a24ajnw45qash7sc9ah95cd517bj6rnki";
"https://github.com/simplex-chat/simplexmq.git"."b4bcfd325b43caefd9b649653c1b3ee6920bad61" = "1mg01aj2cafrkiz2pdjp39x8rdbqip4jyn5p1vwbi5rj0fsdifkm";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+1 -1
View File
@@ -670,7 +670,7 @@ receiveFileEvt' user ft userApprovedRelays rcvInline_ filePath_ = do
rctFileCancelled :: ChatError -> Bool
rctFileCancelled = \case
ChatErrorAgent (SMP _ SMP.AUTH) _ -> True
ChatErrorAgent (CONN DUPLICATE) _ -> True
ChatErrorAgent (CONN DUPLICATE _) _ -> True
_ -> False
acceptFileReceive :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM AChatItem
+1 -1
View File
@@ -2477,7 +2477,7 @@ viewChatError isCmd logLevel testView = \case
BROKER _ TIMEOUT | not isCmd -> []
AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd]
AGENT (A_PROHIBITED e) -> [withConnEntity <> "error: AGENT A_PROHIBITED, " <> plain e | logLevel <= CLLWarning || isCmd]
CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning || isCmd]
CONN NOT_FOUND _ -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning || isCmd]
CRITICAL restart e -> [plain $ "critical error: " <> e] <> ["please restart the app" | restart]
INTERNAL e -> [plain $ "internal error: " <> e]
e -> [withConnEntity <> "smp agent error: " <> sShow e | logLevel <= CLLWarning || isCmd]