Merge branch 'master' into remote-desktop
This commit is contained in:
commit
5d4006f291
@ -31,11 +31,11 @@ struct ContentView: View {
|
|||||||
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
@State private var chatListActionSheet: ChatListActionSheet? = nil
|
||||||
|
|
||||||
private enum ChatListActionSheet: Identifiable {
|
private enum ChatListActionSheet: Identifiable {
|
||||||
case connectViaUrl(action: ConnReqType, link: String)
|
case planAndConnectSheet(sheet: PlanAndConnectActionSheet)
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case let .connectViaUrl(_, link): return "connectViaUrl \(link)"
|
case let .planAndConnectSheet(sheet): return sheet.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,7 +93,7 @@ struct ContentView: View {
|
|||||||
mainView()
|
mainView()
|
||||||
.actionSheet(item: $chatListActionSheet) { sheet in
|
.actionSheet(item: $chatListActionSheet) { sheet in
|
||||||
switch sheet {
|
switch sheet {
|
||||||
case let .connectViaUrl(action, link): return connectViaUrlSheet(action, link)
|
case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -290,12 +290,19 @@ struct ContentView: View {
|
|||||||
if let url = m.appOpenUrl {
|
if let url = m.appOpenUrl {
|
||||||
m.appOpenUrl = nil
|
m.appOpenUrl = nil
|
||||||
var path = url.path
|
var path = url.path
|
||||||
logger.debug("ContentView.connectViaUrl path: \(path)")
|
|
||||||
if (path == "/contact" || path == "/invitation") {
|
if (path == "/contact" || path == "/invitation") {
|
||||||
path.removeFirst()
|
path.removeFirst()
|
||||||
let action: ConnReqType = path == "contact" ? .contact : .invitation
|
// TODO normalize in backend; revert
|
||||||
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
|
// let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
|
||||||
chatListActionSheet = .connectViaUrl(action: action, link: link)
|
var link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
|
||||||
|
link = link.starts(with: "simplex:/") ? link.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/") : link
|
||||||
|
planAndConnect(
|
||||||
|
link,
|
||||||
|
showAlert: showPlanAndConnectAlert,
|
||||||
|
showActionSheet: { chatListActionSheet = .planAndConnectSheet(sheet: $0) },
|
||||||
|
dismiss: false,
|
||||||
|
incognito: nil
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid")))
|
AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid")))
|
||||||
}
|
}
|
||||||
@ -303,20 +310,8 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func connectViaUrlSheet(_ action: ConnReqType, _ link: String) -> ActionSheet {
|
private func showPlanAndConnectAlert(_ alert: PlanAndConnectAlert) {
|
||||||
let title: LocalizedStringKey
|
AlertManager.shared.showAlert(planAndConnectAlert(alert, dismiss: false))
|
||||||
switch action {
|
|
||||||
case .contact: title = "Connect via contact link"
|
|
||||||
case .invitation: title = "Connect via one-time link"
|
|
||||||
}
|
|
||||||
return ActionSheet(
|
|
||||||
title: Text(title),
|
|
||||||
buttons: [
|
|
||||||
.default(Text("Use current profile")) { connectViaLink(link, incognito: false) },
|
|
||||||
.default(Text("Use new incognito profile")) { connectViaLink(link, incognito: true) },
|
|
||||||
.cancel()
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +152,16 @@ final class ChatModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getGroupChat(_ groupId: Int64) -> Chat? {
|
||||||
|
chats.first { chat in
|
||||||
|
if case let .group(groupInfo) = chat.chatInfo {
|
||||||
|
return groupInfo.groupId == groupId
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func getChatIndex(_ id: String) -> Int? {
|
private func getChatIndex(_ id: String) -> Int? {
|
||||||
chats.firstIndex(where: { $0.id == id })
|
chats.firstIndex(where: { $0.id == id })
|
||||||
}
|
}
|
||||||
@ -688,41 +698,3 @@ final class Chat: ObservableObject, Identifiable {
|
|||||||
|
|
||||||
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NetworkStatus: Decodable, Equatable {
|
|
||||||
case unknown
|
|
||||||
case connected
|
|
||||||
case disconnected
|
|
||||||
case error(String)
|
|
||||||
|
|
||||||
var statusString: LocalizedStringKey {
|
|
||||||
get {
|
|
||||||
switch self {
|
|
||||||
case .connected: return "connected"
|
|
||||||
case .error: return "error"
|
|
||||||
default: return "connecting"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusExplanation: LocalizedStringKey {
|
|
||||||
get {
|
|
||||||
switch self {
|
|
||||||
case .connected: return "You are connected to the server used to receive messages from this contact."
|
|
||||||
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
|
|
||||||
default: return "Trying to connect to the server used to receive messages from this contact."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var imageName: String {
|
|
||||||
get {
|
|
||||||
switch self {
|
|
||||||
case .unknown: return "circle.dotted"
|
|
||||||
case .connected: return "circle.fill"
|
|
||||||
case .disconnected: return "ellipsis.circle.fill"
|
|
||||||
case .error: return "exclamationmark.circle.fill"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -586,6 +586,15 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiConnectPlan(connReq: String) async throws -> ConnectionPlan {
|
||||||
|
logger.error("apiConnectPlan connReq: \(connReq)")
|
||||||
|
let userId = try currentUserId("apiConnectPlan")
|
||||||
|
let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq))
|
||||||
|
if case let .connectionPlan(_, connectionPlan) = r { return connectionPlan }
|
||||||
|
logger.error("apiConnectPlan error: \(responseError(r))")
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? {
|
func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? {
|
||||||
let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
|
let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq)
|
||||||
if let alert = alert {
|
if let alert = alert {
|
||||||
@ -610,10 +619,7 @@ func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert
|
|||||||
if let c = m.getContactChat(contact.contactId) {
|
if let c = m.getContactChat(contact.contactId) {
|
||||||
await MainActor.run { m.chatId = c.id }
|
await MainActor.run { m.chatId = c.id }
|
||||||
}
|
}
|
||||||
let alert = mkAlert(
|
let alert = contactAlreadyExistsAlert(contact)
|
||||||
title: "Contact already exists",
|
|
||||||
message: "You are already connected to \(contact.displayName)."
|
|
||||||
)
|
|
||||||
return (nil, alert)
|
return (nil, alert)
|
||||||
case .chatCmdError(_, .error(.invalidConnReq)):
|
case .chatCmdError(_, .error(.invalidConnReq)):
|
||||||
let alert = mkAlert(
|
let alert = mkAlert(
|
||||||
@ -641,6 +647,13 @@ func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert
|
|||||||
return (nil, alert)
|
return (nil, alert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func contactAlreadyExistsAlert(_ contact: Contact) -> Alert {
|
||||||
|
mkAlert(
|
||||||
|
title: "Contact already exists",
|
||||||
|
message: "You are already connected to \(contact.displayName)."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private func connectionErrorAlert(_ r: ChatResponse) -> Alert {
|
private func connectionErrorAlert(_ r: ChatResponse) -> Alert {
|
||||||
if let networkErrorAlert = networkErrorAlert(r) {
|
if let networkErrorAlert = networkErrorAlert(r) {
|
||||||
return networkErrorAlert
|
return networkErrorAlert
|
||||||
@ -944,6 +957,12 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] {
|
||||||
|
let r = chatSendCmdSync(.apiGetNetworkStatuses)
|
||||||
|
if case let .networkStatuses(_, statuses) = r { return statuses }
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async {
|
func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async {
|
||||||
do {
|
do {
|
||||||
if chat.chatStats.unreadCount > 0 {
|
if chat.chatStats.unreadCount > 0 {
|
||||||
@ -1348,13 +1367,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
await updateContactsStatus(contactRefs, status: .connected)
|
await updateContactsStatus(contactRefs, status: .connected)
|
||||||
case let .contactsDisconnected(_, contactRefs):
|
case let .contactsDisconnected(_, contactRefs):
|
||||||
await updateContactsStatus(contactRefs, status: .disconnected)
|
await updateContactsStatus(contactRefs, status: .disconnected)
|
||||||
case let .contactSubError(user, contact, chatError):
|
|
||||||
await MainActor.run {
|
|
||||||
if active(user) {
|
|
||||||
m.updateContact(contact)
|
|
||||||
}
|
|
||||||
processContactSubError(contact, chatError)
|
|
||||||
}
|
|
||||||
case let .contactSubSummary(_, contactSubscriptions):
|
case let .contactSubSummary(_, contactSubscriptions):
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
for sub in contactSubscriptions {
|
for sub in contactSubscriptions {
|
||||||
@ -1369,6 +1381,18 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case let .networkStatus(status, connections):
|
||||||
|
await MainActor.run {
|
||||||
|
for cId in connections {
|
||||||
|
m.networkStatuses[cId] = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case let .networkStatuses(_, statuses): ()
|
||||||
|
await MainActor.run {
|
||||||
|
for s in statuses {
|
||||||
|
m.networkStatuses[s.agentConnId] = s.networkStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
case let .newChatItem(user, aChatItem):
|
case let .newChatItem(user, aChatItem):
|
||||||
let cInfo = aChatItem.chatInfo
|
let cInfo = aChatItem.chatInfo
|
||||||
let cItem = aChatItem.chatItem
|
let cItem = aChatItem.chatItem
|
||||||
@ -1649,7 +1673,7 @@ func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
|
|||||||
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
|
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
|
||||||
default: err = String(describing: chatError)
|
default: err = String(describing: chatError)
|
||||||
}
|
}
|
||||||
m.setContactNetworkStatus(contact, .error(err))
|
m.setContactNetworkStatus(contact, .error(connectionError: err))
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshCallInvitations() throws {
|
func refreshCallInvitations() throws {
|
||||||
|
@ -19,7 +19,7 @@ struct GroupMemberInfoView: View {
|
|||||||
@State private var connectionCode: String? = nil
|
@State private var connectionCode: String? = nil
|
||||||
@State private var newRole: GroupMemberRole = .member
|
@State private var newRole: GroupMemberRole = .member
|
||||||
@State private var alert: GroupMemberInfoViewAlert?
|
@State private var alert: GroupMemberInfoViewAlert?
|
||||||
@State private var connectToMemberDialog: Bool = false
|
@State private var sheet: PlanAndConnectActionSheet?
|
||||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||||
@State private var justOpened = true
|
@State private var justOpened = true
|
||||||
@State private var progressIndicator = false
|
@State private var progressIndicator = false
|
||||||
@ -30,9 +30,8 @@ struct GroupMemberInfoView: View {
|
|||||||
case switchAddressAlert
|
case switchAddressAlert
|
||||||
case abortSwitchAddressAlert
|
case abortSwitchAddressAlert
|
||||||
case syncConnectionForceAlert
|
case syncConnectionForceAlert
|
||||||
case connRequestSentAlert(type: ConnReqType)
|
case planAndConnectAlert(alert: PlanAndConnectAlert)
|
||||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||||
case other(alert: Alert)
|
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
@ -41,9 +40,8 @@ struct GroupMemberInfoView: View {
|
|||||||
case .switchAddressAlert: return "switchAddressAlert"
|
case .switchAddressAlert: return "switchAddressAlert"
|
||||||
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
case .abortSwitchAddressAlert: return "abortSwitchAddressAlert"
|
||||||
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
case .syncConnectionForceAlert: return "syncConnectionForceAlert"
|
||||||
case .connRequestSentAlert: return "connRequestSentAlert"
|
case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)"
|
||||||
case let .error(title, _): return "error \(title)"
|
case let .error(title, _): return "error \(title)"
|
||||||
case let .other(alert): return "other \(alert)"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,11 +204,11 @@ struct GroupMemberInfoView: View {
|
|||||||
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
|
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
|
||||||
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
|
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress)
|
||||||
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
|
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) })
|
||||||
case let .connRequestSentAlert(type): return connReqSentAlert(type)
|
case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true)
|
||||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||||
case let .other(alert): return alert
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
|
||||||
|
|
||||||
if progressIndicator {
|
if progressIndicator {
|
||||||
ProgressView().scaleEffect(2)
|
ProgressView().scaleEffect(2)
|
||||||
@ -220,25 +218,16 @@ struct GroupMemberInfoView: View {
|
|||||||
|
|
||||||
func connectViaAddressButton(_ contactLink: String) -> some View {
|
func connectViaAddressButton(_ contactLink: String) -> some View {
|
||||||
Button {
|
Button {
|
||||||
connectToMemberDialog = true
|
planAndConnect(
|
||||||
|
contactLink,
|
||||||
|
showAlert: { alert = .planAndConnectAlert(alert: $0) },
|
||||||
|
showActionSheet: { sheet = $0 },
|
||||||
|
dismiss: true,
|
||||||
|
incognito: nil
|
||||||
|
)
|
||||||
} label: {
|
} label: {
|
||||||
Label("Connect", systemImage: "link")
|
Label("Connect", systemImage: "link")
|
||||||
}
|
}
|
||||||
.confirmationDialog("Connect directly", isPresented: $connectToMemberDialog, titleVisibility: .visible) {
|
|
||||||
Button("Use current profile") { connectViaAddress(incognito: false, contactLink: contactLink) }
|
|
||||||
Button("Use new incognito profile") { connectViaAddress(incognito: true, contactLink: contactLink) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func connectViaAddress(incognito: Bool, contactLink: String) {
|
|
||||||
Task {
|
|
||||||
let (connReqType, connectAlert) = await apiConnect_(incognito: incognito, connReq: contactLink)
|
|
||||||
if let connReqType = connReqType {
|
|
||||||
alert = .connRequestSentAlert(type: connReqType)
|
|
||||||
} else if let connectAlert = connectAlert {
|
|
||||||
alert = .other(alert: connectAlert)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func knownDirectChatButton(_ chat: Chat) -> some View {
|
func knownDirectChatButton(_ chat: Chat) -> some View {
|
||||||
|
@ -58,65 +58,331 @@ struct NewChatButton: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ConnReqType: Equatable {
|
enum PlanAndConnectAlert: Identifiable {
|
||||||
case contact
|
case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||||
case invitation
|
case invitationLinkConnecting(connectionLink: String)
|
||||||
|
case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||||
|
case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool)
|
||||||
|
case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)"
|
||||||
|
case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)"
|
||||||
|
case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)"
|
||||||
|
case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)"
|
||||||
|
case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectViaLink(_ connectionLink: String, dismiss: DismissAction? = nil, incognito: Bool) {
|
func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert {
|
||||||
Task {
|
switch alert {
|
||||||
if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) {
|
case let .ownInvitationLinkConfirmConnect(connectionLink, connectionPlan, incognito):
|
||||||
DispatchQueue.main.async {
|
return Alert(
|
||||||
dismiss?()
|
title: Text("Connect to yourself?"),
|
||||||
AlertManager.shared.showAlert(connReqSentAlert(connReqType))
|
message: Text("This is your own one-time link!"),
|
||||||
}
|
primaryButton: .destructive(
|
||||||
|
Text(incognito ? "Connect incognito" : "Connect"),
|
||||||
|
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
|
||||||
|
),
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
case .invitationLinkConnecting:
|
||||||
|
return Alert(
|
||||||
|
title: Text("Already connecting!"),
|
||||||
|
message: Text("You are already connecting via this one-time link!")
|
||||||
|
)
|
||||||
|
case let .ownContactAddressConfirmConnect(connectionLink, connectionPlan, incognito):
|
||||||
|
return Alert(
|
||||||
|
title: Text("Connect to yourself?"),
|
||||||
|
message: Text("This is your own SimpleX address!"),
|
||||||
|
primaryButton: .destructive(
|
||||||
|
Text(incognito ? "Connect incognito" : "Connect"),
|
||||||
|
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
|
||||||
|
),
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito):
|
||||||
|
return Alert(
|
||||||
|
title: Text("Join group?"),
|
||||||
|
message: Text("You will connect to all group members."),
|
||||||
|
primaryButton: .default(
|
||||||
|
Text(incognito ? "Join incognito" : "Join"),
|
||||||
|
action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) }
|
||||||
|
),
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
)
|
||||||
|
case let .groupLinkConnecting(_, groupInfo):
|
||||||
|
if let groupInfo = groupInfo {
|
||||||
|
return Alert(
|
||||||
|
title: Text("Group already exists!"),
|
||||||
|
message: Text("You are already joining the group \(groupInfo.displayName).")
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
DispatchQueue.main.async {
|
return Alert(
|
||||||
dismiss?()
|
title: Text("Already joining the group!"),
|
||||||
|
message: Text("You are already joining the group via this link.")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PlanAndConnectActionSheet: Identifiable {
|
||||||
|
case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey)
|
||||||
|
case ownLinkAskCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
|
||||||
|
case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)"
|
||||||
|
case let .ownLinkAskCurrentOrIncognitoProfile(connectionLink, _, _): return "ownLinkAskCurrentOrIncognitoProfile \(connectionLink)"
|
||||||
|
case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool) -> ActionSheet {
|
||||||
|
switch sheet {
|
||||||
|
case let .askCurrentOrIncognitoProfile(connectionLink, connectionPlan, title):
|
||||||
|
return ActionSheet(
|
||||||
|
title: Text(title),
|
||||||
|
buttons: [
|
||||||
|
.default(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
|
||||||
|
.default(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
|
||||||
|
.cancel()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
case let .ownLinkAskCurrentOrIncognitoProfile(connectionLink, connectionPlan, title):
|
||||||
|
return ActionSheet(
|
||||||
|
title: Text(title),
|
||||||
|
buttons: [
|
||||||
|
.destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
|
||||||
|
.destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
|
||||||
|
.cancel()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
case let .ownGroupLinkConfirmConnect(connectionLink, connectionPlan, incognito, groupInfo):
|
||||||
|
if let incognito = incognito {
|
||||||
|
return ActionSheet(
|
||||||
|
title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
|
||||||
|
buttons: [
|
||||||
|
.default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
|
||||||
|
.destructive(Text(incognito ? "Join incognito" : "Join with current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) },
|
||||||
|
.cancel()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return ActionSheet(
|
||||||
|
title: Text("Join your group?\nThis is your link for group \(groupInfo.displayName)!"),
|
||||||
|
buttons: [
|
||||||
|
.default(Text("Open group")) { openKnownGroup(groupInfo, dismiss: dismiss, showAlreadyExistsAlert: nil) },
|
||||||
|
.destructive(Text("Use current profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: false) },
|
||||||
|
.destructive(Text("Use new incognito profile")) { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: true) },
|
||||||
|
.cancel()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func planAndConnect(
|
||||||
|
_ connectionLink: String,
|
||||||
|
showAlert: @escaping (PlanAndConnectAlert) -> Void,
|
||||||
|
showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void,
|
||||||
|
dismiss: Bool,
|
||||||
|
incognito: Bool?
|
||||||
|
) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let connectionPlan = try await apiConnectPlan(connReq: connectionLink)
|
||||||
|
switch connectionPlan {
|
||||||
|
case let .invitationLink(ilp):
|
||||||
|
switch ilp {
|
||||||
|
case .ok:
|
||||||
|
logger.debug("planAndConnect, .invitationLink, .ok, incognito=\(incognito?.description ?? "nil")")
|
||||||
|
if let incognito = incognito {
|
||||||
|
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito)
|
||||||
|
} else {
|
||||||
|
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link"))
|
||||||
|
}
|
||||||
|
case .ownLink:
|
||||||
|
logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
|
||||||
|
if let incognito = incognito {
|
||||||
|
showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||||
|
} else {
|
||||||
|
showActionSheet(.ownLinkAskCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
|
||||||
|
}
|
||||||
|
case let .connecting(contact_):
|
||||||
|
logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")")
|
||||||
|
if let contact = contact_ {
|
||||||
|
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
|
||||||
|
} else {
|
||||||
|
showAlert(.invitationLinkConnecting(connectionLink: connectionLink))
|
||||||
|
}
|
||||||
|
case let .known(contact):
|
||||||
|
logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")")
|
||||||
|
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
|
||||||
|
}
|
||||||
|
case let .contactAddress(cap):
|
||||||
|
switch cap {
|
||||||
|
case .ok:
|
||||||
|
logger.debug("planAndConnect, .contactAddress, .ok, incognito=\(incognito?.description ?? "nil")")
|
||||||
|
if let incognito = incognito {
|
||||||
|
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito)
|
||||||
|
} else {
|
||||||
|
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address"))
|
||||||
|
}
|
||||||
|
case .ownLink:
|
||||||
|
logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")")
|
||||||
|
if let incognito = incognito {
|
||||||
|
showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||||
|
} else {
|
||||||
|
showActionSheet(.ownLinkAskCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
|
||||||
|
}
|
||||||
|
case let .connecting(contact):
|
||||||
|
logger.debug("planAndConnect, .contactAddress, .connecting, incognito=\(incognito?.description ?? "nil")")
|
||||||
|
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
|
||||||
|
case let .known(contact):
|
||||||
|
logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")")
|
||||||
|
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
|
||||||
|
}
|
||||||
|
case let .groupLink(glp):
|
||||||
|
switch glp {
|
||||||
|
case .ok:
|
||||||
|
if let incognito = incognito {
|
||||||
|
showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
|
||||||
|
} else {
|
||||||
|
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group"))
|
||||||
|
}
|
||||||
|
case let .ownLink(groupInfo):
|
||||||
|
logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
|
||||||
|
showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo))
|
||||||
|
case let .connecting(groupInfo_):
|
||||||
|
logger.debug("planAndConnect, .groupLink, .connecting, incognito=\(incognito?.description ?? "nil")")
|
||||||
|
showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_))
|
||||||
|
case let .known(groupInfo):
|
||||||
|
logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")")
|
||||||
|
openKnownGroup(groupInfo, dismiss: dismiss) { AlertManager.shared.showAlert(groupAlreadyExistsAlert(groupInfo)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logger.debug("planAndConnect, plan error")
|
||||||
|
if let incognito = incognito {
|
||||||
|
connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito)
|
||||||
|
} else {
|
||||||
|
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CReqClientData: Decodable {
|
private func connectViaLink(_ connectionLink: String, connectionPlan: ConnectionPlan?, dismiss: Bool, incognito: Bool) {
|
||||||
var type: String
|
Task {
|
||||||
var groupLinkId: String?
|
if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) {
|
||||||
}
|
let crt: ConnReqType
|
||||||
|
if let plan = connectionPlan {
|
||||||
func parseLinkQueryData(_ connectionLink: String) -> CReqClientData? {
|
crt = planToConnReqType(plan)
|
||||||
if let hashIndex = connectionLink.firstIndex(of: "#"),
|
} else {
|
||||||
let urlQuery = URL(string: String(connectionLink[connectionLink.index(after: hashIndex)...])),
|
crt = connReqType
|
||||||
let components = URLComponents(url: urlQuery, resolvingAgainstBaseURL: false),
|
}
|
||||||
let data = components.queryItems?.first(where: { $0.name == "data" })?.value,
|
DispatchQueue.main.async {
|
||||||
let d = data.data(using: .utf8),
|
if dismiss {
|
||||||
let crData = try? getJSONDecoder().decode(CReqClientData.self, from: d) {
|
dismissAllSheets(animated: true) {
|
||||||
return crData
|
AlertManager.shared.showAlert(connReqSentAlert(crt))
|
||||||
} else {
|
}
|
||||||
return nil
|
} else {
|
||||||
|
AlertManager.shared.showAlert(connReqSentAlert(crt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if dismiss {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
dismissAllSheets(animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkCRDataGroup(_ crData: CReqClientData) -> Bool {
|
func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
|
||||||
return crData.type == "group" && crData.groupLinkId != nil
|
Task {
|
||||||
|
let m = ChatModel.shared
|
||||||
|
if let c = m.getContactChat(contact.contactId) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if dismiss {
|
||||||
|
dismissAllSheets(animated: true) {
|
||||||
|
m.chatId = c.id
|
||||||
|
showAlreadyExistsAlert?()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.chatId = c.id
|
||||||
|
showAlreadyExistsAlert?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func groupLinkAlert(_ connectionLink: String, incognito: Bool) -> Alert {
|
func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
|
||||||
return Alert(
|
Task {
|
||||||
title: Text("Connect via group link?"),
|
let m = ChatModel.shared
|
||||||
message: Text("You will join a group this link refers to and connect to its group members."),
|
if let g = m.getGroupChat(groupInfo.groupId) {
|
||||||
primaryButton: .default(Text(incognito ? "Connect incognito" : "Connect")) {
|
DispatchQueue.main.async {
|
||||||
connectViaLink(connectionLink, incognito: incognito)
|
if dismiss {
|
||||||
},
|
dismissAllSheets(animated: true) {
|
||||||
secondaryButton: .cancel()
|
m.chatId = g.id
|
||||||
|
showAlreadyExistsAlert?()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.chatId = g.id
|
||||||
|
showAlreadyExistsAlert?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert {
|
||||||
|
mkAlert(
|
||||||
|
title: "Contact already exists",
|
||||||
|
message: "You are already connecting to \(contact.displayName)."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert {
|
||||||
|
mkAlert(
|
||||||
|
title: "Group already exists",
|
||||||
|
message: "You are already in group \(groupInfo.displayName)."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConnReqType: Equatable {
|
||||||
|
case invitation
|
||||||
|
case contact
|
||||||
|
case groupLink
|
||||||
|
|
||||||
|
var connReqSentText: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .invitation: return "You will be connected when your contact's device is online, please wait or check later!"
|
||||||
|
case .contact: return "You will be connected when your connection request is accepted, please wait or check later!"
|
||||||
|
case .groupLink: return "You will be connected when group link host's device is online, please wait or check later!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType {
|
||||||
|
switch connectionPlan {
|
||||||
|
case .invitationLink: return .invitation
|
||||||
|
case .contactAddress: return .contact
|
||||||
|
case .groupLink: return .groupLink
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func connReqSentAlert(_ type: ConnReqType) -> Alert {
|
func connReqSentAlert(_ type: ConnReqType) -> Alert {
|
||||||
return mkAlert(
|
return mkAlert(
|
||||||
title: "Connection request sent!",
|
title: "Connection request sent!",
|
||||||
message: type == .contact
|
message: type.connReqSentText
|
||||||
? "You will be connected when your connection request is accepted, please wait or check later!"
|
|
||||||
: "You will be connected when your contact's device is online, please wait or check later!"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ struct PasteToConnectView: View {
|
|||||||
@State private var connectionLink: String = ""
|
@State private var connectionLink: String = ""
|
||||||
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
||||||
@FocusState private var linkEditorFocused: Bool
|
@FocusState private var linkEditorFocused: Bool
|
||||||
|
@State private var alert: PlanAndConnectAlert?
|
||||||
|
@State private var sheet: PlanAndConnectActionSheet?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
@ -57,6 +59,8 @@ struct PasteToConnectView: View {
|
|||||||
+ Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.")
|
+ Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }
|
||||||
|
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func linkEditor() -> some View {
|
private func linkEditor() -> some View {
|
||||||
@ -83,13 +87,13 @@ struct PasteToConnectView: View {
|
|||||||
|
|
||||||
private func connect() {
|
private func connect() {
|
||||||
let link = connectionLink.trimmingCharacters(in: .whitespaces)
|
let link = connectionLink.trimmingCharacters(in: .whitespaces)
|
||||||
if let crData = parseLinkQueryData(link),
|
planAndConnect(
|
||||||
checkCRDataGroup(crData) {
|
link,
|
||||||
dismiss()
|
showAlert: { alert = $0 },
|
||||||
AlertManager.shared.showAlert(groupLinkAlert(link, incognito: incognitoDefault))
|
showActionSheet: { sheet = $0 },
|
||||||
} else {
|
dismiss: true,
|
||||||
connectViaLink(link, dismiss: dismiss, incognito: incognitoDefault)
|
incognito: incognitoDefault
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ import CodeScanner
|
|||||||
struct ScanToConnectView: View {
|
struct ScanToConnectView: View {
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
|
||||||
|
@State private var alert: PlanAndConnectAlert?
|
||||||
|
@State private var sheet: PlanAndConnectActionSheet?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@ -49,18 +51,20 @@ struct ScanToConnectView: View {
|
|||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
}
|
}
|
||||||
.background(Color(.systemGroupedBackground))
|
.background(Color(.systemGroupedBackground))
|
||||||
|
.alert(item: $alert) { a in planAndConnectAlert(a, dismiss: true) }
|
||||||
|
.actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||||
switch resp {
|
switch resp {
|
||||||
case let .success(r):
|
case let .success(r):
|
||||||
if let crData = parseLinkQueryData(r.string),
|
planAndConnect(
|
||||||
checkCRDataGroup(crData) {
|
r.string,
|
||||||
dismiss()
|
showAlert: { alert = $0 },
|
||||||
AlertManager.shared.showAlert(groupLinkAlert(r.string, incognito: incognitoDefault))
|
showActionSheet: { sheet = $0 },
|
||||||
} else {
|
dismiss: true,
|
||||||
Task { connectViaLink(r.string, dismiss: dismiss, incognito: incognitoDefault) }
|
incognito: incognitoDefault
|
||||||
}
|
)
|
||||||
case let .failure(e):
|
case let .failure(e):
|
||||||
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
|
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
|
||||||
dismiss()
|
dismiss()
|
||||||
|
@ -176,6 +176,11 @@
|
|||||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
|
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
|
||||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
||||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
||||||
|
64AB9C832AD6B6B900B21C4C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C7E2AD6B6B900B21C4C /* libgmp.a */; };
|
||||||
|
64AB9C842AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C7F2AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */; };
|
||||||
|
64AB9C852AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C802AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */; };
|
||||||
|
64AB9C862AD6B6B900B21C4C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C812AD6B6B900B21C4C /* libffi.a */; };
|
||||||
|
64AB9C872AD6B6B900B21C4C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C822AD6B6B900B21C4C /* libgmpxx.a */; };
|
||||||
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
|
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
|
||||||
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
|
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
|
||||||
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
|
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
|
||||||
@ -458,6 +463,11 @@
|
|||||||
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
|
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
|
||||||
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
||||||
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
||||||
|
64AB9C7E2AD6B6B900B21C4C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||||
|
64AB9C7F2AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||||
|
64AB9C802AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a"; sourceTree = "<group>"; };
|
||||||
|
64AB9C812AD6B6B900B21C4C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||||
|
64AB9C822AD6B6B900B21C4C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
|
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
|
||||||
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
|
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
|
||||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
||||||
@ -507,13 +517,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5CA8D0162AD746C8001FD661 /* libgmpxx.a in Frameworks */,
|
64AB9C842AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a in Frameworks */,
|
||||||
5CA8D01A2AD746C8001FD661 /* libgmp.a in Frameworks */,
|
64AB9C862AD6B6B900B21C4C /* libffi.a in Frameworks */,
|
||||||
5CA8D0182AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a in Frameworks */,
|
64AB9C852AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a in Frameworks */,
|
||||||
5CA8D0192AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a in Frameworks */,
|
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
|
64AB9C832AD6B6B900B21C4C /* libgmp.a in Frameworks */,
|
||||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||||
5CA8D0172AD746C8001FD661 /* libffi.a in Frameworks */,
|
64AB9C872AD6B6B900B21C4C /* libgmpxx.a in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -574,11 +584,11 @@
|
|||||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5CA8D0122AD746C8001FD661 /* libffi.a */,
|
64AB9C812AD6B6B900B21C4C /* libffi.a */,
|
||||||
5CA8D0152AD746C8001FD661 /* libgmp.a */,
|
64AB9C7E2AD6B6B900B21C4C /* libgmp.a */,
|
||||||
5CA8D0112AD746C8001FD661 /* libgmpxx.a */,
|
64AB9C822AD6B6B900B21C4C /* libgmpxx.a */,
|
||||||
5CA8D0142AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */,
|
64AB9C7F2AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */,
|
||||||
5CA8D0132AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */,
|
64AB9C802AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */,
|
||||||
);
|
);
|
||||||
path = Libraries;
|
path = Libraries;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -86,6 +86,7 @@ public enum ChatCommand {
|
|||||||
case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?)
|
case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?)
|
||||||
case apiAddContact(userId: Int64, incognito: Bool)
|
case apiAddContact(userId: Int64, incognito: Bool)
|
||||||
case apiSetConnectionIncognito(connId: Int64, incognito: Bool)
|
case apiSetConnectionIncognito(connId: Int64, incognito: Bool)
|
||||||
|
case apiConnectPlan(userId: Int64, connReq: String)
|
||||||
case apiConnect(userId: Int64, incognito: Bool, connReq: String)
|
case apiConnect(userId: Int64, incognito: Bool, connReq: String)
|
||||||
case apiDeleteChat(type: ChatType, id: Int64)
|
case apiDeleteChat(type: ChatType, id: Int64)
|
||||||
case apiClearChat(type: ChatType, id: Int64)
|
case apiClearChat(type: ChatType, id: Int64)
|
||||||
@ -110,6 +111,7 @@ public enum ChatCommand {
|
|||||||
case apiEndCall(contact: Contact)
|
case apiEndCall(contact: Contact)
|
||||||
case apiGetCallInvitations
|
case apiGetCallInvitations
|
||||||
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
|
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
|
||||||
|
case apiGetNetworkStatuses
|
||||||
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
||||||
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
||||||
case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?)
|
case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?)
|
||||||
@ -218,6 +220,7 @@ public enum ChatCommand {
|
|||||||
case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)"
|
case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)"
|
||||||
case let .apiAddContact(userId, incognito): return "/_connect \(userId) 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 .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))"
|
||||||
|
case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)"
|
||||||
case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)"
|
case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)"
|
||||||
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
|
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
|
||||||
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
|
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
|
||||||
@ -241,6 +244,7 @@ public enum ChatCommand {
|
|||||||
case let .apiEndCall(contact): return "/_call end @\(contact.apiId)"
|
case let .apiEndCall(contact): return "/_call end @\(contact.apiId)"
|
||||||
case .apiGetCallInvitations: return "/_call get"
|
case .apiGetCallInvitations: return "/_call get"
|
||||||
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
|
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
|
||||||
|
case .apiGetNetworkStatuses: return "/_network_statuses"
|
||||||
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
|
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
|
||||||
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
|
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
|
||||||
case let .receiveFile(fileId, encrypted, inline):
|
case let .receiveFile(fileId, encrypted, inline):
|
||||||
@ -333,6 +337,7 @@ public enum ChatCommand {
|
|||||||
case .apiVerifyGroupMember: return "apiVerifyGroupMember"
|
case .apiVerifyGroupMember: return "apiVerifyGroupMember"
|
||||||
case .apiAddContact: return "apiAddContact"
|
case .apiAddContact: return "apiAddContact"
|
||||||
case .apiSetConnectionIncognito: return "apiSetConnectionIncognito"
|
case .apiSetConnectionIncognito: return "apiSetConnectionIncognito"
|
||||||
|
case .apiConnectPlan: return "apiConnectPlan"
|
||||||
case .apiConnect: return "apiConnect"
|
case .apiConnect: return "apiConnect"
|
||||||
case .apiDeleteChat: return "apiDeleteChat"
|
case .apiDeleteChat: return "apiDeleteChat"
|
||||||
case .apiClearChat: return "apiClearChat"
|
case .apiClearChat: return "apiClearChat"
|
||||||
@ -356,6 +361,7 @@ public enum ChatCommand {
|
|||||||
case .apiEndCall: return "apiEndCall"
|
case .apiEndCall: return "apiEndCall"
|
||||||
case .apiGetCallInvitations: return "apiGetCallInvitations"
|
case .apiGetCallInvitations: return "apiGetCallInvitations"
|
||||||
case .apiCallStatus: return "apiCallStatus"
|
case .apiCallStatus: return "apiCallStatus"
|
||||||
|
case .apiGetNetworkStatuses: return "apiGetNetworkStatuses"
|
||||||
case .apiChatRead: return "apiChatRead"
|
case .apiChatRead: return "apiChatRead"
|
||||||
case .apiChatUnread: return "apiChatUnread"
|
case .apiChatUnread: return "apiChatUnread"
|
||||||
case .receiveFile: return "receiveFile"
|
case .receiveFile: return "receiveFile"
|
||||||
@ -457,6 +463,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case connectionVerified(user: UserRef, verified: Bool, expectedCode: String)
|
case connectionVerified(user: UserRef, verified: Bool, expectedCode: String)
|
||||||
case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection)
|
case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection)
|
||||||
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
|
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
|
||||||
|
case connectionPlan(user: UserRef, connectionPlan: ConnectionPlan)
|
||||||
case sentConfirmation(user: UserRef)
|
case sentConfirmation(user: UserRef)
|
||||||
case sentInvitation(user: UserRef)
|
case sentInvitation(user: UserRef)
|
||||||
case contactAlreadyExists(user: UserRef, contact: Contact)
|
case contactAlreadyExists(user: UserRef, contact: Contact)
|
||||||
@ -480,11 +487,14 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case acceptingContactRequest(user: UserRef, contact: Contact)
|
case acceptingContactRequest(user: UserRef, contact: Contact)
|
||||||
case contactRequestRejected(user: UserRef)
|
case contactRequestRejected(user: UserRef)
|
||||||
case contactUpdated(user: UserRef, toContact: Contact)
|
case contactUpdated(user: UserRef, toContact: Contact)
|
||||||
|
// TODO remove events below
|
||||||
case contactsSubscribed(server: String, contactRefs: [ContactRef])
|
case contactsSubscribed(server: String, contactRefs: [ContactRef])
|
||||||
case contactsDisconnected(server: String, contactRefs: [ContactRef])
|
case contactsDisconnected(server: String, contactRefs: [ContactRef])
|
||||||
case contactSubError(user: UserRef, contact: Contact, chatError: ChatError)
|
|
||||||
case contactSubSummary(user: UserRef, contactSubscriptions: [ContactSubStatus])
|
case contactSubSummary(user: UserRef, contactSubscriptions: [ContactSubStatus])
|
||||||
case groupSubscribed(user: UserRef, groupInfo: GroupInfo)
|
// TODO remove events above
|
||||||
|
case networkStatus(networkStatus: NetworkStatus, connections: [String])
|
||||||
|
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
|
||||||
|
case groupSubscribed(user: UserRef, groupInfo: GroupRef)
|
||||||
case memberSubErrors(user: UserRef, memberSubErrors: [MemberSubError])
|
case memberSubErrors(user: UserRef, memberSubErrors: [MemberSubError])
|
||||||
case groupEmpty(user: UserRef, groupInfo: GroupInfo)
|
case groupEmpty(user: UserRef, groupInfo: GroupInfo)
|
||||||
case userContactLinkSubscribed
|
case userContactLinkSubscribed
|
||||||
@ -595,6 +605,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case .connectionVerified: return "connectionVerified"
|
case .connectionVerified: return "connectionVerified"
|
||||||
case .invitation: return "invitation"
|
case .invitation: return "invitation"
|
||||||
case .connectionIncognitoUpdated: return "connectionIncognitoUpdated"
|
case .connectionIncognitoUpdated: return "connectionIncognitoUpdated"
|
||||||
|
case .connectionPlan: return "connectionPlan"
|
||||||
case .sentConfirmation: return "sentConfirmation"
|
case .sentConfirmation: return "sentConfirmation"
|
||||||
case .sentInvitation: return "sentInvitation"
|
case .sentInvitation: return "sentInvitation"
|
||||||
case .contactAlreadyExists: return "contactAlreadyExists"
|
case .contactAlreadyExists: return "contactAlreadyExists"
|
||||||
@ -620,8 +631,9 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case .contactUpdated: return "contactUpdated"
|
case .contactUpdated: return "contactUpdated"
|
||||||
case .contactsSubscribed: return "contactsSubscribed"
|
case .contactsSubscribed: return "contactsSubscribed"
|
||||||
case .contactsDisconnected: return "contactsDisconnected"
|
case .contactsDisconnected: return "contactsDisconnected"
|
||||||
case .contactSubError: return "contactSubError"
|
|
||||||
case .contactSubSummary: return "contactSubSummary"
|
case .contactSubSummary: return "contactSubSummary"
|
||||||
|
case .networkStatus: return "networkStatus"
|
||||||
|
case .networkStatuses: return "networkStatuses"
|
||||||
case .groupSubscribed: return "groupSubscribed"
|
case .groupSubscribed: return "groupSubscribed"
|
||||||
case .memberSubErrors: return "memberSubErrors"
|
case .memberSubErrors: return "memberSubErrors"
|
||||||
case .groupEmpty: return "groupEmpty"
|
case .groupEmpty: return "groupEmpty"
|
||||||
@ -732,6 +744,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
|
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
|
||||||
case let .invitation(u, connReqInvitation, _): return withUser(u, connReqInvitation)
|
case let .invitation(u, connReqInvitation, _): return withUser(u, connReqInvitation)
|
||||||
case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
|
case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
|
||||||
|
case let .connectionPlan(u, connectionPlan): return withUser(u, String(describing: connectionPlan))
|
||||||
case .sentConfirmation: return noDetails
|
case .sentConfirmation: return noDetails
|
||||||
case .sentInvitation: return noDetails
|
case .sentInvitation: return noDetails
|
||||||
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
|
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
|
||||||
@ -757,8 +770,9 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
|
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
|
||||||
case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
|
case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
|
||||||
case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
|
case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
|
||||||
case let .contactSubError(u, contact, chatError): return withUser(u, "contact:\n\(String(describing: contact))\nerror:\n\(String(describing: chatError))")
|
|
||||||
case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions))
|
case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions))
|
||||||
|
case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))"
|
||||||
|
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
|
||||||
case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||||
case let .memberSubErrors(u, memberSubErrors): return withUser(u, String(describing: memberSubErrors))
|
case let .memberSubErrors(u, memberSubErrors): return withUser(u, String(describing: memberSubErrors))
|
||||||
case let .groupEmpty(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
case let .groupEmpty(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||||
@ -851,6 +865,33 @@ public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ConnectionPlan: Decodable {
|
||||||
|
case invitationLink(invitationLinkPlan: InvitationLinkPlan)
|
||||||
|
case contactAddress(contactAddressPlan: ContactAddressPlan)
|
||||||
|
case groupLink(groupLinkPlan: GroupLinkPlan)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum InvitationLinkPlan: Decodable {
|
||||||
|
case ok
|
||||||
|
case ownLink
|
||||||
|
case connecting(contact_: Contact?)
|
||||||
|
case known(contact: Contact)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ContactAddressPlan: Decodable {
|
||||||
|
case ok
|
||||||
|
case ownLink
|
||||||
|
case connecting(contact: Contact)
|
||||||
|
case known(contact: Contact)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum GroupLinkPlan: Decodable {
|
||||||
|
case ok
|
||||||
|
case ownLink(groupInfo: GroupInfo)
|
||||||
|
case connecting(groupInfo_: GroupInfo?)
|
||||||
|
case known(groupInfo: GroupInfo)
|
||||||
|
}
|
||||||
|
|
||||||
struct NewUser: Encodable {
|
struct NewUser: Encodable {
|
||||||
var profile: Profile?
|
var profile: Profile?
|
||||||
var sameServers: Bool
|
var sameServers: Bool
|
||||||
@ -1181,6 +1222,49 @@ public struct KeepAliveOpts: Codable, Equatable {
|
|||||||
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
|
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum NetworkStatus: Decodable, Equatable {
|
||||||
|
case unknown
|
||||||
|
case connected
|
||||||
|
case disconnected
|
||||||
|
case error(connectionError: String)
|
||||||
|
|
||||||
|
public var statusString: LocalizedStringKey {
|
||||||
|
get {
|
||||||
|
switch self {
|
||||||
|
case .connected: return "connected"
|
||||||
|
case .error: return "error"
|
||||||
|
default: return "connecting"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var statusExplanation: LocalizedStringKey {
|
||||||
|
get {
|
||||||
|
switch self {
|
||||||
|
case .connected: return "You are connected to the server used to receive messages from this contact."
|
||||||
|
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
|
||||||
|
default: return "Trying to connect to the server used to receive messages from this contact."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var imageName: String {
|
||||||
|
get {
|
||||||
|
switch self {
|
||||||
|
case .unknown: return "circle.dotted"
|
||||||
|
case .connected: return "circle.fill"
|
||||||
|
case .disconnected: return "ellipsis.circle.fill"
|
||||||
|
case .error: return "exclamationmark.circle.fill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ConnNetworkStatus: Decodable {
|
||||||
|
public var agentConnId: String
|
||||||
|
public var networkStatus: NetworkStatus
|
||||||
|
}
|
||||||
|
|
||||||
public struct ChatSettings: Codable {
|
public struct ChatSettings: Codable {
|
||||||
public var enableNtfs: MsgFilter
|
public var enableNtfs: MsgFilter
|
||||||
public var sendRcpts: Bool?
|
public var sendRcpts: Bool?
|
||||||
@ -1426,6 +1510,7 @@ public enum ChatErrorType: Decodable {
|
|||||||
case chatNotStarted
|
case chatNotStarted
|
||||||
case chatNotStopped
|
case chatNotStopped
|
||||||
case chatStoreChanged
|
case chatStoreChanged
|
||||||
|
case connectionPlan(connectionPlan: ConnectionPlan)
|
||||||
case invalidConnReq
|
case invalidConnReq
|
||||||
case invalidChatMessage(connection: Connection, message: String)
|
case invalidChatMessage(connection: Connection, message: String)
|
||||||
case contactNotReady(contact: Contact)
|
case contactNotReady(contact: Contact)
|
||||||
|
@ -1729,6 +1729,11 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct GroupRef: Decodable {
|
||||||
|
public var groupId: Int64
|
||||||
|
var localDisplayName: GroupName
|
||||||
|
}
|
||||||
|
|
||||||
public struct GroupProfile: Codable, NamedChat {
|
public struct GroupProfile: Codable, NamedChat {
|
||||||
public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) {
|
public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) {
|
||||||
self.displayName = displayName
|
self.displayName = displayName
|
||||||
@ -1871,6 +1876,11 @@ public struct GroupMemberRef: Decodable {
|
|||||||
var profile: Profile
|
var profile: Profile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct GroupMemberIds: Decodable {
|
||||||
|
var groupMemberId: Int64
|
||||||
|
var groupId: Int64
|
||||||
|
}
|
||||||
|
|
||||||
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
|
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
|
||||||
case observer = "observer"
|
case observer = "observer"
|
||||||
case member = "member"
|
case member = "member"
|
||||||
@ -1963,7 +1973,7 @@ public enum InvitedBy: Decodable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public struct MemberSubError: Decodable {
|
public struct MemberSubError: Decodable {
|
||||||
var member: GroupMember
|
var member: GroupMemberIds
|
||||||
var memberError: ChatError
|
var memberError: ChatError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,12 +9,14 @@ import chat.simplex.common.helpers.APPLICATION_ID
|
|||||||
import chat.simplex.common.helpers.requiresIgnoringBattery
|
import chat.simplex.common.helpers.requiresIgnoringBattery
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.model.ChatController.appPrefs
|
import chat.simplex.common.model.ChatController.appPrefs
|
||||||
|
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.views.call.RcvCallInvitation
|
import chat.simplex.common.views.call.RcvCallInvitation
|
||||||
import com.jakewharton.processphoenix.ProcessPhoenix
|
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -52,21 +54,23 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
Lifecycle.Event.ON_START -> {
|
Lifecycle.Event.ON_START -> {
|
||||||
isAppOnForeground = true
|
isAppOnForeground = true
|
||||||
if (chatModel.chatRunning.value == true) {
|
if (chatModel.chatRunning.value == true) {
|
||||||
kotlin.runCatching {
|
updatingChatsMutex.withLock {
|
||||||
val currentUserId = chatModel.currentUser.value?.userId
|
kotlin.runCatching {
|
||||||
val chats = ArrayList(chatController.apiGetChats())
|
val currentUserId = chatModel.currentUser.value?.userId
|
||||||
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
|
val chats = ArrayList(chatController.apiGetChats())
|
||||||
if (chatModel.currentUser.value?.userId == currentUserId) {
|
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
|
||||||
val currentChatId = chatModel.chatId.value
|
if (chatModel.currentUser.value?.userId == currentUserId) {
|
||||||
val oldStats = if (currentChatId != null) chatModel.getChat(currentChatId)?.chatStats else null
|
val currentChatId = chatModel.chatId.value
|
||||||
if (oldStats != null) {
|
val oldStats = if (currentChatId != null) chatModel.getChat(currentChatId)?.chatStats else null
|
||||||
val indexOfCurrentChat = chats.indexOfFirst { it.id == currentChatId }
|
if (oldStats != null) {
|
||||||
/** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */
|
val indexOfCurrentChat = chats.indexOfFirst { it.id == currentChatId }
|
||||||
if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats)
|
/** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */
|
||||||
|
if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats)
|
||||||
|
}
|
||||||
|
chatModel.updateChats(chats)
|
||||||
}
|
}
|
||||||
chatModel.updateChats(chats)
|
}.onFailure { Log.e(TAG, it.stackTraceToString()) }
|
||||||
}
|
}
|
||||||
}.onFailure { Log.e(TAG, it.stackTraceToString()) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Lifecycle.Event.ON_RESUME -> {
|
Lifecycle.Event.ON_RESUME -> {
|
||||||
|
@ -6,7 +6,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.font.*
|
import androidx.compose.ui.text.font.*
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import chat.simplex.common.model.*
|
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.call.*
|
import chat.simplex.common.views.call.*
|
||||||
@ -16,6 +15,8 @@ import chat.simplex.res.MR
|
|||||||
import dev.icerock.moko.resources.ImageResource
|
import dev.icerock.moko.resources.ImageResource
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.datetime.*
|
import kotlinx.datetime.*
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
@ -102,6 +103,8 @@ object ChatModel {
|
|||||||
val filesToDelete = mutableSetOf<File>()
|
val filesToDelete = mutableSetOf<File>()
|
||||||
val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) }
|
val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) }
|
||||||
|
|
||||||
|
var updatingChatsMutex: Mutex = Mutex()
|
||||||
|
|
||||||
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
|
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
|
||||||
currentUser.value
|
currentUser.value
|
||||||
} else {
|
} else {
|
||||||
@ -198,7 +201,7 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) {
|
suspend fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock {
|
||||||
// update previews
|
// update previews
|
||||||
val i = getChatIndex(cInfo.id)
|
val i = getChatIndex(cInfo.id)
|
||||||
val chat: Chat
|
val chat: Chat
|
||||||
@ -221,10 +224,11 @@ object ChatModel {
|
|||||||
} else {
|
} else {
|
||||||
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||||
}
|
}
|
||||||
// add to current chat
|
Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
if (chatId.value == cInfo.id) {
|
withContext(Dispatchers.Main) {
|
||||||
Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
// add to current chat
|
||||||
withContext(Dispatchers.Main) {
|
if (chatId.value == cInfo.id) {
|
||||||
|
Log.d(TAG, "TODOCHAT: addChatItem: chatIds are equal, size ${chatItems.size}")
|
||||||
// Prevent situation when chat item already in the list received from backend
|
// Prevent situation when chat item already in the list received from backend
|
||||||
if (chatItems.none { it.id == cItem.id }) {
|
if (chatItems.none { it.id == cItem.id }) {
|
||||||
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
@ -238,7 +242,7 @@ object ChatModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean {
|
suspend fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock {
|
||||||
// update previews
|
// update previews
|
||||||
val i = getChatIndex(cInfo.id)
|
val i = getChatIndex(cInfo.id)
|
||||||
val chat: Chat
|
val chat: Chat
|
||||||
@ -258,10 +262,10 @@ object ChatModel {
|
|||||||
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
|
||||||
res = true
|
res = true
|
||||||
}
|
}
|
||||||
// update current chat
|
Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
return if (chatId.value == cInfo.id) {
|
return withContext(Dispatchers.Main) {
|
||||||
Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
// update current chat
|
||||||
withContext(Dispatchers.Main) {
|
if (chatId.value == cInfo.id) {
|
||||||
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||||
if (itemIndex >= 0) {
|
if (itemIndex >= 0) {
|
||||||
chatItems[itemIndex] = cItem
|
chatItems[itemIndex] = cItem
|
||||||
@ -272,15 +276,15 @@ object ChatModel {
|
|||||||
Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
res
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
res
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem) {
|
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem) {
|
||||||
if (chatId.value == cInfo.id) {
|
withContext(Dispatchers.Main) {
|
||||||
withContext(Dispatchers.Main) {
|
if (chatId.value == cInfo.id) {
|
||||||
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||||
if (itemIndex >= 0) {
|
if (itemIndex >= 0) {
|
||||||
chatItems[itemIndex] = cItem
|
chatItems[itemIndex] = cItem
|
||||||
@ -785,16 +789,19 @@ sealed class NetworkStatus {
|
|||||||
val statusExplanation: String get() =
|
val statusExplanation: String get() =
|
||||||
when (this) {
|
when (this) {
|
||||||
is Connected -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact)
|
is Connected -> generalGetString(MR.strings.connected_to_server_to_receive_messages_from_contact)
|
||||||
is Error -> String.format(generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages_with_error), error)
|
is Error -> String.format(generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages_with_error), connectionError)
|
||||||
else -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages)
|
else -> generalGetString(MR.strings.trying_to_connect_to_server_to_receive_messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
|
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
|
||||||
@Serializable @SerialName("connected") class Connected: NetworkStatus()
|
@Serializable @SerialName("connected") class Connected: NetworkStatus()
|
||||||
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
|
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
|
||||||
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
|
@Serializable @SerialName("error") class Error(val connectionError: String): NetworkStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ConnNetworkStatus(val agentConnId: String, val networkStatus: NetworkStatus)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Contact(
|
data class Contact(
|
||||||
val contactId: Long,
|
val contactId: Long,
|
||||||
@ -1047,6 +1054,9 @@ data class GroupInfo (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GroupRef(val groupId: Long, val localDisplayName: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class GroupProfile (
|
data class GroupProfile (
|
||||||
override val displayName: String,
|
override val displayName: String,
|
||||||
@ -1155,11 +1165,17 @@ data class GroupMember (
|
|||||||
data class GroupMemberSettings(val showMessages: Boolean) {}
|
data class GroupMemberSettings(val showMessages: Boolean) {}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class GroupMemberRef(
|
data class GroupMemberRef(
|
||||||
val groupMemberId: Long,
|
val groupMemberId: Long,
|
||||||
val profile: Profile
|
val profile: Profile
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GroupMemberIds(
|
||||||
|
val groupMemberId: Long,
|
||||||
|
val groupId: Long
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class GroupMemberRole(val memberRole: String) {
|
enum class GroupMemberRole(val memberRole: String) {
|
||||||
@SerialName("observer") Observer("observer"), // order matters in comparisons
|
@SerialName("observer") Observer("observer"), // order matters in comparisons
|
||||||
@ -1253,7 +1269,7 @@ class LinkPreview (
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class MemberSubError (
|
class MemberSubError (
|
||||||
val member: GroupMember,
|
val member: GroupMemberIds,
|
||||||
val memberError: ChatError
|
val memberError: ChatError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import chat.simplex.common.views.helpers.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
@ -16,6 +17,7 @@ import com.charleskorn.kaml.YamlConfiguration
|
|||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import com.russhwolf.settings.Settings
|
import com.russhwolf.settings.Settings
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.datetime.Clock
|
import kotlinx.datetime.Clock
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
@ -349,8 +351,10 @@ object ChatController {
|
|||||||
startReceiver()
|
startReceiver()
|
||||||
Log.d(TAG, "startChat: started")
|
Log.d(TAG, "startChat: started")
|
||||||
} else {
|
} else {
|
||||||
val chats = apiGetChats()
|
updatingChatsMutex.withLock {
|
||||||
chatModel.updateChats(chats)
|
val chats = apiGetChats()
|
||||||
|
chatModel.updateChats(chats)
|
||||||
|
}
|
||||||
Log.d(TAG, "startChat: running")
|
Log.d(TAG, "startChat: running")
|
||||||
}
|
}
|
||||||
} catch (e: Error) {
|
} catch (e: Error) {
|
||||||
@ -384,8 +388,10 @@ object ChatController {
|
|||||||
suspend fun getUserChatData() {
|
suspend fun getUserChatData() {
|
||||||
chatModel.userAddress.value = apiGetUserAddress()
|
chatModel.userAddress.value = apiGetUserAddress()
|
||||||
chatModel.chatItemTTL.value = getChatItemTTL()
|
chatModel.chatItemTTL.value = getChatItemTTL()
|
||||||
val chats = apiGetChats()
|
updatingChatsMutex.withLock {
|
||||||
chatModel.updateChats(chats)
|
val chats = apiGetChats()
|
||||||
|
chatModel.updateChats(chats)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startReceiver() {
|
private fun startReceiver() {
|
||||||
@ -1076,6 +1082,13 @@ object ChatController {
|
|||||||
return r is CR.CmdOk
|
return r is CR.CmdOk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun apiGetNetworkStatuses(): List<ConnNetworkStatus>? {
|
||||||
|
val r = sendCmd(CC.ApiGetNetworkStatuses())
|
||||||
|
if (r is CR.NetworkStatuses) return r.networkStatuses
|
||||||
|
Log.e(TAG, "apiGetNetworkStatuses bad response: ${r.responseType} ${r.details}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun apiChatRead(type: ChatType, id: Long, range: CC.ItemRange): Boolean {
|
suspend fun apiChatRead(type: ChatType, id: Long, range: CC.ItemRange): Boolean {
|
||||||
val r = sendCmd(CC.ApiChatRead(type, id, range))
|
val r = sendCmd(CC.ApiChatRead(type, id, range))
|
||||||
if (r is CR.CmdOk) return true
|
if (r is CR.CmdOk) return true
|
||||||
@ -1419,12 +1432,6 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected())
|
is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected())
|
||||||
is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected())
|
is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected())
|
||||||
is CR.ContactSubError -> {
|
|
||||||
if (active(r.user)) {
|
|
||||||
chatModel.updateContact(r.contact)
|
|
||||||
}
|
|
||||||
processContactSubError(r.contact, r.chatError)
|
|
||||||
}
|
|
||||||
is CR.ContactSubSummary -> {
|
is CR.ContactSubSummary -> {
|
||||||
for (sub in r.contactSubscriptions) {
|
for (sub in r.contactSubscriptions) {
|
||||||
if (active(r.user)) {
|
if (active(r.user)) {
|
||||||
@ -1438,6 +1445,16 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is CR.NetworkStatusResp -> {
|
||||||
|
for (cId in r.connections) {
|
||||||
|
chatModel.networkStatuses[cId] = r.networkStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CR.NetworkStatuses -> {
|
||||||
|
for (s in r.networkStatuses) {
|
||||||
|
chatModel.networkStatuses[s.agentConnId] = s.networkStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
is CR.NewChatItem -> {
|
is CR.NewChatItem -> {
|
||||||
val cInfo = r.chatItem.chatInfo
|
val cInfo = r.chatItem.chatInfo
|
||||||
val cItem = r.chatItem.chatItem
|
val cItem = r.chatItem.chatItem
|
||||||
@ -1909,6 +1926,7 @@ sealed class CC {
|
|||||||
class ApiSendCallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CC()
|
class ApiSendCallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CC()
|
||||||
class ApiEndCall(val contact: Contact): CC()
|
class ApiEndCall(val contact: Contact): CC()
|
||||||
class ApiCallStatus(val contact: Contact, val callStatus: WebRTCCallStatus): CC()
|
class ApiCallStatus(val contact: Contact, val callStatus: WebRTCCallStatus): CC()
|
||||||
|
class ApiGetNetworkStatuses(): CC()
|
||||||
class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC()
|
class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC()
|
||||||
class ApiRejectContact(val contactReqId: Long): CC()
|
class ApiRejectContact(val contactReqId: Long): CC()
|
||||||
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
|
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
|
||||||
@ -2030,6 +2048,7 @@ sealed class CC {
|
|||||||
is ApiSendCallExtraInfo -> "/_call extra @${contact.apiId} ${json.encodeToString(extraInfo)}"
|
is ApiSendCallExtraInfo -> "/_call extra @${contact.apiId} ${json.encodeToString(extraInfo)}"
|
||||||
is ApiEndCall -> "/_call end @${contact.apiId}"
|
is ApiEndCall -> "/_call end @${contact.apiId}"
|
||||||
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
|
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
|
||||||
|
is ApiGetNetworkStatuses -> "/_network_statuses"
|
||||||
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
|
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
|
||||||
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
||||||
is ReceiveFile -> "/freceive $fileId encrypt=${onOff(encrypted)}" + (if (inline == null) "" else " inline=${onOff(inline)}")
|
is ReceiveFile -> "/freceive $fileId encrypt=${onOff(encrypted)}" + (if (inline == null) "" else " inline=${onOff(inline)}")
|
||||||
@ -2138,6 +2157,7 @@ sealed class CC {
|
|||||||
is ApiSendCallExtraInfo -> "apiSendCallExtraInfo"
|
is ApiSendCallExtraInfo -> "apiSendCallExtraInfo"
|
||||||
is ApiEndCall -> "apiEndCall"
|
is ApiEndCall -> "apiEndCall"
|
||||||
is ApiCallStatus -> "apiCallStatus"
|
is ApiCallStatus -> "apiCallStatus"
|
||||||
|
is ApiGetNetworkStatuses -> "apiGetNetworkStatuses"
|
||||||
is ApiChatRead -> "apiChatRead"
|
is ApiChatRead -> "apiChatRead"
|
||||||
is ApiChatUnread -> "apiChatUnread"
|
is ApiChatUnread -> "apiChatUnread"
|
||||||
is ReceiveFile -> "receiveFile"
|
is ReceiveFile -> "receiveFile"
|
||||||
@ -3391,11 +3411,14 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: UserRef, 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): CR()
|
||||||
@Serializable @SerialName("contactUpdated") class ContactUpdated(val user: UserRef, val toContact: Contact): CR()
|
@Serializable @SerialName("contactUpdated") class ContactUpdated(val user: UserRef, val toContact: Contact): CR()
|
||||||
|
// TODO remove below
|
||||||
@Serializable @SerialName("contactsSubscribed") class ContactsSubscribed(val server: String, val contactRefs: List<ContactRef>): CR()
|
@Serializable @SerialName("contactsSubscribed") class ContactsSubscribed(val server: String, val contactRefs: List<ContactRef>): CR()
|
||||||
@Serializable @SerialName("contactsDisconnected") class ContactsDisconnected(val server: String, val contactRefs: List<ContactRef>): CR()
|
@Serializable @SerialName("contactsDisconnected") class ContactsDisconnected(val server: String, val contactRefs: List<ContactRef>): CR()
|
||||||
@Serializable @SerialName("contactSubError") class ContactSubError(val user: UserRef, val contact: Contact, val chatError: ChatError): CR()
|
|
||||||
@Serializable @SerialName("contactSubSummary") class ContactSubSummary(val user: UserRef, val contactSubscriptions: List<ContactSubStatus>): CR()
|
@Serializable @SerialName("contactSubSummary") class ContactSubSummary(val user: UserRef, val contactSubscriptions: List<ContactSubStatus>): CR()
|
||||||
@Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: UserRef, val group: GroupInfo): CR()
|
// TODO remove above
|
||||||
|
@Serializable @SerialName("networkStatus") class NetworkStatusResp(val networkStatus: NetworkStatus, val connections: List<String>): CR()
|
||||||
|
@Serializable @SerialName("networkStatuses") class NetworkStatuses(val user_: UserRef?, val networkStatuses: List<ConnNetworkStatus>): CR()
|
||||||
|
@Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: UserRef, val group: GroupRef): CR()
|
||||||
@Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List<MemberSubError>): CR()
|
@Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List<MemberSubError>): CR()
|
||||||
@Serializable @SerialName("groupEmpty") class GroupEmpty(val user: UserRef, val group: GroupInfo): CR()
|
@Serializable @SerialName("groupEmpty") class GroupEmpty(val user: UserRef, val group: GroupInfo): CR()
|
||||||
@Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR()
|
@Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR()
|
||||||
@ -3545,8 +3568,9 @@ sealed class CR {
|
|||||||
is ContactUpdated -> "contactUpdated"
|
is ContactUpdated -> "contactUpdated"
|
||||||
is ContactsSubscribed -> "contactsSubscribed"
|
is ContactsSubscribed -> "contactsSubscribed"
|
||||||
is ContactsDisconnected -> "contactsDisconnected"
|
is ContactsDisconnected -> "contactsDisconnected"
|
||||||
is ContactSubError -> "contactSubError"
|
|
||||||
is ContactSubSummary -> "contactSubSummary"
|
is ContactSubSummary -> "contactSubSummary"
|
||||||
|
is NetworkStatusResp -> "networkStatus"
|
||||||
|
is NetworkStatuses -> "networkStatuses"
|
||||||
is GroupSubscribed -> "groupSubscribed"
|
is GroupSubscribed -> "groupSubscribed"
|
||||||
is MemberSubErrors -> "memberSubErrors"
|
is MemberSubErrors -> "memberSubErrors"
|
||||||
is GroupEmpty -> "groupEmpty"
|
is GroupEmpty -> "groupEmpty"
|
||||||
@ -3691,8 +3715,9 @@ sealed class CR {
|
|||||||
is ContactUpdated -> withUser(user, json.encodeToString(toContact))
|
is ContactUpdated -> withUser(user, json.encodeToString(toContact))
|
||||||
is ContactsSubscribed -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}"
|
is ContactsSubscribed -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}"
|
||||||
is ContactsDisconnected -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}"
|
is ContactsDisconnected -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}"
|
||||||
is ContactSubError -> withUser(user, "error:\n${chatError.string}\ncontact:\n${json.encodeToString(contact)}")
|
|
||||||
is ContactSubSummary -> withUser(user, json.encodeToString(contactSubscriptions))
|
is ContactSubSummary -> withUser(user, json.encodeToString(contactSubscriptions))
|
||||||
|
is NetworkStatusResp -> "networkStatus $networkStatus\nconnections: $connections"
|
||||||
|
is NetworkStatuses -> withUser(user_, json.encodeToString(networkStatuses))
|
||||||
is GroupSubscribed -> withUser(user, json.encodeToString(group))
|
is GroupSubscribed -> withUser(user, json.encodeToString(group))
|
||||||
is MemberSubErrors -> withUser(user, json.encodeToString(memberSubErrors))
|
is MemberSubErrors -> withUser(user, json.encodeToString(memberSubErrors))
|
||||||
is GroupEmpty -> withUser(user, json.encodeToString(group))
|
is GroupEmpty -> withUser(user, json.encodeToString(group))
|
||||||
|
@ -20,11 +20,13 @@ import androidx.compose.ui.text.*
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
|
import chat.simplex.common.model.ChatModel.updatingChatsMutex
|
||||||
import chat.simplex.common.ui.theme.*
|
import chat.simplex.common.ui.theme.*
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.common.views.usersettings.*
|
import chat.simplex.common.views.usersettings.*
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.datetime.*
|
import kotlinx.datetime.*
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@ -620,8 +622,10 @@ private fun afterSetCiTTL(
|
|||||||
appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath)
|
appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath)
|
||||||
withApi {
|
withApi {
|
||||||
try {
|
try {
|
||||||
val chats = m.controller.apiGetChats()
|
updatingChatsMutex.withLock {
|
||||||
m.updateChats(chats)
|
val chats = m.controller.apiGetChats()
|
||||||
|
m.updateChats(chats)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "apiGetChats error: ${e.message}")
|
Log.e(TAG, "apiGetChats error: ${e.message}")
|
||||||
}
|
}
|
||||||
|
34
docs/rfcs/2023-10-12-desktop-calls.md
Normal file
34
docs/rfcs/2023-10-12-desktop-calls.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Desktop calls
|
||||||
|
|
||||||
|
To make audio and video calls on desktop there are some options:
|
||||||
|
- adapt [libwebrtc](webrtc.googlesource.com/) from Google which would be the most reliable, performant and seamless solution;
|
||||||
|
- include some kind of WebView to the app via libraries;
|
||||||
|
- implement a signaling server in the app and let users to use HTML page with WebRTC code that already exist for Android.
|
||||||
|
|
||||||
|
## WebRTC lib
|
||||||
|
|
||||||
|
To adapt libwebrtc we need to make SDK that is compatible with Java (JNI layer + desktop implementation of VideoCodecs and other features). There are two SDKs exist already: for Android and for Objective-C. Making Android SDK compatible with Java-only SDK gives a lot of problems and requires to have 5+ professional C++ developers with weeks/months for developing. Which is not something good.
|
||||||
|
|
||||||
|
Another considered option was adopting Jitsi Java SDK, but it's state is not very clear, and in any case it is much more effort.
|
||||||
|
|
||||||
|
## WebView
|
||||||
|
|
||||||
|
Including WebView is possible but requires a lot of megabytes of storage to waste on such libs. Because only Chromium-like WebViews support WebRTC features. Which means 100+ MB to the package on top of 200+ MB now.
|
||||||
|
|
||||||
|
## Standalone browser + WebRTC HTML page
|
||||||
|
|
||||||
|
The last solution is what can give the most useful result: the same package size as before + already existent code which can be reused with small modifications + quality of result will depend on Chromium/Firefox/Safari devs (which is good, since they are interested in making all features working for everyone).
|
||||||
|
|
||||||
|
# Details of implementation
|
||||||
|
|
||||||
|
Since the code for WebView has already written (https://github.com/simplex-chat/simplex-chat/tree/0e4376bada2d0c4ec2ade7f30b0048dc8b13abd8/apps/multiplatform/android/src/main/assets/www) it can be used to make calls on a desktop browser too. The only differences are these:
|
||||||
|
- UI needs to be changed - buttons controlling calls should be added. For example, end call, disable camera/mic. This will be added to separate HTML and JS files that would communication with the existing WebRTC JS code via the existing functional API.
|
||||||
|
- signaling websocket server should be started in order to allow Haskell backend to talk with HTML page and to exchange messages between all parties. It is bidirectional communication between server and webpage since both parties have to send messages to each other.
|
||||||
|
|
||||||
|
There is no need to have TLS-secured connection between server and webpage since it's only used locally. And browser will not be happy with self-signed certificate too.
|
||||||
|
|
||||||
|
After accepting the call, webpage will be opened in the default browser. URL will look like: `https://localhost:123/simplex/call/` (to reduce questions and to namespace other files - the main UI page will be index.html, that would load via folder path) After that internal machinary will connect both parties together. Same as on Android but with a different signaling channel, in this case it's websockets.
|
||||||
|
|
||||||
|
Ending the call by the user will also send a new action to signaling server to allow the backend to notify other party.
|
||||||
|
|
||||||
|
The solution will also allow to make screen sharing in the future that will be supported on every OS and graphics environment where the user's browser supports it.
|
@ -145,7 +145,8 @@ defaultChatConfig =
|
|||||||
initialCleanupManagerDelay = 30 * 1000000, -- 30 seconds
|
initialCleanupManagerDelay = 30 * 1000000, -- 30 seconds
|
||||||
cleanupManagerInterval = 30 * 60, -- 30 minutes
|
cleanupManagerInterval = 30 * 60, -- 30 minutes
|
||||||
cleanupManagerStepDelay = 3 * 1000000, -- 3 seconds
|
cleanupManagerStepDelay = 3 * 1000000, -- 3 seconds
|
||||||
ciExpirationInterval = 30 * 60 * 1000000 -- 30 minutes
|
ciExpirationInterval = 30 * 60 * 1000000, -- 30 minutes
|
||||||
|
coreApi = False
|
||||||
}
|
}
|
||||||
|
|
||||||
_defaultSMPServers :: NonEmpty SMPServerWithAuth
|
_defaultSMPServers :: NonEmpty SMPServerWithAuth
|
||||||
@ -198,6 +199,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
|
|||||||
idsDrg <- newTVarIO =<< liftIO drgNew
|
idsDrg <- newTVarIO =<< liftIO drgNew
|
||||||
inputQ <- newTBQueueIO tbqSize
|
inputQ <- newTBQueueIO tbqSize
|
||||||
outputQ <- newTBQueueIO tbqSize
|
outputQ <- newTBQueueIO tbqSize
|
||||||
|
connNetworkStatuses <- atomically TM.empty
|
||||||
subscriptionMode <- newTVarIO SMSubscribe
|
subscriptionMode <- newTVarIO SMSubscribe
|
||||||
chatLock <- newEmptyTMVarIO
|
chatLock <- newEmptyTMVarIO
|
||||||
sndFiles <- newTVarIO M.empty
|
sndFiles <- newTVarIO M.empty
|
||||||
@ -228,6 +230,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
|
|||||||
idsDrg,
|
idsDrg,
|
||||||
inputQ,
|
inputQ,
|
||||||
outputQ,
|
outputQ,
|
||||||
|
connNetworkStatuses,
|
||||||
subscriptionMode,
|
subscriptionMode,
|
||||||
chatLock,
|
chatLock,
|
||||||
sndFiles,
|
sndFiles,
|
||||||
@ -1103,6 +1106,8 @@ processChatCommand = \case
|
|||||||
user <- getUserByContactId db contactId
|
user <- getUserByContactId db contactId
|
||||||
contact <- getContact db user contactId
|
contact <- getContact db user contactId
|
||||||
pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callTs}
|
pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callTs}
|
||||||
|
APIGetNetworkStatuses -> withUser $ \_ ->
|
||||||
|
CRNetworkStatuses Nothing . map (uncurry ConnNetworkStatus) . M.toList <$> chatReadVar connNetworkStatuses
|
||||||
APICallStatus contactId receivedStatus ->
|
APICallStatus contactId receivedStatus ->
|
||||||
withCurrentCall contactId $ \user ct call ->
|
withCurrentCall contactId $ \user ct call ->
|
||||||
updateCallItemStatus user ct call receivedStatus Nothing $> Just call
|
updateCallItemStatus user ct call receivedStatus Nothing $> Just call
|
||||||
@ -1705,6 +1710,8 @@ processChatCommand = \case
|
|||||||
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode
|
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode
|
||||||
-- [incognito] reuse membership incognito profile
|
-- [incognito] reuse membership incognito profile
|
||||||
ct <- withStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode
|
ct <- withStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode
|
||||||
|
-- TODO not sure it is correct to set connections status here?
|
||||||
|
setContactNetworkStatus ct NSConnected
|
||||||
pure $ CRNewMemberContact user ct g m
|
pure $ CRNewMemberContact user ct g m
|
||||||
_ -> throwChatError CEGroupMemberNotActive
|
_ -> throwChatError CEGroupMemberNotActive
|
||||||
APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do
|
APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do
|
||||||
@ -2656,6 +2663,7 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do
|
|||||||
rs <- withAgent $ \a -> agentBatchSubscribe a conns
|
rs <- withAgent $ \a -> agentBatchSubscribe a conns
|
||||||
-- send connection events to view
|
-- send connection events to view
|
||||||
contactSubsToView rs cts ce
|
contactSubsToView rs cts ce
|
||||||
|
-- TODO possibly, we could either disable these events or replace with less noisy for API
|
||||||
contactLinkSubsToView rs ucs
|
contactLinkSubsToView rs ucs
|
||||||
groupSubsToView rs gs ms ce
|
groupSubsToView rs gs ms ce
|
||||||
sndFileSubsToView rs sfts
|
sndFileSubsToView rs sfts
|
||||||
@ -2716,12 +2724,30 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do
|
|||||||
let connIds = map aConnId' pcs
|
let connIds = map aConnId' pcs
|
||||||
pure (connIds, M.fromList $ zip connIds pcs)
|
pure (connIds, M.fromList $ zip connIds pcs)
|
||||||
contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> m ()
|
contactSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId Contact -> Bool -> m ()
|
||||||
contactSubsToView rs cts ce = do
|
contactSubsToView rs cts ce = ifM (asks $ coreApi . config) notifyAPI notifyCLI
|
||||||
toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs
|
|
||||||
when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors
|
|
||||||
where
|
where
|
||||||
cRs = resultsFor rs cts
|
notifyCLI = do
|
||||||
cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs
|
let cRs = resultsFor rs cts
|
||||||
|
cErrors = sortOn (\(Contact {localDisplayName = n}, _) -> n) $ filterErrors cRs
|
||||||
|
toView . CRContactSubSummary user $ map (uncurry ContactSubStatus) cRs
|
||||||
|
when ce $ mapM_ (toView . uncurry (CRContactSubError user)) cErrors
|
||||||
|
notifyAPI = do
|
||||||
|
let statuses = M.foldrWithKey' addStatus [] cts
|
||||||
|
chatModifyVar connNetworkStatuses $ M.union (M.fromList statuses)
|
||||||
|
toView $ CRNetworkStatuses (Just user) $ map (uncurry ConnNetworkStatus) statuses
|
||||||
|
where
|
||||||
|
addStatus :: ConnId -> Contact -> [(AgentConnId, NetworkStatus)] -> [(AgentConnId, NetworkStatus)]
|
||||||
|
addStatus connId ct =
|
||||||
|
let ns = (contactAgentConnId ct, netStatus $ resultErr connId rs)
|
||||||
|
in (ns :)
|
||||||
|
netStatus :: Maybe ChatError -> NetworkStatus
|
||||||
|
netStatus = maybe NSConnected $ NSError . errorNetworkStatus
|
||||||
|
errorNetworkStatus :: ChatError -> String
|
||||||
|
errorNetworkStatus = \case
|
||||||
|
ChatErrorAgent (BROKER _ NETWORK) _ -> "network"
|
||||||
|
ChatErrorAgent (SMP SMP.AUTH) _ -> "contact deleted"
|
||||||
|
e -> show e
|
||||||
|
-- TODO possibly below could be replaced with less noisy events for API
|
||||||
contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> m ()
|
contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> m ()
|
||||||
contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs
|
contactLinkSubsToView rs = toView . CRUserContactSubSummary user . map (uncurry UserContactSubStatus) . resultsFor rs
|
||||||
groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [Group] -> Map ConnId GroupMember -> Bool -> m ()
|
groupSubsToView :: Map ConnId (Either AgentErrorType ()) -> [Group] -> Map ConnId GroupMember -> Bool -> m ()
|
||||||
@ -2771,12 +2797,12 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do
|
|||||||
resultsFor rs = M.foldrWithKey' addResult []
|
resultsFor rs = M.foldrWithKey' addResult []
|
||||||
where
|
where
|
||||||
addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)]
|
addResult :: ConnId -> a -> [(a, Maybe ChatError)] -> [(a, Maybe ChatError)]
|
||||||
addResult connId = (:) . (,err)
|
addResult connId = (:) . (,resultErr connId rs)
|
||||||
where
|
resultErr :: ConnId -> Map ConnId (Either AgentErrorType ()) -> Maybe ChatError
|
||||||
err = case M.lookup connId rs of
|
resultErr connId rs = case M.lookup connId rs of
|
||||||
Just (Left e) -> Just $ ChatErrorAgent e Nothing
|
Just (Left e) -> Just $ ChatErrorAgent e Nothing
|
||||||
Just _ -> Nothing
|
Just _ -> Nothing
|
||||||
_ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
|
_ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
|
||||||
|
|
||||||
cleanupManager :: forall m. ChatMonad m => m ()
|
cleanupManager :: forall m. ChatMonad m => m ()
|
||||||
cleanupManager = do
|
cleanupManager = do
|
||||||
@ -2921,16 +2947,22 @@ processAgentMessageNoConn :: forall m. ChatMonad m => ACommand 'Agent 'AENone ->
|
|||||||
processAgentMessageNoConn = \case
|
processAgentMessageNoConn = \case
|
||||||
CONNECT p h -> hostEvent $ CRHostConnected p h
|
CONNECT p h -> hostEvent $ CRHostConnected p h
|
||||||
DISCONNECT p h -> hostEvent $ CRHostDisconnected p h
|
DISCONNECT p h -> hostEvent $ CRHostDisconnected p h
|
||||||
DOWN srv conns -> serverEvent srv conns CRContactsDisconnected
|
DOWN srv conns -> serverEvent srv conns NSDisconnected CRContactsDisconnected
|
||||||
UP srv conns -> serverEvent srv conns CRContactsSubscribed
|
UP srv conns -> serverEvent srv conns NSConnected CRContactsSubscribed
|
||||||
SUSPENDED -> toView CRChatSuspended
|
SUSPENDED -> toView CRChatSuspended
|
||||||
DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId
|
DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId
|
||||||
where
|
where
|
||||||
hostEvent :: ChatResponse -> m ()
|
hostEvent :: ChatResponse -> m ()
|
||||||
hostEvent = whenM (asks $ hostEvents . config) . toView
|
hostEvent = whenM (asks $ hostEvents . config) . toView
|
||||||
serverEvent srv conns event = do
|
serverEvent srv conns nsStatus event = ifM (asks $ coreApi . config) notifyAPI notifyCLI
|
||||||
cs <- withStore' (`getConnectionsContacts` conns)
|
where
|
||||||
toView $ event srv cs
|
notifyAPI = do
|
||||||
|
let connIds = map AgentConnId conns
|
||||||
|
chatModifyVar connNetworkStatuses $ \m -> foldl' (\m' cId -> M.insert cId nsStatus m') m connIds
|
||||||
|
toView $ CRNetworkStatus nsStatus connIds
|
||||||
|
notifyCLI = do
|
||||||
|
cs <- withStore' (`getConnectionsContacts` conns)
|
||||||
|
toView $ event srv cs
|
||||||
|
|
||||||
processAgentMsgSndFile :: forall m. ChatMonad m => ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> m ()
|
processAgentMsgSndFile :: forall m. ChatMonad m => ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> m ()
|
||||||
processAgentMsgSndFile _corrId aFileId msg =
|
processAgentMsgSndFile _corrId aFileId msg =
|
||||||
@ -3217,6 +3249,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||||||
Nothing -> do
|
Nothing -> do
|
||||||
-- [incognito] print incognito profile used for this contact
|
-- [incognito] print incognito profile used for this contact
|
||||||
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
|
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
|
||||||
|
setContactNetworkStatus ct NSConnected
|
||||||
toView $ CRContactConnected user ct (fmap fromLocalProfile incognitoProfile)
|
toView $ CRContactConnected user ct (fmap fromLocalProfile incognitoProfile)
|
||||||
when (directOrUsed ct) $ createFeatureEnabledItems ct
|
when (directOrUsed ct) $ createFeatureEnabledItems ct
|
||||||
when (contactConnInitiated conn) $ do
|
when (contactConnInitiated conn) $ do
|
||||||
@ -3791,6 +3824,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||||||
notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> m ()
|
notifyMemberConnected :: GroupInfo -> GroupMember -> Maybe Contact -> m ()
|
||||||
notifyMemberConnected gInfo m ct_ = do
|
notifyMemberConnected gInfo m ct_ = do
|
||||||
memberConnectedChatItem gInfo m
|
memberConnectedChatItem gInfo m
|
||||||
|
mapM_ (`setContactNetworkStatus` NSConnected) ct_
|
||||||
toView $ CRConnectedToGroupMember user gInfo m ct_
|
toView $ CRConnectedToGroupMember user gInfo m ct_
|
||||||
|
|
||||||
probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> m ()
|
probeMatchingContactsAndMembers :: Contact -> IncognitoEnabled -> Bool -> m ()
|
||||||
@ -5598,6 +5632,7 @@ chatCommandP =
|
|||||||
"/_call end @" *> (APIEndCall <$> A.decimal),
|
"/_call end @" *> (APIEndCall <$> A.decimal),
|
||||||
"/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP),
|
"/_call status @" *> (APICallStatus <$> A.decimal <* A.space <*> strP),
|
||||||
"/_call get" $> APIGetCallInvitations,
|
"/_call get" $> APIGetCallInvitations,
|
||||||
|
"/_network_statuses" $> APIGetNetworkStatuses,
|
||||||
"/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP),
|
"/_profile " *> (APIUpdateProfile <$> A.decimal <* A.space <*> jsonP),
|
||||||
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
|
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
|
||||||
"/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
|
"/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
|
||||||
|
@ -36,6 +36,7 @@ import Data.Constraint (Dict (..))
|
|||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.List.NonEmpty (NonEmpty)
|
import Data.List.NonEmpty (NonEmpty)
|
||||||
import Data.Map.Strict (Map)
|
import Data.Map.Strict (Map)
|
||||||
|
import qualified Data.Map.Strict as M
|
||||||
import Data.String
|
import Data.String
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Time (NominalDiffTime, UTCTime)
|
import Data.Time (NominalDiffTime, UTCTime)
|
||||||
@ -130,7 +131,8 @@ data ChatConfig = ChatConfig
|
|||||||
initialCleanupManagerDelay :: Int64,
|
initialCleanupManagerDelay :: Int64,
|
||||||
cleanupManagerInterval :: NominalDiffTime,
|
cleanupManagerInterval :: NominalDiffTime,
|
||||||
cleanupManagerStepDelay :: Int64,
|
cleanupManagerStepDelay :: Int64,
|
||||||
ciExpirationInterval :: Int64 -- microseconds
|
ciExpirationInterval :: Int64, -- microseconds
|
||||||
|
coreApi :: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
data DefaultAgentServers = DefaultAgentServers
|
data DefaultAgentServers = DefaultAgentServers
|
||||||
@ -171,6 +173,7 @@ data ChatController = ChatController
|
|||||||
idsDrg :: TVar ChaChaDRG,
|
idsDrg :: TVar ChaChaDRG,
|
||||||
inputQ :: TBQueue String,
|
inputQ :: TBQueue String,
|
||||||
outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse),
|
outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse),
|
||||||
|
connNetworkStatuses :: TMap AgentConnId NetworkStatus,
|
||||||
subscriptionMode :: TVar SubscriptionMode,
|
subscriptionMode :: TVar SubscriptionMode,
|
||||||
chatLock :: Lock,
|
chatLock :: Lock,
|
||||||
sndFiles :: TVar (Map Int64 Handle),
|
sndFiles :: TVar (Map Int64 Handle),
|
||||||
@ -263,6 +266,7 @@ data ChatCommand
|
|||||||
| APIEndCall ContactId
|
| APIEndCall ContactId
|
||||||
| APIGetCallInvitations
|
| APIGetCallInvitations
|
||||||
| APICallStatus ContactId WebRTCCallStatus
|
| APICallStatus ContactId WebRTCCallStatus
|
||||||
|
| APIGetNetworkStatuses
|
||||||
| APIUpdateProfile UserId Profile
|
| APIUpdateProfile UserId Profile
|
||||||
| APISetContactPrefs ContactId Preferences
|
| APISetContactPrefs ContactId Preferences
|
||||||
| APISetContactAlias ContactId LocalAlias
|
| APISetContactAlias ContactId LocalAlias
|
||||||
@ -576,6 +580,8 @@ data ChatResponse
|
|||||||
| CRContactSubError {user :: User, contact :: Contact, chatError :: ChatError}
|
| CRContactSubError {user :: User, contact :: Contact, chatError :: ChatError}
|
||||||
| CRContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]}
|
| CRContactSubSummary {user :: User, contactSubscriptions :: [ContactSubStatus]}
|
||||||
| CRUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]}
|
| CRUserContactSubSummary {user :: User, userContactSubscriptions :: [UserContactSubStatus]}
|
||||||
|
| CRNetworkStatus {networkStatus :: NetworkStatus, connections :: [AgentConnId]}
|
||||||
|
| CRNetworkStatuses {user_ :: Maybe User, networkStatuses :: [ConnNetworkStatus]}
|
||||||
| CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
|
| CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost}
|
||||||
| CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost}
|
| CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost}
|
||||||
| CRGroupInvitation {user :: User, groupInfo :: GroupInfo}
|
| CRGroupInvitation {user :: User, groupInfo :: GroupInfo}
|
||||||
@ -1254,6 +1260,9 @@ chatModifyVar :: ChatMonad' m => (ChatController -> TVar a) -> (a -> a) -> m ()
|
|||||||
chatModifyVar f newValue = asks f >>= atomically . (`modifyTVar'` newValue)
|
chatModifyVar f newValue = asks f >>= atomically . (`modifyTVar'` newValue)
|
||||||
{-# INLINE chatModifyVar #-}
|
{-# INLINE chatModifyVar #-}
|
||||||
|
|
||||||
|
setContactNetworkStatus :: ChatMonad' m => Contact -> NetworkStatus -> m ()
|
||||||
|
setContactNetworkStatus ct = chatModifyVar connNetworkStatuses . M.insert (contactAgentConnId ct)
|
||||||
|
|
||||||
tryChatError :: ChatMonad m => m a -> m (Either ChatError a)
|
tryChatError :: ChatMonad m => m a -> m (Either ChatError a)
|
||||||
tryChatError = tryAllErrors mkChatError
|
tryChatError = tryAllErrors mkChatError
|
||||||
{-# INLINE tryChatError #-}
|
{-# INLINE tryChatError #-}
|
||||||
|
@ -182,7 +182,8 @@ defaultMobileConfig :: ChatConfig
|
|||||||
defaultMobileConfig =
|
defaultMobileConfig =
|
||||||
defaultChatConfig
|
defaultChatConfig
|
||||||
{ confirmMigrations = MCYesUp,
|
{ confirmMigrations = MCYesUp,
|
||||||
logLevel = CLLError
|
logLevel = CLLError,
|
||||||
|
coreApi = True
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveUser_ :: SQLiteStore -> IO (Maybe User)
|
getActiveUser_ :: SQLiteStore -> IO (Maybe User)
|
||||||
|
@ -180,6 +180,9 @@ data Contact = Contact
|
|||||||
contactConn :: Contact -> Connection
|
contactConn :: Contact -> Connection
|
||||||
contactConn Contact {activeConn} = activeConn
|
contactConn Contact {activeConn} = activeConn
|
||||||
|
|
||||||
|
contactAgentConnId :: Contact -> AgentConnId
|
||||||
|
contactAgentConnId Contact {activeConn = Connection {agentConnId}} = agentConnId
|
||||||
|
|
||||||
contactConnId :: Contact -> ConnId
|
contactConnId :: Contact -> ConnId
|
||||||
contactConnId = aConnId . contactConn
|
contactConnId = aConnId . contactConn
|
||||||
|
|
||||||
@ -1139,7 +1142,7 @@ liveRcvFileTransferPath ft = fp <$> liveRcvFileTransferInfo ft
|
|||||||
fp RcvFileInfo {filePath} = filePath
|
fp RcvFileInfo {filePath} = filePath
|
||||||
|
|
||||||
newtype AgentConnId = AgentConnId ConnId
|
newtype AgentConnId = AgentConnId ConnId
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Ord, Show)
|
||||||
|
|
||||||
instance StrEncoding AgentConnId where
|
instance StrEncoding AgentConnId where
|
||||||
strEncode (AgentConnId connId) = strEncode connId
|
strEncode (AgentConnId connId) = strEncode connId
|
||||||
@ -1501,6 +1504,35 @@ serializeIntroStatus = \case
|
|||||||
textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a
|
textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a
|
||||||
textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode
|
textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode
|
||||||
|
|
||||||
|
data NetworkStatus
|
||||||
|
= NSUnknown
|
||||||
|
| NSConnected
|
||||||
|
| NSDisconnected
|
||||||
|
| NSError {connectionError :: String}
|
||||||
|
deriving (Eq, Ord, Show, Generic)
|
||||||
|
|
||||||
|
netStatusStr :: NetworkStatus -> String
|
||||||
|
netStatusStr = \case
|
||||||
|
NSUnknown -> "unknown"
|
||||||
|
NSConnected -> "connected"
|
||||||
|
NSDisconnected -> "disconnected"
|
||||||
|
NSError e -> "error: " <> e
|
||||||
|
|
||||||
|
instance FromJSON NetworkStatus where
|
||||||
|
parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "NS"
|
||||||
|
|
||||||
|
instance ToJSON NetworkStatus where
|
||||||
|
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "NS"
|
||||||
|
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "NS"
|
||||||
|
|
||||||
|
data ConnNetworkStatus = ConnNetworkStatus
|
||||||
|
{ agentConnId :: AgentConnId,
|
||||||
|
networkStatus :: NetworkStatus
|
||||||
|
}
|
||||||
|
deriving (Show, Generic, FromJSON)
|
||||||
|
|
||||||
|
instance ToJSON ConnNetworkStatus where toEncoding = J.genericToEncoding J.defaultOptions
|
||||||
|
|
||||||
type CommandId = Int64
|
type CommandId = Int64
|
||||||
|
|
||||||
aCorrId :: CommandId -> ACorrId
|
aCorrId :: CommandId -> ACorrId
|
||||||
|
@ -19,7 +19,7 @@ import Data.Char (isSpace, toUpper)
|
|||||||
import Data.Function (on)
|
import Data.Function (on)
|
||||||
import Data.Int (Int64)
|
import Data.Int (Int64)
|
||||||
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
|
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
|
||||||
import Data.List.NonEmpty (NonEmpty)
|
import Data.List.NonEmpty (NonEmpty (..))
|
||||||
import qualified Data.List.NonEmpty as L
|
import qualified Data.List.NonEmpty as L
|
||||||
import Data.Map.Strict (Map)
|
import Data.Map.Strict (Map)
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
@ -211,6 +211,8 @@ responseToView (currentRH, user_) ChatConfig {logLevel, showReactions, showRecei
|
|||||||
(addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary
|
(addresses, groupLinks) = partition (\UserContactSubStatus {userContact} -> isNothing . userContactGroupId $ userContact) summary
|
||||||
addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError
|
addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError
|
||||||
(groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks
|
(groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks
|
||||||
|
CRNetworkStatus status conns -> if testView then [plain $ show (length conns) <> " connections " <> netStatusStr status] else []
|
||||||
|
CRNetworkStatuses u statuses -> if testView then ttyUser' u $ viewNetworkStatuses statuses else []
|
||||||
CRGroupInvitation u g -> ttyUser u [groupInvitation' g]
|
CRGroupInvitation u g -> ttyUser u [groupInvitation' g]
|
||||||
CRReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r
|
CRReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r
|
||||||
CRUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g
|
CRUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g
|
||||||
@ -820,6 +822,12 @@ viewDirectMessagesProhibited :: MsgDirection -> Contact -> [StyledString]
|
|||||||
viewDirectMessagesProhibited MDSnd c = ["direct messages to indirect contact " <> ttyContact' c <> " are prohibited"]
|
viewDirectMessagesProhibited MDSnd c = ["direct messages to indirect contact " <> ttyContact' c <> " are prohibited"]
|
||||||
viewDirectMessagesProhibited MDRcv c = ["received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"]
|
viewDirectMessagesProhibited MDRcv c = ["received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"]
|
||||||
|
|
||||||
|
viewNetworkStatuses :: [ConnNetworkStatus] -> [StyledString]
|
||||||
|
viewNetworkStatuses = map viewStatuses . L.groupBy ((==) `on` netStatus) . sortOn netStatus
|
||||||
|
where
|
||||||
|
netStatus ConnNetworkStatus {networkStatus} = networkStatus
|
||||||
|
viewStatuses ss@(s :| _) = plain $ show (L.length ss) <> " connections " <> netStatusStr (netStatus s)
|
||||||
|
|
||||||
viewUserJoinedGroup :: GroupInfo -> [StyledString]
|
viewUserJoinedGroup :: GroupInfo -> [StyledString]
|
||||||
viewUserJoinedGroup g =
|
viewUserJoinedGroup g =
|
||||||
case incognitoMembershipProfile g of
|
case incognitoMembershipProfile g of
|
||||||
|
@ -119,6 +119,8 @@ chatDirectTests = do
|
|||||||
testReqVRange vr11 supportedChatVRange
|
testReqVRange vr11 supportedChatVRange
|
||||||
testReqVRange vr11 vr11
|
testReqVRange vr11 vr11
|
||||||
it "update peer version range on received messages" testUpdatePeerChatVRange
|
it "update peer version range on received messages" testUpdatePeerChatVRange
|
||||||
|
describe "network statuses" $ do
|
||||||
|
it "should get network statuses" testGetNetworkStatuses
|
||||||
where
|
where
|
||||||
testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2
|
testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2
|
||||||
testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2
|
testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2
|
||||||
@ -2623,6 +2625,20 @@ testUpdatePeerChatVRange tmp =
|
|||||||
where
|
where
|
||||||
cfg11 = testCfg {chatVRange = vr11} :: ChatConfig
|
cfg11 = testCfg {chatVRange = vr11} :: ChatConfig
|
||||||
|
|
||||||
|
testGetNetworkStatuses :: HasCallStack => FilePath -> IO ()
|
||||||
|
testGetNetworkStatuses tmp = do
|
||||||
|
withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do
|
||||||
|
withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do
|
||||||
|
connectUsers alice bob
|
||||||
|
alice ##> "/_network_statuses"
|
||||||
|
alice <## "1 connections connected"
|
||||||
|
withTestChatCfg tmp cfg "alice" $ \alice ->
|
||||||
|
withTestChatCfg tmp cfg "bob" $ \bob -> do
|
||||||
|
alice <## "1 connections connected"
|
||||||
|
bob <## "1 connections connected"
|
||||||
|
where
|
||||||
|
cfg = testCfg {coreApi = True}
|
||||||
|
|
||||||
vr11 :: VersionRange
|
vr11 :: VersionRange
|
||||||
vr11 = mkVersionRange 1 1
|
vr11 = mkVersionRange 1 1
|
||||||
|
|
||||||
|
@ -120,19 +120,19 @@ chatStartedSwift = "{\"resp\":{\"_owsf\":true,\"chatStarted\":{}}}"
|
|||||||
chatStartedTagged :: LB.ByteString
|
chatStartedTagged :: LB.ByteString
|
||||||
chatStartedTagged = "{\"resp\":{\"type\":\"chatStarted\"}}"
|
chatStartedTagged = "{\"resp\":{\"type\":\"chatStarted\"}}"
|
||||||
|
|
||||||
contactSubSummary :: LB.ByteString
|
networkStatuses :: LB.ByteString
|
||||||
contactSubSummary =
|
networkStatuses =
|
||||||
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
#if defined(darwin_HOST_OS) && defined(swiftJSON)
|
||||||
contactSubSummarySwift
|
networkStatusesSwift
|
||||||
#else
|
#else
|
||||||
contactSubSummaryTagged
|
networkStatusesTagged
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
contactSubSummarySwift :: LB.ByteString
|
networkStatusesSwift :: LB.ByteString
|
||||||
contactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"contactSubSummary\":{" <> userJSON <> ",\"contactSubscriptions\":[]}}}"
|
networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}"
|
||||||
|
|
||||||
contactSubSummaryTagged :: LB.ByteString
|
networkStatusesTagged :: LB.ByteString
|
||||||
contactSubSummaryTagged = "{\"resp\":{\"type\":\"contactSubSummary\"," <> userJSON <> ",\"contactSubscriptions\":[]}}"
|
networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}"
|
||||||
|
|
||||||
memberSubSummary :: LB.ByteString
|
memberSubSummary :: LB.ByteString
|
||||||
memberSubSummary =
|
memberSubSummary =
|
||||||
@ -143,10 +143,10 @@ memberSubSummary =
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
memberSubSummarySwift :: LB.ByteString
|
memberSubSummarySwift :: LB.ByteString
|
||||||
memberSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"memberSubSummary\":{" <> userJSON <> ",\"memberSubscriptions\":[]}}}"
|
memberSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"memberSubSummary\":{\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}}"
|
||||||
|
|
||||||
memberSubSummaryTagged :: LB.ByteString
|
memberSubSummaryTagged :: LB.ByteString
|
||||||
memberSubSummaryTagged = "{\"resp\":{\"type\":\"memberSubSummary\"," <> userJSON <> ",\"memberSubscriptions\":[]}}"
|
memberSubSummaryTagged = "{\"resp\":{\"type\":\"memberSubSummary\",\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}"
|
||||||
|
|
||||||
userContactSubSummary :: LB.ByteString
|
userContactSubSummary :: LB.ByteString
|
||||||
userContactSubSummary =
|
userContactSubSummary =
|
||||||
@ -157,10 +157,10 @@ userContactSubSummary =
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
userContactSubSummarySwift :: LB.ByteString
|
userContactSubSummarySwift :: LB.ByteString
|
||||||
userContactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"userContactSubSummary\":{" <> userJSON <> ",\"userContactSubscriptions\":[]}}}"
|
userContactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"userContactSubSummary\":{\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}}"
|
||||||
|
|
||||||
userContactSubSummaryTagged :: LB.ByteString
|
userContactSubSummaryTagged :: LB.ByteString
|
||||||
userContactSubSummaryTagged = "{\"resp\":{\"type\":\"userContactSubSummary\"," <> userJSON <> ",\"userContactSubscriptions\":[]}}"
|
userContactSubSummaryTagged = "{\"resp\":{\"type\":\"userContactSubSummary\",\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}"
|
||||||
|
|
||||||
pendingSubSummary :: LB.ByteString
|
pendingSubSummary :: LB.ByteString
|
||||||
pendingSubSummary =
|
pendingSubSummary =
|
||||||
@ -171,13 +171,13 @@ pendingSubSummary =
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
pendingSubSummarySwift :: LB.ByteString
|
pendingSubSummarySwift :: LB.ByteString
|
||||||
pendingSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"pendingSubSummary\":{" <> userJSON <> ",\"pendingSubscriptions\":[]}}}"
|
pendingSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"pendingSubSummary\":{\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}}"
|
||||||
|
|
||||||
pendingSubSummaryTagged :: LB.ByteString
|
pendingSubSummaryTagged :: LB.ByteString
|
||||||
pendingSubSummaryTagged = "{\"resp\":{\"type\":\"pendingSubSummary\"," <> userJSON <> ",\"pendingSubscriptions\":[]}}"
|
pendingSubSummaryTagged = "{\"resp\":{\"type\":\"pendingSubSummary\",\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}"
|
||||||
|
|
||||||
userJSON :: LB.ByteString
|
userJSON :: LB.ByteString
|
||||||
userJSON = "\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}"
|
userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}"
|
||||||
|
|
||||||
parsedMarkdown :: LB.ByteString
|
parsedMarkdown :: LB.ByteString
|
||||||
parsedMarkdown =
|
parsedMarkdown =
|
||||||
@ -215,7 +215,7 @@ testChatApi tmp = do
|
|||||||
chatSendCmd cc "/u" `shouldReturn` activeUser
|
chatSendCmd cc "/u" `shouldReturn` activeUser
|
||||||
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists
|
chatSendCmd cc "/create user alice Alice" `shouldReturn` activeUserExists
|
||||||
chatSendCmd cc "/_start" `shouldReturn` chatStarted
|
chatSendCmd cc "/_start" `shouldReturn` chatStarted
|
||||||
chatRecvMsg cc `shouldReturn` contactSubSummary
|
chatRecvMsg cc `shouldReturn` networkStatuses
|
||||||
chatRecvMsg cc `shouldReturn` userContactSubSummary
|
chatRecvMsg cc `shouldReturn` userContactSubSummary
|
||||||
chatRecvMsg cc `shouldReturn` memberSubSummary
|
chatRecvMsg cc `shouldReturn` memberSubSummary
|
||||||
chatRecvMsgWait cc 10000 `shouldReturn` pendingSubSummary
|
chatRecvMsgWait cc 10000 `shouldReturn` pendingSubSummary
|
||||||
|
Loading…
Reference in New Issue
Block a user