Merge remote-tracking branch 'origin/master' into ab/remote-discover-upd
This commit is contained in:
commit
bf7917bd67
@ -1285,6 +1285,12 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
m.removeChat(connection.id)
|
m.removeChat(connection.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case let .contactDeletedByContact(user, contact):
|
||||||
|
if active(user) && contact.directOrUsed {
|
||||||
|
await MainActor.run {
|
||||||
|
m.updateContact(contact)
|
||||||
|
}
|
||||||
|
}
|
||||||
case let .contactConnected(user, contact, _):
|
case let .contactConnected(user, contact, _):
|
||||||
if active(user) && contact.directOrUsed {
|
if active(user) && contact.directOrUsed {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
@ -164,7 +164,7 @@ struct ChatInfoView: View {
|
|||||||
// synchronizeConnectionButtonForce()
|
// synchronizeConnectionButtonForce()
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
.disabled(!contact.ready)
|
.disabled(!contact.ready || !contact.active)
|
||||||
|
|
||||||
if let contactLink = contact.contactLink {
|
if let contactLink = contact.contactLink {
|
||||||
Section {
|
Section {
|
||||||
@ -181,7 +181,7 @@ struct ChatInfoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if contact.ready {
|
if contact.ready && contact.active {
|
||||||
Section("Servers") {
|
Section("Servers") {
|
||||||
networkStatusRow()
|
networkStatusRow()
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
@ -192,8 +192,7 @@ struct ChatInfoView: View {
|
|||||||
alert = .switchAddressAlert
|
alert = .switchAddressAlert
|
||||||
}
|
}
|
||||||
.disabled(
|
.disabled(
|
||||||
!contact.ready
|
connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|
||||||
|| connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil }
|
|
||||||
|| connStats.ratchetSyncSendProhibited
|
|| connStats.ratchetSyncSendProhibited
|
||||||
)
|
)
|
||||||
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
|
if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) {
|
||||||
|
@ -79,6 +79,7 @@ struct ChatItemContentView<Content: View>: View {
|
|||||||
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem)
|
case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem)
|
||||||
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||||
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||||
|
case .rcvDirectEvent: eventItemView()
|
||||||
case .rcvGroupEvent(.memberConnected): CIEventView(eventText: membersConnectedItemText)
|
case .rcvGroupEvent(.memberConnected): CIEventView(eventText: membersConnectedItemText)
|
||||||
case .rcvGroupEvent(.memberCreatedContact): CIMemberCreatedContactView(chatItem: chatItem)
|
case .rcvGroupEvent(.memberCreatedContact): CIMemberCreatedContactView(chatItem: chatItem)
|
||||||
case .rcvGroupEvent: eventItemView()
|
case .rcvGroupEvent: eventItemView()
|
||||||
|
@ -150,7 +150,7 @@ struct ChatView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
if contact.allowsFeature(.calls) {
|
if contact.allowsFeature(.calls) {
|
||||||
callButton(contact, .audio, imageName: "phone")
|
callButton(contact, .audio, imageName: "phone")
|
||||||
.disabled(!contact.ready)
|
.disabled(!contact.ready || !contact.active)
|
||||||
}
|
}
|
||||||
Menu {
|
Menu {
|
||||||
if contact.allowsFeature(.calls) {
|
if contact.allowsFeature(.calls) {
|
||||||
@ -159,11 +159,11 @@ struct ChatView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Label("Video call", systemImage: "video")
|
Label("Video call", systemImage: "video")
|
||||||
}
|
}
|
||||||
.disabled(!contact.ready)
|
.disabled(!contact.ready || !contact.active)
|
||||||
}
|
}
|
||||||
searchButton()
|
searchButton()
|
||||||
toggleNtfsButton(chat)
|
toggleNtfsButton(chat)
|
||||||
.disabled(!contact.ready)
|
.disabled(!contact.ready || !contact.active)
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "ellipsis")
|
Image(systemName: "ellipsis")
|
||||||
}
|
}
|
||||||
@ -321,6 +321,7 @@ struct ChatView: View {
|
|||||||
@ViewBuilder private func connectingText() -> some View {
|
@ViewBuilder private func connectingText() -> some View {
|
||||||
if case let .direct(contact) = chat.chatInfo,
|
if case let .direct(contact) = chat.chatInfo,
|
||||||
!contact.ready,
|
!contact.ready,
|
||||||
|
contact.active,
|
||||||
!contact.nextSendGrpInv {
|
!contact.nextSendGrpInv {
|
||||||
Text("connecting…")
|
Text("connecting…")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
@ -65,7 +65,7 @@ struct ChatListNavLink: View {
|
|||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
AlertManager.shared.showAlert(
|
AlertManager.shared.showAlert(
|
||||||
contact.ready
|
contact.ready || !contact.active
|
||||||
? deleteContactAlert(chat.chatInfo)
|
? deleteContactAlert(chat.chatInfo)
|
||||||
: deletePendingContactAlert(chat, contact)
|
: deletePendingContactAlert(chat, contact)
|
||||||
)
|
)
|
||||||
|
@ -57,19 +57,26 @@ struct ChatPreviewView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func chatPreviewImageOverlayIcon() -> some View {
|
@ViewBuilder private func chatPreviewImageOverlayIcon() -> some View {
|
||||||
if case let .group(groupInfo) = chat.chatInfo {
|
switch chat.chatInfo {
|
||||||
|
case let .direct(contact):
|
||||||
|
if !contact.active {
|
||||||
|
inactiveIcon()
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
case let .group(groupInfo):
|
||||||
switch (groupInfo.membership.memberStatus) {
|
switch (groupInfo.membership.memberStatus) {
|
||||||
case .memLeft: groupInactiveIcon()
|
case .memLeft: inactiveIcon()
|
||||||
case .memRemoved: groupInactiveIcon()
|
case .memRemoved: inactiveIcon()
|
||||||
case .memGroupDeleted: groupInactiveIcon()
|
case .memGroupDeleted: inactiveIcon()
|
||||||
default: EmptyView()
|
default: EmptyView()
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func groupInactiveIcon() -> some View {
|
@ViewBuilder private func inactiveIcon() -> some View {
|
||||||
Image(systemName: "multiply.circle.fill")
|
Image(systemName: "multiply.circle.fill")
|
||||||
.foregroundColor(.secondary.opacity(0.65))
|
.foregroundColor(.secondary.opacity(0.65))
|
||||||
.background(Circle().foregroundColor(Color(uiColor: .systemBackground)))
|
.background(Circle().foregroundColor(Color(uiColor: .systemBackground)))
|
||||||
@ -80,7 +87,6 @@ struct ChatPreviewView: View {
|
|||||||
switch chat.chatInfo {
|
switch chat.chatInfo {
|
||||||
case let .direct(contact):
|
case let .direct(contact):
|
||||||
previewTitle(contact.verified == true ? verifiedIcon + t : t)
|
previewTitle(contact.verified == true ? verifiedIcon + t : t)
|
||||||
.foregroundColor(chat.chatInfo.ready ? .primary : .secondary)
|
|
||||||
case let .group(groupInfo):
|
case let .group(groupInfo):
|
||||||
let v = previewTitle(t)
|
let v = previewTitle(t)
|
||||||
switch (groupInfo.membership.memberStatus) {
|
switch (groupInfo.membership.memberStatus) {
|
||||||
@ -183,7 +189,7 @@ struct ChatPreviewView: View {
|
|||||||
if !contact.ready {
|
if !contact.ready {
|
||||||
if contact.nextSendGrpInv {
|
if contact.nextSendGrpInv {
|
||||||
chatPreviewInfoText("send direct message")
|
chatPreviewInfoText("send direct message")
|
||||||
} else {
|
} else if contact.active {
|
||||||
chatPreviewInfoText("connecting…")
|
chatPreviewInfoText("connecting…")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,6 +234,7 @@ struct ChatPreviewView: View {
|
|||||||
@ViewBuilder private func chatStatusImage() -> some View {
|
@ViewBuilder private func chatStatusImage() -> some View {
|
||||||
switch chat.chatInfo {
|
switch chat.chatInfo {
|
||||||
case let .direct(contact):
|
case let .direct(contact):
|
||||||
|
if contact.active {
|
||||||
switch (chatModel.contactNetworkStatus(contact)) {
|
switch (chatModel.contactNetworkStatus(contact)) {
|
||||||
case .connected: incognitoIcon(chat.chatInfo.incognito)
|
case .connected: incognitoIcon(chat.chatInfo.incognito)
|
||||||
case .error:
|
case .error:
|
||||||
@ -239,6 +246,9 @@ struct ChatPreviewView: View {
|
|||||||
default:
|
default:
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
incognitoIcon(chat.chatInfo.incognito)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
incognitoIcon(chat.chatInfo.incognito)
|
incognitoIcon(chat.chatInfo.incognito)
|
||||||
}
|
}
|
||||||
|
@ -462,6 +462,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case contactAlreadyExists(user: UserRef, contact: Contact)
|
case contactAlreadyExists(user: UserRef, contact: Contact)
|
||||||
case contactRequestAlreadyAccepted(user: UserRef, contact: Contact)
|
case contactRequestAlreadyAccepted(user: UserRef, contact: Contact)
|
||||||
case contactDeleted(user: UserRef, contact: Contact)
|
case contactDeleted(user: UserRef, contact: Contact)
|
||||||
|
case contactDeletedByContact(user: UserRef, contact: Contact)
|
||||||
case chatCleared(user: UserRef, chatInfo: ChatInfo)
|
case chatCleared(user: UserRef, chatInfo: ChatInfo)
|
||||||
case userProfileNoChange(user: User)
|
case userProfileNoChange(user: User)
|
||||||
case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile, updateSummary: UserProfileUpdateSummary)
|
case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile, updateSummary: UserProfileUpdateSummary)
|
||||||
@ -599,6 +600,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case .contactAlreadyExists: return "contactAlreadyExists"
|
case .contactAlreadyExists: return "contactAlreadyExists"
|
||||||
case .contactRequestAlreadyAccepted: return "contactRequestAlreadyAccepted"
|
case .contactRequestAlreadyAccepted: return "contactRequestAlreadyAccepted"
|
||||||
case .contactDeleted: return "contactDeleted"
|
case .contactDeleted: return "contactDeleted"
|
||||||
|
case .contactDeletedByContact: return "contactDeletedByContact"
|
||||||
case .chatCleared: return "chatCleared"
|
case .chatCleared: return "chatCleared"
|
||||||
case .userProfileNoChange: return "userProfileNoChange"
|
case .userProfileNoChange: return "userProfileNoChange"
|
||||||
case .userProfileUpdated: return "userProfileUpdated"
|
case .userProfileUpdated: return "userProfileUpdated"
|
||||||
@ -735,6 +737,7 @@ public enum ChatResponse: Decodable, Error {
|
|||||||
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
|
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
|
||||||
case let .contactRequestAlreadyAccepted(u, contact): return withUser(u, String(describing: contact))
|
case let .contactRequestAlreadyAccepted(u, contact): return withUser(u, String(describing: contact))
|
||||||
case let .contactDeleted(u, contact): return withUser(u, String(describing: contact))
|
case let .contactDeleted(u, contact): return withUser(u, String(describing: contact))
|
||||||
|
case let .contactDeletedByContact(u, contact): return withUser(u, String(describing: contact))
|
||||||
case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo))
|
case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo))
|
||||||
case .userProfileNoChange: return noDetails
|
case .userProfileNoChange: return noDetails
|
||||||
case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile))
|
case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile))
|
||||||
@ -1420,6 +1423,7 @@ public enum ChatErrorType: Decodable {
|
|||||||
case invalidConnReq
|
case invalidConnReq
|
||||||
case invalidChatMessage(connection: Connection, message: String)
|
case invalidChatMessage(connection: Connection, message: String)
|
||||||
case contactNotReady(contact: Contact)
|
case contactNotReady(contact: Contact)
|
||||||
|
case contactNotActive(contact: Contact)
|
||||||
case contactDisabled(contact: Contact)
|
case contactDisabled(contact: Contact)
|
||||||
case connectionDisabled(connection: Connection)
|
case connectionDisabled(connection: Connection)
|
||||||
case groupUserRole(groupInfo: GroupInfo, requiredRole: GroupMemberRole)
|
case groupUserRole(groupInfo: GroupInfo, requiredRole: GroupMemberRole)
|
||||||
|
@ -1373,6 +1373,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
|||||||
public var activeConn: Connection
|
public var activeConn: Connection
|
||||||
public var viaGroup: Int64?
|
public var viaGroup: Int64?
|
||||||
public var contactUsed: Bool
|
public var contactUsed: Bool
|
||||||
|
public var contactStatus: ContactStatus
|
||||||
public var chatSettings: ChatSettings
|
public var chatSettings: ChatSettings
|
||||||
public var userPreferences: Preferences
|
public var userPreferences: Preferences
|
||||||
public var mergedPreferences: ContactUserPreferences
|
public var mergedPreferences: ContactUserPreferences
|
||||||
@ -1384,8 +1385,9 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
|||||||
public var id: ChatId { get { "@\(contactId)" } }
|
public var id: ChatId { get { "@\(contactId)" } }
|
||||||
public var apiId: Int64 { get { contactId } }
|
public var apiId: Int64 { get { contactId } }
|
||||||
public var ready: Bool { get { activeConn.connStatus == .ready } }
|
public var ready: Bool { get { activeConn.connStatus == .ready } }
|
||||||
|
public var active: Bool { get { contactStatus == .active } }
|
||||||
public var sendMsgEnabled: Bool { get {
|
public var sendMsgEnabled: Bool { get {
|
||||||
(ready && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?? false))
|
(ready && active && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?? false))
|
||||||
|| nextSendGrpInv
|
|| nextSendGrpInv
|
||||||
} }
|
} }
|
||||||
public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } }
|
public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } }
|
||||||
@ -1430,6 +1432,7 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
|||||||
profile: LocalProfile.sampleData,
|
profile: LocalProfile.sampleData,
|
||||||
activeConn: Connection.sampleData,
|
activeConn: Connection.sampleData,
|
||||||
contactUsed: true,
|
contactUsed: true,
|
||||||
|
contactStatus: .active,
|
||||||
chatSettings: ChatSettings.defaults,
|
chatSettings: ChatSettings.defaults,
|
||||||
userPreferences: Preferences.sampleData,
|
userPreferences: Preferences.sampleData,
|
||||||
mergedPreferences: ContactUserPreferences.sampleData,
|
mergedPreferences: ContactUserPreferences.sampleData,
|
||||||
@ -1439,6 +1442,11 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ContactStatus: String, Decodable {
|
||||||
|
case active = "active"
|
||||||
|
case deleted = "deleted"
|
||||||
|
}
|
||||||
|
|
||||||
public struct ContactRef: Decodable, Equatable {
|
public struct ContactRef: Decodable, Equatable {
|
||||||
var contactId: Int64
|
var contactId: Int64
|
||||||
public var agentConnId: String
|
public var agentConnId: String
|
||||||
@ -2091,6 +2099,7 @@ public struct ChatItem: Identifiable, Decodable {
|
|||||||
case .rcvDecryptionError: return showNtfDir
|
case .rcvDecryptionError: return showNtfDir
|
||||||
case .rcvGroupInvitation: return showNtfDir
|
case .rcvGroupInvitation: return showNtfDir
|
||||||
case .sndGroupInvitation: return showNtfDir
|
case .sndGroupInvitation: return showNtfDir
|
||||||
|
case .rcvDirectEvent: return false
|
||||||
case .rcvGroupEvent(rcvGroupEvent: let rcvGroupEvent):
|
case .rcvGroupEvent(rcvGroupEvent: let rcvGroupEvent):
|
||||||
switch rcvGroupEvent {
|
switch rcvGroupEvent {
|
||||||
case .groupUpdated: return false
|
case .groupUpdated: return false
|
||||||
@ -2513,6 +2522,7 @@ public enum CIContent: Decodable, ItemContent {
|
|||||||
case rcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt32)
|
case rcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt32)
|
||||||
case rcvGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
|
case rcvGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
|
||||||
case sndGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
|
case sndGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
|
||||||
|
case rcvDirectEvent(rcvDirectEvent: RcvDirectEvent)
|
||||||
case rcvGroupEvent(rcvGroupEvent: RcvGroupEvent)
|
case rcvGroupEvent(rcvGroupEvent: RcvGroupEvent)
|
||||||
case sndGroupEvent(sndGroupEvent: SndGroupEvent)
|
case sndGroupEvent(sndGroupEvent: SndGroupEvent)
|
||||||
case rcvConnEvent(rcvConnEvent: RcvConnEvent)
|
case rcvConnEvent(rcvConnEvent: RcvConnEvent)
|
||||||
@ -2542,6 +2552,7 @@ public enum CIContent: Decodable, ItemContent {
|
|||||||
case let .rcvDecryptionError(msgDecryptError, _): return msgDecryptError.text
|
case let .rcvDecryptionError(msgDecryptError, _): return msgDecryptError.text
|
||||||
case let .rcvGroupInvitation(groupInvitation, _): return groupInvitation.text
|
case let .rcvGroupInvitation(groupInvitation, _): return groupInvitation.text
|
||||||
case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text
|
case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text
|
||||||
|
case let .rcvDirectEvent(rcvDirectEvent): return rcvDirectEvent.text
|
||||||
case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text
|
case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text
|
||||||
case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text
|
case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text
|
||||||
case let .rcvConnEvent(rcvConnEvent): return rcvConnEvent.text
|
case let .rcvConnEvent(rcvConnEvent): return rcvConnEvent.text
|
||||||
@ -3195,6 +3206,16 @@ public enum CIGroupInvitationStatus: String, Decodable {
|
|||||||
case expired
|
case expired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum RcvDirectEvent: Decodable {
|
||||||
|
case contactDeleted
|
||||||
|
|
||||||
|
var text: String {
|
||||||
|
switch self {
|
||||||
|
case .contactDeleted: return NSLocalizedString("deleted contact", comment: "rcv direct event chat item")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum RcvGroupEvent: Decodable {
|
public enum RcvGroupEvent: Decodable {
|
||||||
case memberAdded(groupMemberId: Int64, profile: Profile)
|
case memberAdded(groupMemberId: Int64, profile: Profile)
|
||||||
case memberConnected
|
case memberConnected
|
||||||
|
@ -97,7 +97,7 @@ kotlin {
|
|||||||
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6")
|
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6")
|
||||||
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
|
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
|
||||||
implementation("org.slf4j:slf4j-simple:2.0.7")
|
implementation("org.slf4j:slf4j-simple:2.0.7")
|
||||||
implementation("uk.co.caprica:vlcj:4.7.0")
|
implementation("uk.co.caprica:vlcj:4.7.3")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val desktopTest by getting
|
val desktopTest by getting
|
||||||
|
@ -73,6 +73,11 @@ else()
|
|||||||
target_link_libraries(app-lib rts simplex)
|
target_link_libraries(app-lib rts simplex)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(APPLE)
|
||||||
|
add_custom_command(TARGET app-lib POST_BUILD
|
||||||
|
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/patch-libapp-mac.sh
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
# Trying to copy resulting files into needed directory, but none of these work for some reason. This could allow to
|
# Trying to copy resulting files into needed directory, but none of these work for some reason. This could allow to
|
||||||
|
8
apps/multiplatform/common/src/commonMain/cpp/desktop/patch-libapp-mac.sh
Executable file
8
apps/multiplatform/common/src/commonMain/cpp/desktop/patch-libapp-mac.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
lib=libapp-lib.dylib
|
||||||
|
RPATHS=$(otool -l $lib | grep -E '/Users|/opt/|/usr/local' | cut -d' ' -f11)
|
||||||
|
for RPATH in $RPATHS; do
|
||||||
|
install_name_tool -delete_rpath $RPATH $lib
|
||||||
|
done
|
@ -797,6 +797,7 @@ data class Contact(
|
|||||||
val activeConn: Connection,
|
val activeConn: Connection,
|
||||||
val viaGroup: Long? = null,
|
val viaGroup: Long? = null,
|
||||||
val contactUsed: Boolean,
|
val contactUsed: Boolean,
|
||||||
|
val contactStatus: ContactStatus,
|
||||||
val chatSettings: ChatSettings,
|
val chatSettings: ChatSettings,
|
||||||
val userPreferences: ChatPreferences,
|
val userPreferences: ChatPreferences,
|
||||||
val mergedPreferences: ContactUserPreferences,
|
val mergedPreferences: ContactUserPreferences,
|
||||||
@ -809,8 +810,9 @@ data class Contact(
|
|||||||
override val id get() = "@$contactId"
|
override val id get() = "@$contactId"
|
||||||
override val apiId get() = contactId
|
override val apiId get() = contactId
|
||||||
override val ready get() = activeConn.connStatus == ConnStatus.Ready
|
override val ready get() = activeConn.connStatus == ConnStatus.Ready
|
||||||
|
val active get() = contactStatus == ContactStatus.Active
|
||||||
override val sendMsgEnabled get() =
|
override val sendMsgEnabled get() =
|
||||||
(ready && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false))
|
(ready && active && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false))
|
||||||
|| nextSendGrpInv
|
|| nextSendGrpInv
|
||||||
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
|
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
|
||||||
override val ntfsEnabled get() = chatSettings.enableNtfs
|
override val ntfsEnabled get() = chatSettings.enableNtfs
|
||||||
@ -859,6 +861,7 @@ data class Contact(
|
|||||||
profile = LocalProfile.sampleData,
|
profile = LocalProfile.sampleData,
|
||||||
activeConn = Connection.sampleData,
|
activeConn = Connection.sampleData,
|
||||||
contactUsed = true,
|
contactUsed = true,
|
||||||
|
contactStatus = ContactStatus.Active,
|
||||||
chatSettings = ChatSettings(enableNtfs = true, sendRcpts = null, favorite = false),
|
chatSettings = ChatSettings(enableNtfs = true, sendRcpts = null, favorite = false),
|
||||||
userPreferences = ChatPreferences.sampleData,
|
userPreferences = ChatPreferences.sampleData,
|
||||||
mergedPreferences = ContactUserPreferences.sampleData,
|
mergedPreferences = ContactUserPreferences.sampleData,
|
||||||
@ -869,6 +872,12 @@ data class Contact(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class ContactStatus {
|
||||||
|
@SerialName("active") Active,
|
||||||
|
@SerialName("deleted") Deleted;
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class ContactRef(
|
class ContactRef(
|
||||||
val contactId: Long,
|
val contactId: Long,
|
||||||
@ -1471,6 +1480,7 @@ data class ChatItem (
|
|||||||
is CIContent.RcvDecryptionError -> showNtfDir
|
is CIContent.RcvDecryptionError -> showNtfDir
|
||||||
is CIContent.RcvGroupInvitation -> showNtfDir
|
is CIContent.RcvGroupInvitation -> showNtfDir
|
||||||
is CIContent.SndGroupInvitation -> showNtfDir
|
is CIContent.SndGroupInvitation -> showNtfDir
|
||||||
|
is CIContent.RcvDirectEventContent -> false
|
||||||
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
|
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
|
||||||
is RcvGroupEvent.MemberAdded -> false
|
is RcvGroupEvent.MemberAdded -> false
|
||||||
is RcvGroupEvent.MemberConnected -> false
|
is RcvGroupEvent.MemberConnected -> false
|
||||||
@ -1854,6 +1864,7 @@ sealed class CIContent: ItemContent {
|
|||||||
@Serializable @SerialName("rcvDecryptionError") class RcvDecryptionError(val msgDecryptError: MsgDecryptError, val msgCount: UInt): CIContent() { override val msgContent: MsgContent? get() = null }
|
@Serializable @SerialName("rcvDecryptionError") class RcvDecryptionError(val msgDecryptError: MsgDecryptError, val msgCount: UInt): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
@Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
|
@Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
|
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
|
@Serializable @SerialName("rcvDirectEvent") class RcvDirectEventContent(val rcvDirectEvent: RcvDirectEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
@Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
@Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
@Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
@Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
@Serializable @SerialName("rcvConnEvent") class RcvConnEventContent(val rcvConnEvent: RcvConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
@Serializable @SerialName("rcvConnEvent") class RcvConnEventContent(val rcvConnEvent: RcvConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||||
@ -1881,6 +1892,7 @@ sealed class CIContent: ItemContent {
|
|||||||
is RcvDecryptionError -> msgDecryptError.text
|
is RcvDecryptionError -> msgDecryptError.text
|
||||||
is RcvGroupInvitation -> groupInvitation.text
|
is RcvGroupInvitation -> groupInvitation.text
|
||||||
is SndGroupInvitation -> groupInvitation.text
|
is SndGroupInvitation -> groupInvitation.text
|
||||||
|
is RcvDirectEventContent -> rcvDirectEvent.text
|
||||||
is RcvGroupEventContent -> rcvGroupEvent.text
|
is RcvGroupEventContent -> rcvGroupEvent.text
|
||||||
is SndGroupEventContent -> sndGroupEvent.text
|
is SndGroupEventContent -> sndGroupEvent.text
|
||||||
is RcvConnEventContent -> rcvConnEvent.text
|
is RcvConnEventContent -> rcvConnEvent.text
|
||||||
@ -2487,6 +2499,15 @@ sealed class MsgErrorType() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class RcvDirectEvent() {
|
||||||
|
@Serializable @SerialName("contactDeleted") class ContactDeleted(): RcvDirectEvent()
|
||||||
|
|
||||||
|
val text: String get() = when (this) {
|
||||||
|
is ContactDeleted -> generalGetString(MR.strings.rcv_direct_event_contact_deleted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed class RcvGroupEvent() {
|
sealed class RcvGroupEvent() {
|
||||||
@Serializable @SerialName("memberAdded") class MemberAdded(val groupMemberId: Long, val profile: Profile): RcvGroupEvent()
|
@Serializable @SerialName("memberAdded") class MemberAdded(val groupMemberId: Long, val profile: Profile): RcvGroupEvent()
|
||||||
|
@ -1366,6 +1366,11 @@ object ChatController {
|
|||||||
chatModel.removeChat(r.connection.id)
|
chatModel.removeChat(r.connection.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is CR.ContactDeletedByContact -> {
|
||||||
|
if (active(r.user) && r.contact.directOrUsed) {
|
||||||
|
chatModel.updateContact(r.contact)
|
||||||
|
}
|
||||||
|
}
|
||||||
is CR.ContactConnected -> {
|
is CR.ContactConnected -> {
|
||||||
if (active(r.user) && r.contact.directOrUsed) {
|
if (active(r.user) && r.contact.directOrUsed) {
|
||||||
chatModel.updateContact(r.contact)
|
chatModel.updateContact(r.contact)
|
||||||
@ -3295,6 +3300,7 @@ sealed class CR {
|
|||||||
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
|
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
|
||||||
@Serializable @SerialName("contactRequestAlreadyAccepted") class ContactRequestAlreadyAccepted(val user: UserRef, val contact: Contact): CR()
|
@Serializable @SerialName("contactRequestAlreadyAccepted") class ContactRequestAlreadyAccepted(val user: UserRef, val contact: Contact): CR()
|
||||||
@Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR()
|
@Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR()
|
||||||
|
@Serializable @SerialName("contactDeletedByContact") class ContactDeletedByContact(val user: UserRef, val contact: Contact): CR()
|
||||||
@Serializable @SerialName("chatCleared") class ChatCleared(val user: UserRef, val chatInfo: ChatInfo): CR()
|
@Serializable @SerialName("chatCleared") class ChatCleared(val user: UserRef, val chatInfo: ChatInfo): CR()
|
||||||
@Serializable @SerialName("userProfileNoChange") class UserProfileNoChange(val user: User): CR()
|
@Serializable @SerialName("userProfileNoChange") class UserProfileNoChange(val user: User): CR()
|
||||||
@Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val user: User, val fromProfile: Profile, val toProfile: Profile, val updateSummary: UserProfileUpdateSummary): CR()
|
@Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val user: User, val fromProfile: Profile, val toProfile: Profile, val updateSummary: UserProfileUpdateSummary): CR()
|
||||||
@ -3426,6 +3432,7 @@ sealed class CR {
|
|||||||
is ContactAlreadyExists -> "contactAlreadyExists"
|
is ContactAlreadyExists -> "contactAlreadyExists"
|
||||||
is ContactRequestAlreadyAccepted -> "contactRequestAlreadyAccepted"
|
is ContactRequestAlreadyAccepted -> "contactRequestAlreadyAccepted"
|
||||||
is ContactDeleted -> "contactDeleted"
|
is ContactDeleted -> "contactDeleted"
|
||||||
|
is ContactDeletedByContact -> "contactDeletedByContact"
|
||||||
is ChatCleared -> "chatCleared"
|
is ChatCleared -> "chatCleared"
|
||||||
is UserProfileNoChange -> "userProfileNoChange"
|
is UserProfileNoChange -> "userProfileNoChange"
|
||||||
is UserProfileUpdated -> "userProfileUpdated"
|
is UserProfileUpdated -> "userProfileUpdated"
|
||||||
@ -3554,6 +3561,7 @@ sealed class CR {
|
|||||||
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
|
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
|
||||||
is ContactRequestAlreadyAccepted -> withUser(user, json.encodeToString(contact))
|
is ContactRequestAlreadyAccepted -> withUser(user, json.encodeToString(contact))
|
||||||
is ContactDeleted -> withUser(user, json.encodeToString(contact))
|
is ContactDeleted -> withUser(user, json.encodeToString(contact))
|
||||||
|
is ContactDeletedByContact -> withUser(user, json.encodeToString(contact))
|
||||||
is ChatCleared -> withUser(user, json.encodeToString(chatInfo))
|
is ChatCleared -> withUser(user, json.encodeToString(chatInfo))
|
||||||
is UserProfileNoChange -> withUser(user, noDetails())
|
is UserProfileNoChange -> withUser(user, noDetails())
|
||||||
is UserProfileUpdated -> withUser(user, json.encodeToString(toProfile))
|
is UserProfileUpdated -> withUser(user, json.encodeToString(toProfile))
|
||||||
@ -3822,6 +3830,7 @@ sealed class ChatErrorType {
|
|||||||
is InvalidConnReq -> "invalidConnReq"
|
is InvalidConnReq -> "invalidConnReq"
|
||||||
is InvalidChatMessage -> "invalidChatMessage"
|
is InvalidChatMessage -> "invalidChatMessage"
|
||||||
is ContactNotReady -> "contactNotReady"
|
is ContactNotReady -> "contactNotReady"
|
||||||
|
is ContactNotActive -> "contactNotActive"
|
||||||
is ContactDisabled -> "contactDisabled"
|
is ContactDisabled -> "contactDisabled"
|
||||||
is ConnectionDisabled -> "connectionDisabled"
|
is ConnectionDisabled -> "connectionDisabled"
|
||||||
is GroupUserRole -> "groupUserRole"
|
is GroupUserRole -> "groupUserRole"
|
||||||
@ -3897,6 +3906,7 @@ sealed class ChatErrorType {
|
|||||||
@Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType()
|
@Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType()
|
||||||
@Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType()
|
@Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType()
|
||||||
@Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType()
|
@Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType()
|
||||||
|
@Serializable @SerialName("contactNotActive") class ContactNotActive(val contact: Contact): ChatErrorType()
|
||||||
@Serializable @SerialName("contactDisabled") class ContactDisabled(val contact: Contact): ChatErrorType()
|
@Serializable @SerialName("contactDisabled") class ContactDisabled(val contact: Contact): ChatErrorType()
|
||||||
@Serializable @SerialName("connectionDisabled") class ConnectionDisabled(val connection: Connection): ChatErrorType()
|
@Serializable @SerialName("connectionDisabled") class ConnectionDisabled(val connection: Connection): ChatErrorType()
|
||||||
@Serializable @SerialName("groupUserRole") class GroupUserRole(val groupInfo: GroupInfo, val requiredRole: GroupMemberRole): ChatErrorType()
|
@Serializable @SerialName("groupUserRole") class GroupUserRole(val groupInfo: GroupInfo, val requiredRole: GroupMemberRole): ChatErrorType()
|
||||||
|
@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.*
|
|||||||
import androidx.compose.foundation.text.*
|
import androidx.compose.foundation.text.*
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@ -291,7 +290,7 @@ fun ChatInfoLayout(
|
|||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contact.ready) {
|
if (contact.ready && contact.active) {
|
||||||
SectionView {
|
SectionView {
|
||||||
if (connectionCode != null) {
|
if (connectionCode != null) {
|
||||||
VerifyCodeButton(contact.verified, verifyClicked)
|
VerifyCodeButton(contact.verified, verifyClicked)
|
||||||
@ -318,7 +317,7 @@ fun ChatInfoLayout(
|
|||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contact.ready) {
|
if (contact.ready && contact.active) {
|
||||||
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
|
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
|
||||||
SectionItemView({
|
SectionItemView({
|
||||||
AlertManager.shared.showAlertMsg(
|
AlertManager.shared.showAlertMsg(
|
||||||
|
@ -118,7 +118,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
|||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
if (chat.chatInfo is ChatInfo.Direct && !chat.chatInfo.contact.ready && !chat.chatInfo.contact.nextSendGrpInv) {
|
if (
|
||||||
|
chat.chatInfo is ChatInfo.Direct
|
||||||
|
&& !chat.chatInfo.contact.ready
|
||||||
|
&& chat.chatInfo.contact.active
|
||||||
|
&& !chat.chatInfo.contact.nextSendGrpInv
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
generalGetString(MR.strings.contact_connection_pending),
|
generalGetString(MR.strings.contact_connection_pending),
|
||||||
Modifier.padding(top = 4.dp),
|
Modifier.padding(top = 4.dp),
|
||||||
@ -550,15 +555,15 @@ fun ChatInfoToolbar(
|
|||||||
showMenu.value = false
|
showMenu.value = false
|
||||||
startCall(CallMediaType.Audio)
|
startCall(CallMediaType.Audio)
|
||||||
},
|
},
|
||||||
enabled = chat.chatInfo.contact.ready) {
|
enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active) {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(MR.images.ic_call_500),
|
painterResource(MR.images.ic_call_500),
|
||||||
stringResource(MR.strings.icon_descr_more_button),
|
stringResource(MR.strings.icon_descr_more_button),
|
||||||
tint = if (chat.chatInfo.contact.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (chat.chatInfo.contact.ready) {
|
if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) {
|
||||||
menuItems.add {
|
menuItems.add {
|
||||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||||
showMenu.value = false
|
showMenu.value = false
|
||||||
@ -576,7 +581,7 @@ fun ChatInfoToolbar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready) || chat.chatInfo is ChatInfo.Group) {
|
if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready && chat.chatInfo.contact.active) || chat.chatInfo is ChatInfo.Group) {
|
||||||
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
||||||
menuItems.add {
|
menuItems.add {
|
||||||
ItemAction(
|
ItemAction(
|
||||||
|
@ -127,7 +127,7 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau
|
|||||||
)
|
)
|
||||||
if (showPreview.value) {
|
if (showPreview.value) {
|
||||||
VideoPreviewImageView(preview, onClick, onLongClick)
|
VideoPreviewImageView(preview, onClick, onLongClick)
|
||||||
PlayButton(brokenVideo, onLongClick = onLongClick, if (appPlatform.isAndroid) play else onClick)
|
PlayButton(brokenVideo, onLongClick = onLongClick, play)
|
||||||
}
|
}
|
||||||
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
|
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
|
||||||
}
|
}
|
||||||
|
@ -352,6 +352,7 @@ fun ChatItemView(
|
|||||||
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember)
|
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember)
|
||||||
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||||
|
is CIContent.RcvDirectEventContent -> EventItemView()
|
||||||
is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) {
|
is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) {
|
||||||
is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText())
|
is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText())
|
||||||
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
|
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
|
||||||
|
@ -158,12 +158,11 @@ private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap,
|
|||||||
player.stop()
|
player.stop()
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
player.enableSound(true)
|
|
||||||
snapshotFlow { isCurrentPage.value }
|
snapshotFlow { isCurrentPage.value }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect {
|
.collect {
|
||||||
// Do not autoplay on desktop because it needs workaround
|
if (it) play() else stop()
|
||||||
if (it && appPlatform.isAndroid) play() else if (!it) stop()
|
player.enableSound(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ fun ChatPreviewView(
|
|||||||
val cInfo = chat.chatInfo
|
val cInfo = chat.chatInfo
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun groupInactiveIcon() {
|
fun inactiveIcon() {
|
||||||
Icon(
|
Icon(
|
||||||
painterResource(MR.images.ic_cancel_filled),
|
painterResource(MR.images.ic_cancel_filled),
|
||||||
stringResource(MR.strings.icon_descr_group_inactive),
|
stringResource(MR.strings.icon_descr_group_inactive),
|
||||||
@ -53,13 +53,19 @@ fun ChatPreviewView(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun chatPreviewImageOverlayIcon() {
|
fun chatPreviewImageOverlayIcon() {
|
||||||
if (cInfo is ChatInfo.Group) {
|
when (cInfo) {
|
||||||
|
is ChatInfo.Direct ->
|
||||||
|
if (!cInfo.contact.active) {
|
||||||
|
inactiveIcon()
|
||||||
|
}
|
||||||
|
is ChatInfo.Group ->
|
||||||
when (cInfo.groupInfo.membership.memberStatus) {
|
when (cInfo.groupInfo.membership.memberStatus) {
|
||||||
GroupMemberStatus.MemLeft -> groupInactiveIcon()
|
GroupMemberStatus.MemLeft -> inactiveIcon()
|
||||||
GroupMemberStatus.MemRemoved -> groupInactiveIcon()
|
GroupMemberStatus.MemRemoved -> inactiveIcon()
|
||||||
GroupMemberStatus.MemGroupDeleted -> groupInactiveIcon()
|
GroupMemberStatus.MemGroupDeleted -> inactiveIcon()
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +131,7 @@ fun ChatPreviewView(
|
|||||||
if (cInfo.contact.verified) {
|
if (cInfo.contact.verified) {
|
||||||
VerifiedIcon()
|
VerifiedIcon()
|
||||||
}
|
}
|
||||||
chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else MaterialTheme.colors.secondary)
|
chatPreviewTitleText()
|
||||||
}
|
}
|
||||||
is ChatInfo.Group ->
|
is ChatInfo.Group ->
|
||||||
when (cInfo.groupInfo.membership.memberStatus) {
|
when (cInfo.groupInfo.membership.memberStatus) {
|
||||||
@ -174,7 +180,7 @@ fun ChatPreviewView(
|
|||||||
is ChatInfo.Direct ->
|
is ChatInfo.Direct ->
|
||||||
if (cInfo.contact.nextSendGrpInv) {
|
if (cInfo.contact.nextSendGrpInv) {
|
||||||
Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary)
|
Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary)
|
||||||
} else if (!cInfo.ready) {
|
} else if (!cInfo.ready && cInfo.contact.active) {
|
||||||
Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary)
|
Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary)
|
||||||
}
|
}
|
||||||
is ChatInfo.Group ->
|
is ChatInfo.Group ->
|
||||||
@ -191,6 +197,7 @@ fun ChatPreviewView(
|
|||||||
@Composable
|
@Composable
|
||||||
fun chatStatusImage() {
|
fun chatStatusImage() {
|
||||||
if (cInfo is ChatInfo.Direct) {
|
if (cInfo is ChatInfo.Direct) {
|
||||||
|
if (cInfo.contact.active) {
|
||||||
val descr = contactNetworkStatus?.statusString
|
val descr = contactNetworkStatus?.statusString
|
||||||
when (contactNetworkStatus) {
|
when (contactNetworkStatus) {
|
||||||
is NetworkStatus.Connected ->
|
is NetworkStatus.Connected ->
|
||||||
@ -217,6 +224,9 @@ fun ChatPreviewView(
|
|||||||
} else {
|
} else {
|
||||||
IncognitoIcon(chat.chatInfo.incognito)
|
IncognitoIcon(chat.chatInfo.incognito)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
IncognitoIcon(chat.chatInfo.incognito)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
@ -1105,6 +1105,9 @@
|
|||||||
<string name="you_rejected_group_invitation">You rejected group invitation</string>
|
<string name="you_rejected_group_invitation">You rejected group invitation</string>
|
||||||
<string name="group_invitation_expired">Group invitation expired</string>
|
<string name="group_invitation_expired">Group invitation expired</string>
|
||||||
|
|
||||||
|
<!-- Direct event chat items -->
|
||||||
|
<string name="rcv_direct_event_contact_deleted">deleted contact</string>
|
||||||
|
|
||||||
<!-- Group event chat items -->
|
<!-- Group event chat items -->
|
||||||
<string name="rcv_group_event_member_added">invited %1$s</string>
|
<string name="rcv_group_event_member_added">invited %1$s</string>
|
||||||
<string name="rcv_group_event_member_connected">connected</string>
|
<string name="rcv_group_event_member_connected">connected</string>
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
package org.jetbrains.compose.videoplayer
|
||||||
|
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asComposeImageBitmap
|
||||||
|
import org.jetbrains.skia.Bitmap
|
||||||
|
import org.jetbrains.skia.ColorAlphaType
|
||||||
|
import org.jetbrains.skia.ColorType
|
||||||
|
import org.jetbrains.skia.ImageInfo
|
||||||
|
import uk.co.caprica.vlcj.player.base.MediaPlayer
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.CallbackVideoSurface
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.VideoSurface
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.VideoSurfaceAdapters
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormat
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormatCallback
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.callback.RenderCallback
|
||||||
|
import uk.co.caprica.vlcj.player.embedded.videosurface.callback.format.RV32BufferFormat
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import javax.swing.SwingUtilities
|
||||||
|
|
||||||
|
// https://github.com/JetBrains/compose-multiplatform/pull/3336/files
|
||||||
|
internal class SkiaBitmapVideoSurface : VideoSurface(VideoSurfaceAdapters.getVideoSurfaceAdapter()) {
|
||||||
|
|
||||||
|
private val videoSurface = SkiaBitmapVideoSurface()
|
||||||
|
private lateinit var imageInfo: ImageInfo
|
||||||
|
private lateinit var frameBytes: ByteArray
|
||||||
|
private val skiaBitmap: Bitmap = Bitmap()
|
||||||
|
private val composeBitmap = mutableStateOf<ImageBitmap?>(null)
|
||||||
|
|
||||||
|
val bitmap: State<ImageBitmap?> = composeBitmap
|
||||||
|
|
||||||
|
override fun attach(mediaPlayer: MediaPlayer) {
|
||||||
|
videoSurface.attach(mediaPlayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SkiaBitmapBufferFormatCallback : BufferFormatCallback {
|
||||||
|
private var sourceWidth: Int = 0
|
||||||
|
private var sourceHeight: Int = 0
|
||||||
|
|
||||||
|
override fun getBufferFormat(sourceWidth: Int, sourceHeight: Int): BufferFormat {
|
||||||
|
this.sourceWidth = sourceWidth
|
||||||
|
this.sourceHeight = sourceHeight
|
||||||
|
return RV32BufferFormat(sourceWidth, sourceHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun allocatedBuffers(buffers: Array<ByteBuffer>) {
|
||||||
|
frameBytes = buffers[0].run { ByteArray(remaining()).also(::get) }
|
||||||
|
imageInfo = ImageInfo(
|
||||||
|
sourceWidth,
|
||||||
|
sourceHeight,
|
||||||
|
ColorType.BGRA_8888,
|
||||||
|
ColorAlphaType.PREMUL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SkiaBitmapRenderCallback : RenderCallback {
|
||||||
|
override fun display(
|
||||||
|
mediaPlayer: MediaPlayer,
|
||||||
|
nativeBuffers: Array<ByteBuffer>,
|
||||||
|
bufferFormat: BufferFormat,
|
||||||
|
) {
|
||||||
|
SwingUtilities.invokeLater {
|
||||||
|
nativeBuffers[0].rewind()
|
||||||
|
nativeBuffers[0].get(frameBytes)
|
||||||
|
skiaBitmap.installPixels(imageInfo, frameBytes, bufferFormat.width * 4)
|
||||||
|
composeBitmap.value = skiaBitmap.asComposeImageBitmap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class SkiaBitmapVideoSurface : CallbackVideoSurface(
|
||||||
|
SkiaBitmapBufferFormatCallback(),
|
||||||
|
SkiaBitmapRenderCallback(),
|
||||||
|
true,
|
||||||
|
videoSurfaceAdapter,
|
||||||
|
)
|
||||||
|
}
|
@ -6,6 +6,8 @@ import boofcv.struct.image.GrayU8
|
|||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import org.jetbrains.skia.Image
|
import org.jetbrains.skia.Image
|
||||||
import java.awt.RenderingHints
|
import java.awt.RenderingHints
|
||||||
|
import java.awt.geom.AffineTransform
|
||||||
|
import java.awt.image.AffineTransformOp
|
||||||
import java.awt.image.BufferedImage
|
import java.awt.image.BufferedImage
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
@ -171,3 +173,37 @@ actual fun isAnimImage(uri: URI, drawable: Any?): Boolean {
|
|||||||
@Suppress("NewApi")
|
@Suppress("NewApi")
|
||||||
actual fun loadImageBitmap(inputStream: InputStream): ImageBitmap =
|
actual fun loadImageBitmap(inputStream: InputStream): ImageBitmap =
|
||||||
Image.makeFromEncoded(inputStream.readAllBytes()).toComposeImageBitmap()
|
Image.makeFromEncoded(inputStream.readAllBytes()).toComposeImageBitmap()
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/68926993
|
||||||
|
fun BufferedImage.rotate(angle: Double): BufferedImage {
|
||||||
|
val sin = Math.abs(Math.sin(Math.toRadians(angle)))
|
||||||
|
val cos = Math.abs(Math.cos(Math.toRadians(angle)))
|
||||||
|
val w = width
|
||||||
|
val h = height
|
||||||
|
val neww = Math.floor(w * cos + h * sin).toInt()
|
||||||
|
val newh = Math.floor(h * cos + w * sin).toInt()
|
||||||
|
val rotated = BufferedImage(neww, newh, type)
|
||||||
|
val graphic = rotated.createGraphics()
|
||||||
|
graphic.translate((neww - w) / 2, (newh - h) / 2)
|
||||||
|
graphic.rotate(Math.toRadians(angle), (w / 2).toDouble(), (h / 2).toDouble())
|
||||||
|
graphic.drawRenderedImage(this, null)
|
||||||
|
graphic.dispose()
|
||||||
|
return rotated
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/9559043
|
||||||
|
fun BufferedImage.flip(vertically: Boolean, horizontally: Boolean): BufferedImage {
|
||||||
|
if (!vertically && !horizontally) return this
|
||||||
|
val tx: AffineTransform
|
||||||
|
if (vertically && horizontally) {
|
||||||
|
tx = AffineTransform.getScaleInstance(-1.0, -1.0)
|
||||||
|
tx.translate(-width.toDouble(), -height.toDouble())
|
||||||
|
} else if (vertically) {
|
||||||
|
tx = AffineTransform.getScaleInstance(1.0, -1.0)
|
||||||
|
tx.translate(0.0, -height.toDouble())
|
||||||
|
} else {
|
||||||
|
tx = AffineTransform.getScaleInstance(-1.0, 1.0)
|
||||||
|
tx.translate(-width.toDouble(), 0.0)
|
||||||
|
}
|
||||||
|
return AffineTransformOp(tx, AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(this, null)
|
||||||
|
}
|
||||||
|
@ -2,17 +2,20 @@ package chat.simplex.common.platform
|
|||||||
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.graphics.toComposeImageBitmap
|
|
||||||
import chat.simplex.common.views.helpers.*
|
import chat.simplex.common.views.helpers.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import uk.co.caprica.vlcj.player.base.MediaPlayer
|
import uk.co.caprica.vlcj.media.VideoOrientation
|
||||||
|
import uk.co.caprica.vlcj.player.base.*
|
||||||
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
|
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
|
||||||
import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent
|
import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
actual class VideoPlayer actual constructor(
|
actual class VideoPlayer actual constructor(
|
||||||
@ -29,17 +32,14 @@ actual class VideoPlayer actual constructor(
|
|||||||
override val duration: MutableState<Long> = mutableStateOf(0L)
|
override val duration: MutableState<Long> = mutableStateOf(0L)
|
||||||
override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview)
|
override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview)
|
||||||
|
|
||||||
val mediaPlayerComponent = initializeMediaPlayerComponent()
|
val mediaPlayerComponent by lazy { runBlocking(playerThread.asCoroutineDispatcher()) { getOrCreatePlayer() } }
|
||||||
val player by lazy { mediaPlayerComponent.mediaPlayer() }
|
val player by lazy { mediaPlayerComponent.mediaPlayer() }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
withBGApi {
|
|
||||||
setPreviewAndDuration()
|
setPreviewAndDuration()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private val currentVolume: Int by lazy { player.audio().volume() }
|
private var isReleased: AtomicBoolean = AtomicBoolean(false)
|
||||||
private var isReleased: Boolean = false
|
|
||||||
|
|
||||||
private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null)
|
private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null)
|
||||||
private var progressJob: Job? = null
|
private var progressJob: Job? = null
|
||||||
@ -48,6 +48,7 @@ actual class VideoPlayer actual constructor(
|
|||||||
PLAYING, PAUSED, STOPPED
|
PLAYING, PAUSED, STOPPED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Should be called in [playerThread]. Otherwise, it creates deadlocks in [player.stop] and [player.release] calls */
|
||||||
private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean {
|
private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean {
|
||||||
val filepath = getAppFilePath(uri)
|
val filepath = getAppFilePath(uri)
|
||||||
if (filepath == null || !File(filepath).exists()) {
|
if (filepath == null || !File(filepath).exists()) {
|
||||||
@ -87,7 +88,7 @@ actual class VideoPlayer actual constructor(
|
|||||||
// Player can only be accessed in one specific thread
|
// Player can only be accessed in one specific thread
|
||||||
progressJob = CoroutineScope(Dispatchers.Main).launch {
|
progressJob = CoroutineScope(Dispatchers.Main).launch {
|
||||||
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
|
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
|
||||||
while (isActive && !isReleased && player.isPlaying) {
|
while (isActive && !isReleased.get() && player.isPlaying) {
|
||||||
// Even when current position is equal to duration, the player has isPlaying == true for some time,
|
// Even when current position is equal to duration, the player has isPlaying == true for some time,
|
||||||
// so help to make the playback stopped in UI immediately
|
// so help to make the playback stopped in UI immediately
|
||||||
if (player.currentPosition == player.duration) {
|
if (player.currentPosition == player.duration) {
|
||||||
@ -97,7 +98,7 @@ actual class VideoPlayer actual constructor(
|
|||||||
delay(50)
|
delay(50)
|
||||||
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
|
onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING)
|
||||||
}
|
}
|
||||||
if (isActive && !isReleased) {
|
if (isActive && !isReleased.get()) {
|
||||||
onProgressUpdate(player.currentPosition.toLong(), TrackState.PAUSED)
|
onProgressUpdate(player.currentPosition.toLong(), TrackState.PAUSED)
|
||||||
}
|
}
|
||||||
onProgressUpdate(null, TrackState.PAUSED)
|
onProgressUpdate(null, TrackState.PAUSED)
|
||||||
@ -107,10 +108,12 @@ actual class VideoPlayer actual constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun stop() {
|
override fun stop() {
|
||||||
if (isReleased || !videoPlaying.value) return
|
if (isReleased.get() || !videoPlaying.value) return
|
||||||
player.controls().stop()
|
playerThread.execute {
|
||||||
|
player.stop()
|
||||||
stopListener()
|
stopListener()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun stopListener() {
|
private fun stopListener() {
|
||||||
val afterCoroutineCancel: CompletionHandler = {
|
val afterCoroutineCancel: CompletionHandler = {
|
||||||
@ -133,6 +136,7 @@ actual class VideoPlayer actual constructor(
|
|||||||
if (progress.value == duration.value) {
|
if (progress.value == duration.value) {
|
||||||
progress.value = 0
|
progress.value = 0
|
||||||
}
|
}
|
||||||
|
playerThread.execute {
|
||||||
videoPlaying.value = start(progress.value) { pro, _ ->
|
videoPlaying.value = start(progress.value) { pro, _ ->
|
||||||
if (pro != null) {
|
if (pro != null) {
|
||||||
progress.value = pro
|
progress.value = pro
|
||||||
@ -147,31 +151,42 @@ actual class VideoPlayer actual constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun enableSound(enable: Boolean): Boolean {
|
|
||||||
if (isReleased) return false
|
|
||||||
if (soundEnabled.value == enable) return false
|
|
||||||
soundEnabled.value = enable
|
|
||||||
player.audio().setVolume(if (enable) currentVolume else 0)
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun release(remove: Boolean) { withApi {
|
override fun enableSound(enable: Boolean): Boolean {
|
||||||
if (isReleased) return@withApi
|
// Impossible to change volume for only one player. It changes for every player
|
||||||
isReleased = true
|
// https://github.com/caprica/vlcj/issues/985
|
||||||
// TODO
|
return false
|
||||||
/** [player.release] freezes thread for some reason. It happens periodically. So doing this we don't see the freeze, but it's still there */
|
/*if (isReleased.get() || soundEnabled.value == enable) return false
|
||||||
if (player.isPlaying) player.stop()
|
soundEnabled.value = enable
|
||||||
CoroutineScope(Dispatchers.IO).launch { player.release() }
|
playerThread.execute {
|
||||||
|
player.audio().isMute = !enable
|
||||||
|
}
|
||||||
|
return true*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun release(remove: Boolean) {
|
||||||
|
CoroutineScope(playerThread.asCoroutineDispatcher()).launch {
|
||||||
|
if (isReleased.get()) return@launch
|
||||||
|
isReleased.set(true)
|
||||||
|
if (player.isPlaying) {
|
||||||
|
player.stop()
|
||||||
|
}
|
||||||
|
if (usePool) {
|
||||||
|
putPlayer(mediaPlayerComponent)
|
||||||
|
} else {
|
||||||
|
player.release()
|
||||||
|
}
|
||||||
if (remove) {
|
if (remove) {
|
||||||
VideoPlayerHolder.players.remove(uri to gallery)
|
VideoPlayerHolder.players.remove(uri to gallery)
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val MediaPlayer.currentPosition: Int
|
private val MediaPlayer.currentPosition: Int
|
||||||
get() = if (isReleased) 0 else max(0, player.status().time().toInt())
|
get() = if (isReleased.get()) 0 else max(0, status().time().toInt())
|
||||||
|
|
||||||
private suspend fun setPreviewAndDuration() {
|
private fun setPreviewAndDuration() {
|
||||||
// It freezes main thread, doing it in IO thread
|
// It freezes main thread, doing it in IO thread
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(defaultPreview, uri) }
|
val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(defaultPreview, uri) }
|
||||||
@ -182,6 +197,15 @@ actual class VideoPlayer actual constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val usePool = false
|
||||||
|
|
||||||
|
private fun Component.mediaPlayer() = when (this) {
|
||||||
|
is CallbackMediaPlayerComponent -> mediaPlayer()
|
||||||
|
is EmbeddedMediaPlayerComponent -> mediaPlayer()
|
||||||
|
else -> error("mediaPlayer() can only be called on vlcj player components")
|
||||||
|
}
|
||||||
|
|
||||||
private fun initializeMediaPlayerComponent(): Component {
|
private fun initializeMediaPlayerComponent(): Component {
|
||||||
return if (desktopPlatform.isMac()) {
|
return if (desktopPlatform.isMac()) {
|
||||||
CallbackMediaPlayerComponent()
|
CallbackMediaPlayerComponent()
|
||||||
@ -190,27 +214,62 @@ actual class VideoPlayer actual constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Component.mediaPlayer() = when (this) {
|
suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?): VideoPlayerInterface.PreviewAndDuration = withContext(playerThread.asCoroutineDispatcher()) {
|
||||||
is CallbackMediaPlayerComponent -> mediaPlayer()
|
val mediaComponent = getOrCreateHelperPlayer()
|
||||||
is EmbeddedMediaPlayerComponent -> mediaPlayer()
|
val player = mediaComponent.mediaPlayer()
|
||||||
else -> error("mediaPlayer() can only be called on vlcj player components")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?): VideoPlayerInterface.PreviewAndDuration {
|
|
||||||
val player = CallbackMediaPlayerComponent().mediaPlayer()
|
|
||||||
if (uri == null || !File(uri.rawPath).exists()) {
|
if (uri == null || !File(uri.rawPath).exists()) {
|
||||||
return VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L)
|
return@withContext VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L)
|
||||||
}
|
}
|
||||||
player.media().startPaused(uri.toString().replaceFirst("file:", "file://"))
|
player.media().startPaused(uri.toString().replaceFirst("file:", "file://"))
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
while (player.snapshots()?.get() == null && start + 5000 > System.currentTimeMillis()) {
|
var snap: BufferedImage? = null
|
||||||
|
while (snap == null && start + 5000 > System.currentTimeMillis()) {
|
||||||
|
snap = player.snapshots()?.get()
|
||||||
delay(10)
|
delay(10)
|
||||||
}
|
}
|
||||||
val preview = player.snapshots()?.get()?.toComposeImageBitmap()
|
val orientation = player.media().info().videoTracks().first().orientation()
|
||||||
|
val preview: ImageBitmap? = when (orientation) {
|
||||||
|
VideoOrientation.TOP_LEFT -> snap
|
||||||
|
VideoOrientation.TOP_RIGHT -> snap?.flip(false, true)
|
||||||
|
VideoOrientation.BOTTOM_LEFT -> snap?.flip(true, false)
|
||||||
|
VideoOrientation.BOTTOM_RIGHT -> snap?.rotate(180.0)
|
||||||
|
VideoOrientation.LEFT_TOP -> snap /* Transposed */
|
||||||
|
VideoOrientation.LEFT_BOTTOM -> snap?.rotate(-90.0)
|
||||||
|
VideoOrientation.RIGHT_TOP -> snap?.rotate(90.0)
|
||||||
|
VideoOrientation.RIGHT_BOTTOM -> snap /* Anti-transposed */
|
||||||
|
else -> snap
|
||||||
|
}?.toComposeImageBitmap()
|
||||||
val duration = player.duration.toLong()
|
val duration = player.duration.toLong()
|
||||||
CoroutineScope(Dispatchers.IO).launch { player.release() }
|
player.stop()
|
||||||
return VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration)
|
putHelperPlayer(mediaComponent)
|
||||||
|
return@withContext VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val playerThread = Executors.newSingleThreadExecutor()
|
||||||
|
private val playersPool: ArrayList<Component> = ArrayList()
|
||||||
|
private val helperPlayersPool: ArrayList<CallbackMediaPlayerComponent> = ArrayList()
|
||||||
|
|
||||||
|
private fun getOrCreatePlayer(): Component = playersPool.removeFirstOrNull() ?: createNew()
|
||||||
|
|
||||||
|
private fun createNew(): Component =
|
||||||
|
initializeMediaPlayerComponent().apply {
|
||||||
|
mediaPlayer().events().addMediaPlayerEventListener(object: MediaPlayerEventAdapter() {
|
||||||
|
override fun mediaPlayerReady(mediaPlayer: MediaPlayer?) {
|
||||||
|
playerThread.execute {
|
||||||
|
mediaPlayer?.audio()?.setVolume(100)
|
||||||
|
mediaPlayer?.audio()?.isMute = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stopped(mediaPlayer: MediaPlayer?) {
|
||||||
|
//playerThread.execute { mediaPlayer().videoSurface().set(null) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun putPlayer(player: Component) = playersPool.add(player)
|
||||||
|
|
||||||
|
private fun getOrCreateHelperPlayer(): CallbackMediaPlayerComponent = helperPlayersPool.removeFirstOrNull() ?: CallbackMediaPlayerComponent()
|
||||||
|
private fun putHelperPlayer(player: CallbackMediaPlayerComponent) = helperPlayersPool.add(player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,29 @@
|
|||||||
package chat.simplex.common.views.chat.item
|
package chat.simplex.common.views.chat.item
|
||||||
|
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import chat.simplex.common.platform.VideoPlayer
|
import chat.simplex.common.platform.VideoPlayer
|
||||||
|
import chat.simplex.common.platform.isPlaying
|
||||||
|
import chat.simplex.common.views.helpers.onRightClick
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {}
|
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {
|
||||||
|
Box {
|
||||||
|
SurfaceFromPlayer(player,
|
||||||
|
Modifier
|
||||||
|
.width(width)
|
||||||
|
.combinedClickable(
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
onClick = { if (player.player.isPlaying) stop() else onClick() }
|
||||||
|
)
|
||||||
|
.onRightClick(onLongClick)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun LocalWindowWidth(): Dp {
|
actual fun LocalWindowWidth(): Dp {
|
||||||
|
@ -6,17 +6,15 @@ import androidx.compose.material.*
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.awt.SwingPanel
|
|
||||||
import androidx.compose.ui.graphics.*
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.common.simplexWindowState
|
|
||||||
import chat.simplex.common.views.helpers.getBitmapFromByteArray
|
import chat.simplex.common.views.helpers.getBitmapFromByteArray
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.compose.painterResource
|
import dev.icerock.moko.resources.compose.painterResource
|
||||||
import dev.icerock.moko.resources.compose.stringResource
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
import kotlinx.coroutines.delay
|
import org.jetbrains.compose.videoplayer.SkiaBitmapVideoSurface
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -28,30 +26,40 @@ actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap:
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) {
|
actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) {
|
||||||
// Workaround. Without changing size of the window the screen flashes a lot even if it's not being recomposed
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = simplexWindowState.windowState.size.width + 1.dp)
|
|
||||||
delay(50)
|
|
||||||
player.play(true)
|
|
||||||
simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = simplexWindowState.windowState.size.width - 1.dp)
|
|
||||||
}
|
|
||||||
Box {
|
Box {
|
||||||
Box(Modifier.fillMaxSize().padding(bottom = 50.dp)) {
|
Box(Modifier.fillMaxSize().padding(bottom = 50.dp)) {
|
||||||
val factory = remember { { player.mediaPlayerComponent } }
|
SurfaceFromPlayer(player, modifier)
|
||||||
SwingPanel(
|
IconButton(onClick = close, Modifier.padding(top = 5.dp)) {
|
||||||
background = Color.Transparent,
|
Icon(painterResource(MR.images.ic_arrow_back_ios_new), null, Modifier.size(30.dp), tint = MaterialTheme.colors.primary)
|
||||||
modifier = Modifier,
|
|
||||||
factory = factory
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
Controls(player, close)
|
}
|
||||||
|
Controls(player)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BoxScope.Controls(player: VideoPlayer, close: () -> Unit) {
|
fun BoxScope.SurfaceFromPlayer(player: VideoPlayer, modifier: Modifier) {
|
||||||
|
val surface = remember {
|
||||||
|
SkiaBitmapVideoSurface().also {
|
||||||
|
player.player.videoSurface().set(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
surface.bitmap.value?.let { bitmap ->
|
||||||
|
Image(
|
||||||
|
bitmap,
|
||||||
|
modifier = modifier.align(Alignment.Center),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
alignment = Alignment.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.Controls(player: VideoPlayer) {
|
||||||
val playing = remember(player) { player.videoPlaying }
|
val playing = remember(player) { player.videoPlaying }
|
||||||
val progress = remember(player) { player.progress }
|
val progress = remember(player) { player.progress }
|
||||||
val duration = remember(player) { player.duration }
|
val duration = remember(player) { player.duration }
|
||||||
@ -62,10 +70,7 @@ private fun BoxScope.Controls(player: VideoPlayer, close: () -> Unit) {
|
|||||||
Slider(
|
Slider(
|
||||||
value = progress.value.toFloat() / max(0.0001f, duration.value.toFloat()),
|
value = progress.value.toFloat() / max(0.0001f, duration.value.toFloat()),
|
||||||
onValueChange = { player.player.seekTo((it * duration.value).toInt()) },
|
onValueChange = { player.player.seekTo((it * duration.value).toInt()) },
|
||||||
modifier = Modifier.fillMaxWidth().weight(1f)
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
IconButton(onClick = close,) {
|
|
||||||
Icon(painterResource(MR.images.ic_close), null, Modifier.size(30.dp), tint = MaterialTheme.colors.primary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,9 +38,15 @@ You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order t
|
|||||||
|
|
||||||
- `master` - branch for beta version releases (GHC 9.6.2).
|
- `master` - branch for beta version releases (GHC 9.6.2).
|
||||||
|
|
||||||
- `master-android` - used to build beta Android core library with Nix (GHC 8.10.7).
|
- `master-ghc8107` - branch for beta version releases (GHC 8.10.7).
|
||||||
|
|
||||||
- `master-ios` - used to build beta iOS core library with Nix (GHC 8.10.7) – this branch should be the same as `master-android` except Nix configuration files.
|
- `master-android` - used to build beta Android core library with Nix (GHC 8.10.7), same as `master-ghc8107`
|
||||||
|
|
||||||
|
- `master-ios` - used to build beta iOS core library with Nix (GHC 8.10.7).
|
||||||
|
|
||||||
|
- `windows-ghc8107` - branch for windows core library build (GHC 8.10.7).
|
||||||
|
|
||||||
|
`master-ios` and `windows-ghc8107` branches should be the same as `master-ghc8107` except Nix configuration files.
|
||||||
|
|
||||||
**In simplexmq repo**
|
**In simplexmq repo**
|
||||||
|
|
||||||
@ -54,24 +60,30 @@ You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order t
|
|||||||
|
|
||||||
2. If simplexmq repo was changed, to build mobile core libraries you need to merge its `master` branch into `master-ghc8107` branch.
|
2. If simplexmq repo was changed, to build mobile core libraries you need to merge its `master` branch into `master-ghc8107` branch.
|
||||||
|
|
||||||
3. To build Android core library:
|
3. To build core libraries for Android, iOS and windows:
|
||||||
- merge `master` branch to `master-android` branch.
|
- merge `master` branch to `master-ghc8107` branch.
|
||||||
|
- update `simplexmq` commit in `master-ghc8107` branch to the commit in `master-ghc8107` branch (probably, when resolving merge conflicts).
|
||||||
- update code to be compatible with GHC 8.10.7 (see below).
|
- update code to be compatible with GHC 8.10.7 (see below).
|
||||||
- update `simplexmq` commit in `master-android` branch to the commit in `master-ghc8107` branch.
|
|
||||||
- push to GitHub.
|
- push to GitHub.
|
||||||
|
|
||||||
4. To build iOS core library, merge `master-android` branch to `master-ios` branch, and push to GitHub.
|
4. To build Android core library, merge `master-ghc8107` branch to `master-android` branch, and push to GitHub.
|
||||||
|
|
||||||
5. To build Desktop and CLI apps, make tag in `master` branch, APK files should be attached to the release.
|
5. To build iOS core library, merge `master-ghc8107` branch to `master-ios` branch, and push to GitHub.
|
||||||
|
|
||||||
6. After the public release to App Store and Play Store, merge:
|
6. To build windows core library, merge `master-ghc8107` branch to `windows-ghc8107` branch, and push to GitHub.
|
||||||
|
|
||||||
|
7. To build Desktop and CLI apps, make tag in `master` branch, APK files should be attached to the release.
|
||||||
|
|
||||||
|
8. After the public release to App Store and Play Store, merge:
|
||||||
- `master` to `stable`
|
- `master` to `stable`
|
||||||
- `master` to `master-android` (and compile/update code)
|
- `master` to `master-ghc8107` (and compile/update code)
|
||||||
- `master-android` to `master-ios`
|
- `master-ghc8107` to `master-android`
|
||||||
|
- `master-ghc8107` to `master-ios`
|
||||||
|
- `master-ghc8107` to `windows-ghc8107`
|
||||||
- `master-android` to `stable-android`
|
- `master-android` to `stable-android`
|
||||||
- `master-ios` to `stable-ios`
|
- `master-ios` to `stable-ios`
|
||||||
|
|
||||||
7. Independently, `master` branch of simplexmq repo should be merged to `stable` branch on stable releases.
|
9. Independently, `master` branch of simplexmq repo should be merged to `stable` branch on stable releases.
|
||||||
|
|
||||||
|
|
||||||
## Differences between GHC 8.10.7 and GHC 9.6.2
|
## Differences between GHC 8.10.7 and GHC 9.6.2
|
||||||
|
@ -15,31 +15,30 @@ We want to add up to 3 people to the team.
|
|||||||
|
|
||||||
## Who we are looking for
|
## Who we are looking for
|
||||||
|
|
||||||
### Systems Haskell engineer
|
### Application Haskell engineer
|
||||||
|
|
||||||
You are a servers/network/Haskell expert:
|
You are an expert in language models, databases and Haskell:
|
||||||
- network libraries.
|
- expert knowledge of SQL.
|
||||||
- exception handling, concurrency, STM.
|
- exception handling, concurrency, STM.
|
||||||
- type systems - we use ad hoc dependent types a lot.
|
- type systems - we use ad hoc dependent types a lot.
|
||||||
- strictness.
|
- experience integrating open-source language models.
|
||||||
- has some expertise in network protocols, cryptography and general information security principles and approaches.
|
- experience developing community-centric applications.
|
||||||
- interested to build the next generation of messaging network.
|
- interested to build the next generation of messaging network.
|
||||||
|
|
||||||
You will be focussed mostly on our servers code, and will also contribute to the core client code written in Haskell.
|
You will be focussed mostly on our client applications, and will also contribute to the servers also written in Haskell.
|
||||||
|
|
||||||
|
### iOS / Mac engineer
|
||||||
|
|
||||||
### Product engineer (iOS)
|
You are an expert in Apple platforms, including:
|
||||||
|
- iOS and Mac platform architecture.
|
||||||
You are a product UX expert who designs great user experiences directly in iOS code:
|
- Swift and Objective-C.
|
||||||
- iOS and Mac platforms, including:
|
- SwiftUI and UIKit.
|
||||||
- SwiftUI and UIKit.
|
- extensions, including notification service extension and sharing extension.
|
||||||
- extensions, including notification service extension and sharing extension.
|
- low level inter-process communication primitives for concurrency.
|
||||||
- low level inter-process communication primitives for concurrency.
|
|
||||||
- interested about creating the next generation of UX for a communication/social network.
|
- interested about creating the next generation of UX for a communication/social network.
|
||||||
|
|
||||||
Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin Jetpack Compose for our Android and desktop apps.
|
Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin Jetpack Compose for our Android and desktop apps.
|
||||||
|
|
||||||
|
|
||||||
## About you
|
## About you
|
||||||
|
|
||||||
- **Passionate about joining SimpleX Chat team**:
|
- **Passionate about joining SimpleX Chat team**:
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
OS=mac
|
OS=mac
|
||||||
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
|
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
|
||||||
GHC_VERSION=9.6.2
|
GHC_VERSION=9.6.2
|
||||||
@ -18,7 +20,7 @@ rm -rf $BUILD_DIR
|
|||||||
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi"
|
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi"
|
||||||
|
|
||||||
cd $BUILD_DIR/build
|
cd $BUILD_DIR/build
|
||||||
mkdir deps 2> /dev/null
|
mkdir deps 2> /dev/null || true
|
||||||
|
|
||||||
# It's not included by default for some reason. Compiled lib tries to find system one but it's not always available
|
# It's not included by default for some reason. Compiled lib tries to find system one but it's not always available
|
||||||
#cp $GHC_LIBS_DIR/libffi.dylib ./deps
|
#cp $GHC_LIBS_DIR/libffi.dylib ./deps
|
||||||
@ -79,13 +81,6 @@ copy_deps $LIB
|
|||||||
cp $(ghc --print-libdir)/$ARCH-osx-ghc-$GHC_VERSION/libHSghc-boot-th-$GHC_VERSION-ghc$GHC_VERSION.dylib deps
|
cp $(ghc --print-libdir)/$ARCH-osx-ghc-$GHC_VERSION/libHSghc-boot-th-$GHC_VERSION-ghc$GHC_VERSION.dylib deps
|
||||||
rm deps/`basename $LIB`
|
rm deps/`basename $LIB`
|
||||||
|
|
||||||
if [ -e deps/libHSdrct-*.$LIB_EXT ]; then
|
|
||||||
LIBCRYPTO_PATH=$(otool -l deps/libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11)
|
|
||||||
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSdrct-*.$LIB_EXT
|
|
||||||
cp $LIBCRYPTO_PATH deps/libcrypto.1.1.$LIB_EXT
|
|
||||||
chmod 755 deps/libcrypto.1.1.$LIB_EXT
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd -
|
cd -
|
||||||
|
|
||||||
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
@ -95,4 +90,39 @@ rm -rf apps/multiplatform/desktop/build/cmake
|
|||||||
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
|
|
||||||
|
cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
|
||||||
|
|
||||||
|
LIBCRYPTO_PATH=$(otool -l deps/libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11)
|
||||||
|
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSdrct-*.$LIB_EXT
|
||||||
|
cp $LIBCRYPTO_PATH deps/libcrypto.1.1.$LIB_EXT
|
||||||
|
chmod 755 deps/libcrypto.1.1.$LIB_EXT
|
||||||
|
install_name_tool -id "libcrypto.1.1.$LIB_EXT" deps/libcrypto.1.1.$LIB_EXT
|
||||||
|
install_name_tool -id "libffi.8.$LIB_EXT" deps/libffi.$LIB_EXT
|
||||||
|
|
||||||
|
LIBCRYPTO_PATH=$(otool -l $LIB | grep libcrypto | cut -d' ' -f11)
|
||||||
|
if [ -n "$LIBCRYPTO_PATH" ]; then
|
||||||
|
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT $LIB
|
||||||
|
fi
|
||||||
|
|
||||||
|
LIBCRYPTO_PATH=$(otool -l deps/libHSsmplxmq*.$LIB_EXT | grep libcrypto | cut -d' ' -f11)
|
||||||
|
if [ -n "$LIBCRYPTO_PATH" ]; then
|
||||||
|
install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSsmplxmq*.$LIB_EXT
|
||||||
|
fi
|
||||||
|
|
||||||
|
for lib in $(find . -type f -name "*.$LIB_EXT"); do
|
||||||
|
RPATHS=`otool -l $lib | grep -E "path /Users/|path /usr/local|path /opt/" | cut -d' ' -f11`
|
||||||
|
for RPATH in $RPATHS; do
|
||||||
|
install_name_tool -delete_rpath $RPATH $lib
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
LOCAL_DIRS=`for lib in $(find . -type f -name "*.$LIB_EXT"); do otool -l $lib | grep -E "/Users|/opt/|/usr/local" && echo $lib || true; done`
|
||||||
|
if [ -n "$LOCAL_DIRS" ]; then
|
||||||
|
echo These libs still point to local directories:
|
||||||
|
echo $LOCAL_DIRS
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd -
|
||||||
scripts/desktop/prepare-vlc-mac.sh
|
scripts/desktop/prepare-vlc-mac.sh
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."8d47f690838371bc848e4b31a4b09ef6bf67ccc5" = "1pwasv22ii3wy4xchaknlwczmy5ws7adx7gg2g58lxzrgdjm3650";
|
"https://github.com/simplex-chat/simplexmq.git"."ec1b72cb8013a65a5d9783104a47ae44f5730089" = "1lz5rvgxp242zg95r9zd9j50y45314cf8nfpjg1qsa55nrk2w19b";
|
||||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||||
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
|
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
|
||||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp";
|
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp";
|
||||||
|
@ -114,6 +114,7 @@ library
|
|||||||
Simplex.Chat.Migrations.M20230913_member_contacts
|
Simplex.Chat.Migrations.M20230913_member_contacts
|
||||||
Simplex.Chat.Migrations.M20230914_member_probes
|
Simplex.Chat.Migrations.M20230914_member_probes
|
||||||
Simplex.Chat.Migrations.M20230922_remote_controller
|
Simplex.Chat.Migrations.M20230922_remote_controller
|
||||||
|
Simplex.Chat.Migrations.M20230926_contact_status
|
||||||
Simplex.Chat.Mobile
|
Simplex.Chat.Mobile
|
||||||
Simplex.Chat.Mobile.File
|
Simplex.Chat.Mobile.File
|
||||||
Simplex.Chat.Mobile.Shared
|
Simplex.Chat.Mobile.Shared
|
||||||
|
@ -901,14 +901,15 @@ processChatCommand = \case
|
|||||||
liftIO $ updateGroupUnreadChat db user groupInfo unreadChat
|
liftIO $ updateGroupUnreadChat db user groupInfo unreadChat
|
||||||
ok user
|
ok user
|
||||||
_ -> pure $ chatCmdError (Just user) "not supported"
|
_ -> pure $ chatCmdError (Just user) "not supported"
|
||||||
APIDeleteChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of
|
APIDeleteChat (ChatRef cType chatId) notify -> withUser $ \user@User {userId} -> case cType of
|
||||||
CTDirect -> do
|
CTDirect -> do
|
||||||
ct@Contact {localDisplayName} <- withStore $ \db -> getContact db user chatId
|
ct@Contact {localDisplayName} <- withStore $ \db -> getContact db user chatId
|
||||||
filesInfo <- withStore' $ \db -> getContactFileInfo db user ct
|
filesInfo <- withStore' $ \db -> getContactFileInfo db user ct
|
||||||
contactConnIds <- map aConnId <$> withStore (\db -> getContactConnections db userId ct)
|
|
||||||
withChatLock "deleteChat direct" . procCmd $ do
|
withChatLock "deleteChat direct" . procCmd $ do
|
||||||
fileAgentConnIds <- concat <$> forM filesInfo (deleteFile user)
|
deleteFilesAndConns user filesInfo
|
||||||
deleteAgentConnectionsAsync user $ fileAgentConnIds <> contactConnIds
|
when (contactActive ct && notify) . void $ sendDirectContactMessage ct XDirectDel
|
||||||
|
contactConnIds <- map aConnId <$> withStore (\db -> getContactConnections db userId ct)
|
||||||
|
deleteAgentConnectionsAsync user contactConnIds
|
||||||
-- functions below are called in separate transactions to prevent crashes on android
|
-- functions below are called in separate transactions to prevent crashes on android
|
||||||
-- (possibly, race condition on integrity check?)
|
-- (possibly, race condition on integrity check?)
|
||||||
withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct
|
withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct
|
||||||
@ -1331,7 +1332,7 @@ processChatCommand = \case
|
|||||||
ConnectSimplex incognito -> withUser $ \user ->
|
ConnectSimplex incognito -> withUser $ \user ->
|
||||||
-- [incognito] generate profile to send
|
-- [incognito] generate profile to send
|
||||||
connectViaContact user incognito adminContactReq
|
connectViaContact user incognito adminContactReq
|
||||||
DeleteContact cName -> withContactName cName $ APIDeleteChat . ChatRef CTDirect
|
DeleteContact cName -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) True
|
||||||
ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect
|
ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect
|
||||||
APIListContacts userId -> withUserId userId $ \user ->
|
APIListContacts userId -> withUserId userId $ \user ->
|
||||||
CRContactsList user <$> withStore' (`getUserContacts` user)
|
CRContactsList user <$> withStore' (`getUserContacts` user)
|
||||||
@ -1426,7 +1427,7 @@ processChatCommand = \case
|
|||||||
processChatCommand . APISendMessage chatRef True Nothing $ ComposedMessage Nothing Nothing mc
|
processChatCommand . APISendMessage chatRef True Nothing $ ComposedMessage Nothing Nothing mc
|
||||||
SendMessageBroadcast msg -> withUser $ \user -> do
|
SendMessageBroadcast msg -> withUser $ \user -> do
|
||||||
contacts <- withStore' (`getUserContacts` user)
|
contacts <- withStore' (`getUserContacts` user)
|
||||||
let cts = filter (\ct -> isReady ct && directOrUsed ct) contacts
|
let cts = filter (\ct -> isReady ct && contactActive ct && directOrUsed ct) contacts
|
||||||
ChatConfig {logLevel} <- asks config
|
ChatConfig {logLevel} <- asks config
|
||||||
withChatLock "sendMessageBroadcast" . procCmd $ do
|
withChatLock "sendMessageBroadcast" . procCmd $ do
|
||||||
(successes, failures) <- foldM (sendAndCount user logLevel) (0, 0) cts
|
(successes, failures) <- foldM (sendAndCount user logLevel) (0, 0) cts
|
||||||
@ -1594,7 +1595,7 @@ processChatCommand = \case
|
|||||||
processChatCommand $ APILeaveGroup groupId
|
processChatCommand $ APILeaveGroup groupId
|
||||||
DeleteGroup gName -> withUser $ \user -> do
|
DeleteGroup gName -> withUser $ \user -> do
|
||||||
groupId <- withStore $ \db -> getGroupIdByName db user gName
|
groupId <- withStore $ \db -> getGroupIdByName db user gName
|
||||||
processChatCommand $ APIDeleteChat (ChatRef CTGroup groupId)
|
processChatCommand $ APIDeleteChat (ChatRef CTGroup groupId) True
|
||||||
ClearGroup gName -> withUser $ \user -> do
|
ClearGroup gName -> withUser $ \user -> do
|
||||||
groupId <- withStore $ \db -> getGroupIdByName db user gName
|
groupId <- withStore $ \db -> getGroupIdByName db user gName
|
||||||
processChatCommand $ APIClearChat (ChatRef CTGroup groupId)
|
processChatCommand $ APIClearChat (ChatRef CTGroup groupId)
|
||||||
@ -1988,7 +1989,7 @@ processChatCommand = \case
|
|||||||
-- read contacts before user update to correctly merge preferences
|
-- read contacts before user update to correctly merge preferences
|
||||||
-- [incognito] filter out contacts with whom user has incognito connections
|
-- [incognito] filter out contacts with whom user has incognito connections
|
||||||
contacts <-
|
contacts <-
|
||||||
filter (\ct -> isReady ct && not (contactConnIncognito ct))
|
filter (\ct -> isReady ct && contactActive ct && not (contactConnIncognito ct))
|
||||||
<$> withStore' (`getUserContacts` user)
|
<$> withStore' (`getUserContacts` user)
|
||||||
user' <- updateUser
|
user' <- updateUser
|
||||||
asks currentUser >>= atomically . (`writeTVar` Just user')
|
asks currentUser >>= atomically . (`writeTVar` Just user')
|
||||||
@ -2574,7 +2575,7 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do
|
|||||||
getContactConns :: m ([ConnId], Map ConnId Contact)
|
getContactConns :: m ([ConnId], Map ConnId Contact)
|
||||||
getContactConns = do
|
getContactConns = do
|
||||||
cts <- withStore_ ("subscribeUserConnections " <> show userId <> ", getUserContacts") getUserContacts
|
cts <- withStore_ ("subscribeUserConnections " <> show userId <> ", getUserContacts") getUserContacts
|
||||||
let connIds = map contactConnId cts
|
let connIds = map contactConnId (filter contactActive cts)
|
||||||
pure (connIds, M.fromList $ zip connIds cts)
|
pure (connIds, M.fromList $ zip connIds cts)
|
||||||
getUserContactLinkConns :: m ([ConnId], Map ConnId UserContact)
|
getUserContactLinkConns :: m ([ConnId], Map ConnId UserContact)
|
||||||
getUserContactLinkConns = do
|
getUserContactLinkConns = do
|
||||||
@ -2584,7 +2585,7 @@ subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do
|
|||||||
getGroupMemberConns :: m ([Group], [ConnId], Map ConnId GroupMember)
|
getGroupMemberConns :: m ([Group], [ConnId], Map ConnId GroupMember)
|
||||||
getGroupMemberConns = do
|
getGroupMemberConns = do
|
||||||
gs <- withStore_ ("subscribeUserConnections " <> show userId <> ", getUserGroups") getUserGroups
|
gs <- withStore_ ("subscribeUserConnections " <> show userId <> ", getUserGroups") getUserGroups
|
||||||
let mPairs = concatMap (\(Group _ ms) -> mapMaybe (\m -> (,m) <$> memberConnId m) ms) gs
|
let mPairs = concatMap (\(Group _ ms) -> mapMaybe (\m -> (,m) <$> memberConnId m) (filter (not . memberRemoved) ms)) gs
|
||||||
pure (gs, map fst mPairs, M.fromList mPairs)
|
pure (gs, map fst mPairs, M.fromList mPairs)
|
||||||
getSndFileTransferConns :: m ([ConnId], Map ConnId SndFileTransfer)
|
getSndFileTransferConns :: m ([ConnId], Map ConnId SndFileTransfer)
|
||||||
getSndFileTransferConns = do
|
getSndFileTransferConns = do
|
||||||
@ -3050,6 +3051,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||||||
XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta
|
XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta
|
||||||
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta
|
XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta
|
||||||
XInfo p -> xInfo ct' p
|
XInfo p -> xInfo ct' p
|
||||||
|
XDirectDel -> xDirectDel ct' msg msgMeta
|
||||||
XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta
|
XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta
|
||||||
XInfoProbe probe -> xInfoProbe (CGMContact ct') probe
|
XInfoProbe probe -> xInfoProbe (CGMContact ct') probe
|
||||||
XInfoProbeCheck probeHash -> xInfoProbeCheck ct' probeHash
|
XInfoProbeCheck probeHash -> xInfoProbeCheck ct' probeHash
|
||||||
@ -4254,6 +4256,24 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
|||||||
xInfo :: Contact -> Profile -> m ()
|
xInfo :: Contact -> Profile -> m ()
|
||||||
xInfo c p' = void $ processContactProfileUpdate c p' True
|
xInfo c p' = void $ processContactProfileUpdate c p' True
|
||||||
|
|
||||||
|
xDirectDel :: Contact -> RcvMessage -> MsgMeta -> m ()
|
||||||
|
xDirectDel c msg msgMeta =
|
||||||
|
if directOrUsed c
|
||||||
|
then do
|
||||||
|
checkIntegrityCreateItem (CDDirectRcv c) msgMeta
|
||||||
|
ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted
|
||||||
|
contactConns <- withStore $ \db -> getContactConnections db userId ct'
|
||||||
|
deleteAgentConnectionsAsync user $ map aConnId contactConns
|
||||||
|
forM_ contactConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted
|
||||||
|
let ct'' = ct' {activeConn = (contactConn ct') {connStatus = ConnDeleted}} :: Contact
|
||||||
|
ci <- saveRcvChatItem user (CDDirectRcv ct'') msg msgMeta (CIRcvDirectEvent RDEContactDeleted)
|
||||||
|
toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct'') ci)
|
||||||
|
toView $ CRContactDeletedByContact user ct''
|
||||||
|
else do
|
||||||
|
contactConns <- withStore $ \db -> getContactConnections db userId c
|
||||||
|
deleteAgentConnectionsAsync user $ map aConnId contactConns
|
||||||
|
withStore' $ \db -> deleteContact db user c
|
||||||
|
|
||||||
processContactProfileUpdate :: Contact -> Profile -> Bool -> m Contact
|
processContactProfileUpdate :: Contact -> Profile -> Bool -> m Contact
|
||||||
processContactProfileUpdate c@Contact {profile = p} p' createItems
|
processContactProfileUpdate c@Contact {profile = p} p' createItems
|
||||||
| fromLocalProfile p /= p' = do
|
| fromLocalProfile p /= p' = do
|
||||||
@ -4937,8 +4957,9 @@ deleteOrUpdateMemberRecord user@User {userId} member =
|
|||||||
Nothing -> deleteGroupMember db user member
|
Nothing -> deleteGroupMember db user member
|
||||||
|
|
||||||
sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64)
|
sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64)
|
||||||
sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connStatus}} chatMsgEvent
|
sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connStatus}, contactStatus} chatMsgEvent
|
||||||
| connStatus /= ConnReady && connStatus /= ConnSndReady = throwChatError $ CEContactNotReady ct
|
| connStatus /= ConnReady && connStatus /= ConnSndReady = throwChatError $ CEContactNotReady ct
|
||||||
|
| contactStatus /= CSActive = throwChatError $ CEContactNotActive ct
|
||||||
| connDisabled conn = throwChatError $ CEContactDisabled ct
|
| connDisabled conn = throwChatError $ CEContactDisabled ct
|
||||||
| otherwise = sendDirectMessage conn chatMsgEvent (ConnectionId connId)
|
| otherwise = sendDirectMessage conn chatMsgEvent (ConnectionId connId)
|
||||||
|
|
||||||
@ -5400,7 +5421,7 @@ chatCommandP =
|
|||||||
"/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP),
|
"/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP),
|
||||||
"/_read chat " *> (APIChatRead <$> chatRefP <*> optional (A.space *> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))),
|
"/_read chat " *> (APIChatRead <$> chatRefP <*> optional (A.space *> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))),
|
||||||
"/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP),
|
"/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP),
|
||||||
"/_delete " *> (APIDeleteChat <$> chatRefP),
|
"/_delete " *> (APIDeleteChat <$> chatRefP <*> (A.space *> "notify=" *> onOffP <|> pure True)),
|
||||||
"/_clear chat " *> (APIClearChat <$> chatRefP),
|
"/_clear chat " *> (APIClearChat <$> chatRefP),
|
||||||
"/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal),
|
"/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal),
|
||||||
"/_reject " *> (APIRejectContact <$> A.decimal),
|
"/_reject " *> (APIRejectContact <$> A.decimal),
|
||||||
|
@ -21,7 +21,7 @@ import qualified Data.Text as T
|
|||||||
import qualified Database.SQLite3 as SQL
|
import qualified Database.SQLite3 as SQL
|
||||||
import Simplex.Chat.Controller
|
import Simplex.Chat.Controller
|
||||||
import Simplex.Messaging.Agent.Client (agentClientStore)
|
import Simplex.Messaging.Agent.Client (agentClientStore)
|
||||||
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), sqlString)
|
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), sqlString, closeSQLiteStore)
|
||||||
import Simplex.Messaging.Util
|
import Simplex.Messaging.Util
|
||||||
import System.FilePath
|
import System.FilePath
|
||||||
import UnliftIO.Directory
|
import UnliftIO.Directory
|
||||||
@ -42,9 +42,9 @@ archiveFilesFolder = "simplex_v1_files"
|
|||||||
exportArchive :: ChatMonad m => ArchiveConfig -> m ()
|
exportArchive :: ChatMonad m => ArchiveConfig -> m ()
|
||||||
exportArchive cfg@ArchiveConfig {archivePath, disableCompression} =
|
exportArchive cfg@ArchiveConfig {archivePath, disableCompression} =
|
||||||
withTempDir cfg "simplex-chat." $ \dir -> do
|
withTempDir cfg "simplex-chat." $ \dir -> do
|
||||||
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
StorageFiles {chatStore, agentStore, filesPath} <- storageFiles
|
||||||
copyFile chatDb $ dir </> archiveChatDbFile
|
copyFile (dbFilePath chatStore) $ dir </> archiveChatDbFile
|
||||||
copyFile agentDb $ dir </> archiveAgentDbFile
|
copyFile (dbFilePath agentStore) $ dir </> archiveAgentDbFile
|
||||||
forM_ filesPath $ \fp ->
|
forM_ filesPath $ \fp ->
|
||||||
copyDirectoryFiles fp $ dir </> archiveFilesFolder
|
copyDirectoryFiles fp $ dir </> archiveFilesFolder
|
||||||
let method = if disableCompression == Just True then Z.Store else Z.Deflate
|
let method = if disableCompression == Just True then Z.Store else Z.Deflate
|
||||||
@ -54,11 +54,11 @@ importArchive :: ChatMonad m => ArchiveConfig -> m [ArchiveError]
|
|||||||
importArchive cfg@ArchiveConfig {archivePath} =
|
importArchive cfg@ArchiveConfig {archivePath} =
|
||||||
withTempDir cfg "simplex-chat." $ \dir -> do
|
withTempDir cfg "simplex-chat." $ \dir -> do
|
||||||
Z.withArchive archivePath $ Z.unpackInto dir
|
Z.withArchive archivePath $ Z.unpackInto dir
|
||||||
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
fs@StorageFiles {chatStore, agentStore, filesPath} <- storageFiles
|
||||||
backup chatDb
|
liftIO $ closeSQLiteStore `withStores` fs
|
||||||
backup agentDb
|
backup `withDBs` fs
|
||||||
copyFile (dir </> archiveChatDbFile) chatDb
|
copyFile (dir </> archiveChatDbFile) $ dbFilePath chatStore
|
||||||
copyFile (dir </> archiveAgentDbFile) agentDb
|
copyFile (dir </> archiveAgentDbFile) $ dbFilePath agentStore
|
||||||
copyFiles dir filesPath
|
copyFiles dir filesPath
|
||||||
`E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e]
|
`E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e]
|
||||||
where
|
where
|
||||||
@ -94,53 +94,60 @@ copyDirectoryFiles fromDir toDir = do
|
|||||||
|
|
||||||
deleteStorage :: ChatMonad m => m ()
|
deleteStorage :: ChatMonad m => m ()
|
||||||
deleteStorage = do
|
deleteStorage = do
|
||||||
StorageFiles {chatDb, agentDb, filesPath} <- storageFiles
|
fs <- storageFiles
|
||||||
removeFile chatDb
|
liftIO $ closeSQLiteStore `withStores` fs
|
||||||
removeFile agentDb
|
remove `withDBs` fs
|
||||||
mapM_ removePathForcibly filesPath
|
mapM_ removeDir $ filesPath fs
|
||||||
tmpPath <- readTVarIO =<< asks tempDirectory
|
mapM_ removeDir =<< chatReadVar tempDirectory
|
||||||
mapM_ removePathForcibly tmpPath
|
where
|
||||||
|
remove f = whenM (doesFileExist f) $ removeFile f
|
||||||
|
removeDir d = whenM (doesDirectoryExist d) $ removePathForcibly d
|
||||||
|
|
||||||
data StorageFiles = StorageFiles
|
data StorageFiles = StorageFiles
|
||||||
{ chatDb :: FilePath,
|
{ chatStore :: SQLiteStore,
|
||||||
chatEncrypted :: TVar Bool,
|
agentStore :: SQLiteStore,
|
||||||
agentDb :: FilePath,
|
|
||||||
agentEncrypted :: TVar Bool,
|
|
||||||
filesPath :: Maybe FilePath
|
filesPath :: Maybe FilePath
|
||||||
}
|
}
|
||||||
|
|
||||||
storageFiles :: ChatMonad m => m StorageFiles
|
storageFiles :: ChatMonad m => m StorageFiles
|
||||||
storageFiles = do
|
storageFiles = do
|
||||||
ChatController {chatStore, filesFolder, smpAgent} <- ask
|
ChatController {chatStore, filesFolder, smpAgent} <- ask
|
||||||
let SQLiteStore {dbFilePath = chatDb, dbEncrypted = chatEncrypted} = chatStore
|
let agentStore = agentClientStore smpAgent
|
||||||
SQLiteStore {dbFilePath = agentDb, dbEncrypted = agentEncrypted} = agentClientStore smpAgent
|
|
||||||
filesPath <- readTVarIO filesFolder
|
filesPath <- readTVarIO filesFolder
|
||||||
pure StorageFiles {chatDb, chatEncrypted, agentDb, agentEncrypted, filesPath}
|
pure StorageFiles {chatStore, agentStore, filesPath}
|
||||||
|
|
||||||
sqlCipherExport :: forall m. ChatMonad m => DBEncryptionConfig -> m ()
|
sqlCipherExport :: forall m. ChatMonad m => DBEncryptionConfig -> m ()
|
||||||
sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = DBEncryptionKey key'} =
|
sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = DBEncryptionKey key'} =
|
||||||
when (key /= key') $ do
|
when (key /= key') $ do
|
||||||
fs@StorageFiles {chatDb, chatEncrypted, agentDb, agentEncrypted} <- storageFiles
|
fs <- storageFiles
|
||||||
checkFile `with` fs
|
checkFile `withDBs` fs
|
||||||
backup `with` fs
|
backup `withDBs` fs
|
||||||
(export chatDb chatEncrypted >> export agentDb agentEncrypted)
|
checkEncryption `withStores` fs
|
||||||
`catchChatError` \e -> (restore `with` fs) >> throwError e
|
removeExported `withDBs` fs
|
||||||
|
export `withDBs` fs
|
||||||
|
-- closing after encryption prevents closing in case wrong encryption key was passed
|
||||||
|
liftIO $ closeSQLiteStore `withStores` fs
|
||||||
|
(moveExported `withStores` fs)
|
||||||
|
`catchChatError` \e -> (restore `withDBs` fs) >> throwError e
|
||||||
where
|
where
|
||||||
action `with` StorageFiles {chatDb, agentDb} = action chatDb >> action agentDb
|
|
||||||
backup f = copyFile f (f <> ".bak")
|
backup f = copyFile f (f <> ".bak")
|
||||||
restore f = copyFile (f <> ".bak") f
|
restore f = copyFile (f <> ".bak") f
|
||||||
checkFile f = unlessM (doesFileExist f) $ throwDBError $ DBErrorNoFile f
|
checkFile f = unlessM (doesFileExist f) $ throwDBError $ DBErrorNoFile f
|
||||||
export f dbEnc = do
|
checkEncryption SQLiteStore {dbEncrypted} = do
|
||||||
enc <- readTVarIO dbEnc
|
enc <- readTVarIO dbEncrypted
|
||||||
when (enc && null key) $ throwDBError DBErrorEncrypted
|
when (enc && null key) $ throwDBError DBErrorEncrypted
|
||||||
when (not enc && not (null key)) $ throwDBError DBErrorPlaintext
|
when (not enc && not (null key)) $ throwDBError DBErrorPlaintext
|
||||||
withDB (`SQL.exec` exportSQL) DBErrorExport
|
exported = (<> ".exported")
|
||||||
renameFile (f <> ".exported") f
|
removeExported f = whenM (doesFileExist $ exported f) $ removeFile (exported f)
|
||||||
withDB (`SQL.exec` testSQL) DBErrorOpen
|
moveExported SQLiteStore {dbFilePath = f, dbEncrypted} = do
|
||||||
atomically $ writeTVar dbEnc $ not (null key')
|
renameFile (exported f) f
|
||||||
|
atomically $ writeTVar dbEncrypted $ not (null key')
|
||||||
|
export f = do
|
||||||
|
withDB f (`SQL.exec` exportSQL) DBErrorExport
|
||||||
|
withDB (exported f) (`SQL.exec` testSQL) DBErrorOpen
|
||||||
where
|
where
|
||||||
withDB a err =
|
withDB f' a err =
|
||||||
liftIO (bracket (SQL.open $ T.pack f) SQL.close a $> Nothing)
|
liftIO (bracket (SQL.open $ T.pack f') SQL.close a $> Nothing)
|
||||||
`catch` checkSQLError
|
`catch` checkSQLError
|
||||||
`catch` (\(e :: SomeException) -> sqliteError' e)
|
`catch` (\(e :: SomeException) -> sqliteError' e)
|
||||||
>>= mapM_ (throwDBError . err)
|
>>= mapM_ (throwDBError . err)
|
||||||
@ -162,7 +169,12 @@ sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = D
|
|||||||
keySQL key'
|
keySQL key'
|
||||||
<> [ "PRAGMA foreign_keys = ON;",
|
<> [ "PRAGMA foreign_keys = ON;",
|
||||||
"PRAGMA secure_delete = ON;",
|
"PRAGMA secure_delete = ON;",
|
||||||
"PRAGMA auto_vacuum = FULL;",
|
|
||||||
"SELECT count(*) FROM sqlite_master;"
|
"SELECT count(*) FROM sqlite_master;"
|
||||||
]
|
]
|
||||||
keySQL k = ["PRAGMA key = " <> sqlString k <> ";" | not (null k)]
|
keySQL k = ["PRAGMA key = " <> sqlString k <> ";" | not (null k)]
|
||||||
|
|
||||||
|
withDBs :: Monad m => (FilePath -> m b) -> StorageFiles -> m b
|
||||||
|
action `withDBs` StorageFiles {chatStore, agentStore} = action (dbFilePath chatStore) >> action (dbFilePath agentStore)
|
||||||
|
|
||||||
|
withStores :: Monad m => (SQLiteStore -> m b) -> StorageFiles -> m b
|
||||||
|
action `withStores` StorageFiles {chatStore, agentStore} = action chatStore >> action agentStore
|
||||||
|
@ -55,9 +55,8 @@ import Simplex.Messaging.Agent.Client (AgentLocks, ProtocolTestFailure)
|
|||||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig)
|
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig)
|
||||||
import Simplex.Messaging.Agent.Lock
|
import Simplex.Messaging.Agent.Lock
|
||||||
import Simplex.Messaging.Agent.Protocol
|
import Simplex.Messaging.Agent.Protocol
|
||||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration)
|
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction)
|
||||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||||
import Simplex.Messaging.Agent.Store.SQLite.Common (withTransaction)
|
|
||||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||||
import qualified Simplex.Messaging.Crypto as C
|
import qualified Simplex.Messaging.Crypto as C
|
||||||
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||||
@ -253,7 +252,7 @@ data ChatCommand
|
|||||||
| APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction}
|
| APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction}
|
||||||
| APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId))
|
| APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId))
|
||||||
| APIChatUnread ChatRef Bool
|
| APIChatUnread ChatRef Bool
|
||||||
| APIDeleteChat ChatRef
|
| APIDeleteChat ChatRef Bool -- `notify` flag is only applied to direct chats
|
||||||
| APIClearChat ChatRef
|
| APIClearChat ChatRef
|
||||||
| APIAcceptContact IncognitoEnabled Int64
|
| APIAcceptContact IncognitoEnabled Int64
|
||||||
| APIRejectContact Int64
|
| APIRejectContact Int64
|
||||||
@ -508,6 +507,7 @@ data ChatResponse
|
|||||||
| CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
|
| CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
|
||||||
| CRContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact}
|
| CRContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact}
|
||||||
| CRContactDeleted {user :: User, contact :: Contact}
|
| CRContactDeleted {user :: User, contact :: Contact}
|
||||||
|
| CRContactDeletedByContact {user :: User, contact :: Contact}
|
||||||
| CRChatCleared {user :: User, chatInfo :: AChatInfo}
|
| CRChatCleared {user :: User, chatInfo :: AChatInfo}
|
||||||
| CRUserContactLinkCreated {user :: User, connReqContact :: ConnReqContact}
|
| CRUserContactLinkCreated {user :: User, connReqContact :: ConnReqContact}
|
||||||
| CRUserContactLinkDeleted {user :: User}
|
| CRUserContactLinkDeleted {user :: User}
|
||||||
@ -944,6 +944,7 @@ data ChatErrorType
|
|||||||
| CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String}
|
| CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String}
|
||||||
| CEContactNotFound {contactName :: ContactName, suspectedMember :: Maybe (GroupInfo, GroupMember)}
|
| CEContactNotFound {contactName :: ContactName, suspectedMember :: Maybe (GroupInfo, GroupMember)}
|
||||||
| CEContactNotReady {contact :: Contact}
|
| CEContactNotReady {contact :: Contact}
|
||||||
|
| CEContactNotActive {contact :: Contact}
|
||||||
| CEContactDisabled {contact :: Contact}
|
| CEContactDisabled {contact :: Contact}
|
||||||
| CEConnectionDisabled {connection :: Connection}
|
| CEConnectionDisabled {connection :: Connection}
|
||||||
| CEGroupUserRole {groupInfo :: GroupInfo, requiredRole :: GroupMemberRole}
|
| CEGroupUserRole {groupInfo :: GroupInfo, requiredRole :: GroupMemberRole}
|
||||||
@ -1060,6 +1061,15 @@ instance ToJSON RemoteCtrlError where
|
|||||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RCE"
|
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RCE"
|
||||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RCE"
|
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RCE"
|
||||||
|
|
||||||
|
data ArchiveError
|
||||||
|
= AEImport {chatError :: ChatError}
|
||||||
|
| AEImportFile {file :: String, chatError :: ChatError}
|
||||||
|
deriving (Show, Exception, Generic)
|
||||||
|
|
||||||
|
instance ToJSON ArchiveError where
|
||||||
|
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "AE"
|
||||||
|
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "AE"
|
||||||
|
|
||||||
type ChatMonad' m = (MonadUnliftIO m, MonadReader ChatController m)
|
type ChatMonad' m = (MonadUnliftIO m, MonadReader ChatController m)
|
||||||
|
|
||||||
type ChatMonad m = (ChatMonad' m, MonadError ChatError m)
|
type ChatMonad m = (ChatMonad' m, MonadError ChatError m)
|
||||||
@ -1103,15 +1113,6 @@ unsetActive a = asks activeTo >>= atomically . (`modifyTVar` unset)
|
|||||||
where
|
where
|
||||||
unset a' = if a == a' then ActiveNone else a'
|
unset a' = if a == a' then ActiveNone else a'
|
||||||
|
|
||||||
data ArchiveError
|
|
||||||
= AEImport {chatError :: ChatError}
|
|
||||||
| AEImportFile {file :: String, chatError :: ChatError}
|
|
||||||
deriving (Show, Exception, Generic)
|
|
||||||
|
|
||||||
instance ToJSON ArchiveError where
|
|
||||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "AE"
|
|
||||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "AE"
|
|
||||||
|
|
||||||
-- | Emit local events.
|
-- | Emit local events.
|
||||||
toView :: ChatMonad' m => ChatResponse -> m ()
|
toView :: ChatMonad' m => ChatResponse -> m ()
|
||||||
toView = toView_ Nothing
|
toView = toView_ Nothing
|
||||||
|
@ -132,6 +132,7 @@ data CIContent (d :: MsgDirection) where
|
|||||||
CIRcvDecryptionError :: MsgDecryptError -> Word32 -> CIContent 'MDRcv
|
CIRcvDecryptionError :: MsgDecryptError -> Word32 -> CIContent 'MDRcv
|
||||||
CIRcvGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDRcv
|
CIRcvGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDRcv
|
||||||
CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd
|
CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd
|
||||||
|
CIRcvDirectEvent :: RcvDirectEvent -> CIContent 'MDRcv
|
||||||
CIRcvGroupEvent :: RcvGroupEvent -> CIContent 'MDRcv
|
CIRcvGroupEvent :: RcvGroupEvent -> CIContent 'MDRcv
|
||||||
CISndGroupEvent :: SndGroupEvent -> CIContent 'MDSnd
|
CISndGroupEvent :: SndGroupEvent -> CIContent 'MDSnd
|
||||||
CIRcvConnEvent :: RcvConnEvent -> CIContent 'MDRcv
|
CIRcvConnEvent :: RcvConnEvent -> CIContent 'MDRcv
|
||||||
@ -179,6 +180,7 @@ ciRequiresAttention content = case msgDirection @d of
|
|||||||
CIRcvIntegrityError _ -> True
|
CIRcvIntegrityError _ -> True
|
||||||
CIRcvDecryptionError {} -> True
|
CIRcvDecryptionError {} -> True
|
||||||
CIRcvGroupInvitation {} -> True
|
CIRcvGroupInvitation {} -> True
|
||||||
|
CIRcvDirectEvent _ -> False
|
||||||
CIRcvGroupEvent rge -> case rge of
|
CIRcvGroupEvent rge -> case rge of
|
||||||
RGEMemberAdded {} -> False
|
RGEMemberAdded {} -> False
|
||||||
RGEMemberConnected -> False
|
RGEMemberConnected -> False
|
||||||
@ -300,6 +302,27 @@ instance ToJSON DBSndConnEvent where
|
|||||||
toJSON (SCE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "SCE") v
|
toJSON (SCE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "SCE") v
|
||||||
toEncoding (SCE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "SCE") v
|
toEncoding (SCE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "SCE") v
|
||||||
|
|
||||||
|
data RcvDirectEvent =
|
||||||
|
-- RDEProfileChanged {...}
|
||||||
|
RDEContactDeleted
|
||||||
|
deriving (Show, Generic)
|
||||||
|
|
||||||
|
instance FromJSON RcvDirectEvent where
|
||||||
|
parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RDE"
|
||||||
|
|
||||||
|
instance ToJSON RcvDirectEvent where
|
||||||
|
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RDE"
|
||||||
|
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RDE"
|
||||||
|
|
||||||
|
newtype DBRcvDirectEvent = RDE RcvDirectEvent
|
||||||
|
|
||||||
|
instance FromJSON DBRcvDirectEvent where
|
||||||
|
parseJSON v = RDE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "RDE") v
|
||||||
|
|
||||||
|
instance ToJSON DBRcvDirectEvent where
|
||||||
|
toJSON (RDE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "RDE") v
|
||||||
|
toEncoding (RDE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "RDE") v
|
||||||
|
|
||||||
newtype DBMsgErrorType = DBME MsgErrorType
|
newtype DBMsgErrorType = DBME MsgErrorType
|
||||||
|
|
||||||
instance FromJSON DBMsgErrorType where
|
instance FromJSON DBMsgErrorType where
|
||||||
@ -348,6 +371,7 @@ ciContentToText = \case
|
|||||||
CIRcvDecryptionError err n -> msgDecryptErrorText err n
|
CIRcvDecryptionError err n -> msgDecryptErrorText err n
|
||||||
CIRcvGroupInvitation groupInvitation memberRole -> "received " <> ciGroupInvitationToText groupInvitation memberRole
|
CIRcvGroupInvitation groupInvitation memberRole -> "received " <> ciGroupInvitationToText groupInvitation memberRole
|
||||||
CISndGroupInvitation groupInvitation memberRole -> "sent " <> ciGroupInvitationToText groupInvitation memberRole
|
CISndGroupInvitation groupInvitation memberRole -> "sent " <> ciGroupInvitationToText groupInvitation memberRole
|
||||||
|
CIRcvDirectEvent event -> rcvDirectEventToText event
|
||||||
CIRcvGroupEvent event -> rcvGroupEventToText event
|
CIRcvGroupEvent event -> rcvGroupEventToText event
|
||||||
CISndGroupEvent event -> sndGroupEventToText event
|
CISndGroupEvent event -> sndGroupEventToText event
|
||||||
CIRcvConnEvent event -> rcvConnEventToText event
|
CIRcvConnEvent event -> rcvConnEventToText event
|
||||||
@ -368,6 +392,10 @@ ciGroupInvitationToText :: CIGroupInvitation -> GroupMemberRole -> Text
|
|||||||
ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayName, fullName}} role =
|
ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayName, fullName}} role =
|
||||||
"invitation to join group " <> displayName <> optionalFullName displayName fullName <> " as " <> (decodeLatin1 . strEncode $ role)
|
"invitation to join group " <> displayName <> optionalFullName displayName fullName <> " as " <> (decodeLatin1 . strEncode $ role)
|
||||||
|
|
||||||
|
rcvDirectEventToText :: RcvDirectEvent -> Text
|
||||||
|
rcvDirectEventToText = \case
|
||||||
|
RDEContactDeleted -> "contact deleted"
|
||||||
|
|
||||||
rcvGroupEventToText :: RcvGroupEvent -> Text
|
rcvGroupEventToText :: RcvGroupEvent -> Text
|
||||||
rcvGroupEventToText = \case
|
rcvGroupEventToText = \case
|
||||||
RGEMemberAdded _ p -> "added " <> profileToText p
|
RGEMemberAdded _ p -> "added " <> profileToText p
|
||||||
@ -486,6 +514,7 @@ data JSONCIContent
|
|||||||
| JCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32}
|
| JCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32}
|
||||||
| JCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
| JCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||||
| JCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
| JCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||||
|
| JCIRcvDirectEvent {rcvDirectEvent :: RcvDirectEvent}
|
||||||
| JCIRcvGroupEvent {rcvGroupEvent :: RcvGroupEvent}
|
| JCIRcvGroupEvent {rcvGroupEvent :: RcvGroupEvent}
|
||||||
| JCISndGroupEvent {sndGroupEvent :: SndGroupEvent}
|
| JCISndGroupEvent {sndGroupEvent :: SndGroupEvent}
|
||||||
| JCIRcvConnEvent {rcvConnEvent :: RcvConnEvent}
|
| JCIRcvConnEvent {rcvConnEvent :: RcvConnEvent}
|
||||||
@ -522,6 +551,7 @@ jsonCIContent = \case
|
|||||||
CIRcvDecryptionError err n -> JCIRcvDecryptionError err n
|
CIRcvDecryptionError err n -> JCIRcvDecryptionError err n
|
||||||
CIRcvGroupInvitation groupInvitation memberRole -> JCIRcvGroupInvitation {groupInvitation, memberRole}
|
CIRcvGroupInvitation groupInvitation memberRole -> JCIRcvGroupInvitation {groupInvitation, memberRole}
|
||||||
CISndGroupInvitation groupInvitation memberRole -> JCISndGroupInvitation {groupInvitation, memberRole}
|
CISndGroupInvitation groupInvitation memberRole -> JCISndGroupInvitation {groupInvitation, memberRole}
|
||||||
|
CIRcvDirectEvent rcvDirectEvent -> JCIRcvDirectEvent {rcvDirectEvent}
|
||||||
CIRcvGroupEvent rcvGroupEvent -> JCIRcvGroupEvent {rcvGroupEvent}
|
CIRcvGroupEvent rcvGroupEvent -> JCIRcvGroupEvent {rcvGroupEvent}
|
||||||
CISndGroupEvent sndGroupEvent -> JCISndGroupEvent {sndGroupEvent}
|
CISndGroupEvent sndGroupEvent -> JCISndGroupEvent {sndGroupEvent}
|
||||||
CIRcvConnEvent rcvConnEvent -> JCIRcvConnEvent {rcvConnEvent}
|
CIRcvConnEvent rcvConnEvent -> JCIRcvConnEvent {rcvConnEvent}
|
||||||
@ -550,6 +580,7 @@ aciContentJSON = \case
|
|||||||
JCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n
|
JCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n
|
||||||
JCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole
|
JCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole
|
||||||
JCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole
|
JCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole
|
||||||
|
JCIRcvDirectEvent {rcvDirectEvent} -> ACIContent SMDRcv $ CIRcvDirectEvent rcvDirectEvent
|
||||||
JCIRcvGroupEvent {rcvGroupEvent} -> ACIContent SMDRcv $ CIRcvGroupEvent rcvGroupEvent
|
JCIRcvGroupEvent {rcvGroupEvent} -> ACIContent SMDRcv $ CIRcvGroupEvent rcvGroupEvent
|
||||||
JCISndGroupEvent {sndGroupEvent} -> ACIContent SMDSnd $ CISndGroupEvent sndGroupEvent
|
JCISndGroupEvent {sndGroupEvent} -> ACIContent SMDSnd $ CISndGroupEvent sndGroupEvent
|
||||||
JCIRcvConnEvent {rcvConnEvent} -> ACIContent SMDRcv $ CIRcvConnEvent rcvConnEvent
|
JCIRcvConnEvent {rcvConnEvent} -> ACIContent SMDRcv $ CIRcvConnEvent rcvConnEvent
|
||||||
@ -579,6 +610,7 @@ data DBJSONCIContent
|
|||||||
| DBJCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32}
|
| DBJCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32}
|
||||||
| DBJCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
| DBJCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||||
| DBJCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
| DBJCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole}
|
||||||
|
| DBJCIRcvDirectEvent {rcvDirectEvent :: DBRcvDirectEvent}
|
||||||
| DBJCIRcvGroupEvent {rcvGroupEvent :: DBRcvGroupEvent}
|
| DBJCIRcvGroupEvent {rcvGroupEvent :: DBRcvGroupEvent}
|
||||||
| DBJCISndGroupEvent {sndGroupEvent :: DBSndGroupEvent}
|
| DBJCISndGroupEvent {sndGroupEvent :: DBSndGroupEvent}
|
||||||
| DBJCIRcvConnEvent {rcvConnEvent :: DBRcvConnEvent}
|
| DBJCIRcvConnEvent {rcvConnEvent :: DBRcvConnEvent}
|
||||||
@ -615,6 +647,7 @@ dbJsonCIContent = \case
|
|||||||
CIRcvDecryptionError err n -> DBJCIRcvDecryptionError err n
|
CIRcvDecryptionError err n -> DBJCIRcvDecryptionError err n
|
||||||
CIRcvGroupInvitation groupInvitation memberRole -> DBJCIRcvGroupInvitation {groupInvitation, memberRole}
|
CIRcvGroupInvitation groupInvitation memberRole -> DBJCIRcvGroupInvitation {groupInvitation, memberRole}
|
||||||
CISndGroupInvitation groupInvitation memberRole -> DBJCISndGroupInvitation {groupInvitation, memberRole}
|
CISndGroupInvitation groupInvitation memberRole -> DBJCISndGroupInvitation {groupInvitation, memberRole}
|
||||||
|
CIRcvDirectEvent rde -> DBJCIRcvDirectEvent $ RDE rde
|
||||||
CIRcvGroupEvent rge -> DBJCIRcvGroupEvent $ RGE rge
|
CIRcvGroupEvent rge -> DBJCIRcvGroupEvent $ RGE rge
|
||||||
CISndGroupEvent sge -> DBJCISndGroupEvent $ SGE sge
|
CISndGroupEvent sge -> DBJCISndGroupEvent $ SGE sge
|
||||||
CIRcvConnEvent rce -> DBJCIRcvConnEvent $ RCE rce
|
CIRcvConnEvent rce -> DBJCIRcvConnEvent $ RCE rce
|
||||||
@ -643,6 +676,7 @@ aciContentDBJSON = \case
|
|||||||
DBJCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n
|
DBJCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n
|
||||||
DBJCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole
|
DBJCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole
|
||||||
DBJCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole
|
DBJCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole
|
||||||
|
DBJCIRcvDirectEvent (RDE rde) -> ACIContent SMDRcv $ CIRcvDirectEvent rde
|
||||||
DBJCIRcvGroupEvent (RGE rge) -> ACIContent SMDRcv $ CIRcvGroupEvent rge
|
DBJCIRcvGroupEvent (RGE rge) -> ACIContent SMDRcv $ CIRcvGroupEvent rge
|
||||||
DBJCISndGroupEvent (SGE sge) -> ACIContent SMDSnd $ CISndGroupEvent sge
|
DBJCISndGroupEvent (SGE sge) -> ACIContent SMDSnd $ CISndGroupEvent sge
|
||||||
DBJCIRcvConnEvent (RCE rce) -> ACIContent SMDRcv $ CIRcvConnEvent rce
|
DBJCIRcvConnEvent (RCE rce) -> ACIContent SMDRcv $ CIRcvConnEvent rce
|
||||||
|
18
src/Simplex/Chat/Migrations/M20230926_contact_status.hs
Normal file
18
src/Simplex/Chat/Migrations/M20230926_contact_status.hs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
module Simplex.Chat.Migrations.M20230926_contact_status where
|
||||||
|
|
||||||
|
import Database.SQLite.Simple (Query)
|
||||||
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
|
|
||||||
|
m20230926_contact_status :: Query
|
||||||
|
m20230926_contact_status =
|
||||||
|
[sql|
|
||||||
|
ALTER TABLE contacts ADD COLUMN contact_status TEXT NOT NULL DEFAULT 'active';
|
||||||
|
|]
|
||||||
|
|
||||||
|
down_m20230926_contact_status :: Query
|
||||||
|
down_m20230926_contact_status =
|
||||||
|
[sql|
|
||||||
|
ALTER TABLE contacts DROP COLUMN contact_status;
|
||||||
|
|]
|
@ -71,6 +71,7 @@ CREATE TABLE contacts(
|
|||||||
contact_group_member_id INTEGER
|
contact_group_member_id INTEGER
|
||||||
REFERENCES group_members(group_member_id) ON DELETE SET NULL,
|
REFERENCES group_members(group_member_id) ON DELETE SET NULL,
|
||||||
contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0,
|
contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0,
|
||||||
|
contact_status TEXT NOT NULL DEFAULT 'active',
|
||||||
FOREIGN KEY(user_id, local_display_name)
|
FOREIGN KEY(user_id, local_display_name)
|
||||||
REFERENCES display_names(user_id, local_display_name)
|
REFERENCES display_names(user_id, local_display_name)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
|
@ -215,6 +215,7 @@ data ChatMsgEvent (e :: MsgEncoding) where
|
|||||||
XFileCancel :: SharedMsgId -> ChatMsgEvent 'Json
|
XFileCancel :: SharedMsgId -> ChatMsgEvent 'Json
|
||||||
XInfo :: Profile -> ChatMsgEvent 'Json
|
XInfo :: Profile -> ChatMsgEvent 'Json
|
||||||
XContact :: Profile -> Maybe XContactId -> ChatMsgEvent 'Json
|
XContact :: Profile -> Maybe XContactId -> ChatMsgEvent 'Json
|
||||||
|
XDirectDel :: ChatMsgEvent 'Json
|
||||||
XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json
|
XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json
|
||||||
XGrpAcpt :: MemberId -> ChatMsgEvent 'Json
|
XGrpAcpt :: MemberId -> ChatMsgEvent 'Json
|
||||||
XGrpMemNew :: MemberInfo -> ChatMsgEvent 'Json
|
XGrpMemNew :: MemberInfo -> ChatMsgEvent 'Json
|
||||||
@ -550,6 +551,7 @@ data CMEventTag (e :: MsgEncoding) where
|
|||||||
XFileCancel_ :: CMEventTag 'Json
|
XFileCancel_ :: CMEventTag 'Json
|
||||||
XInfo_ :: CMEventTag 'Json
|
XInfo_ :: CMEventTag 'Json
|
||||||
XContact_ :: CMEventTag 'Json
|
XContact_ :: CMEventTag 'Json
|
||||||
|
XDirectDel_ :: CMEventTag 'Json
|
||||||
XGrpInv_ :: CMEventTag 'Json
|
XGrpInv_ :: CMEventTag 'Json
|
||||||
XGrpAcpt_ :: CMEventTag 'Json
|
XGrpAcpt_ :: CMEventTag 'Json
|
||||||
XGrpMemNew_ :: CMEventTag 'Json
|
XGrpMemNew_ :: CMEventTag 'Json
|
||||||
@ -596,6 +598,7 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where
|
|||||||
XFileCancel_ -> "x.file.cancel"
|
XFileCancel_ -> "x.file.cancel"
|
||||||
XInfo_ -> "x.info"
|
XInfo_ -> "x.info"
|
||||||
XContact_ -> "x.contact"
|
XContact_ -> "x.contact"
|
||||||
|
XDirectDel_ -> "x.direct.del"
|
||||||
XGrpInv_ -> "x.grp.inv"
|
XGrpInv_ -> "x.grp.inv"
|
||||||
XGrpAcpt_ -> "x.grp.acpt"
|
XGrpAcpt_ -> "x.grp.acpt"
|
||||||
XGrpMemNew_ -> "x.grp.mem.new"
|
XGrpMemNew_ -> "x.grp.mem.new"
|
||||||
@ -643,6 +646,7 @@ instance StrEncoding ACMEventTag where
|
|||||||
"x.file.cancel" -> XFileCancel_
|
"x.file.cancel" -> XFileCancel_
|
||||||
"x.info" -> XInfo_
|
"x.info" -> XInfo_
|
||||||
"x.contact" -> XContact_
|
"x.contact" -> XContact_
|
||||||
|
"x.direct.del" -> XDirectDel_
|
||||||
"x.grp.inv" -> XGrpInv_
|
"x.grp.inv" -> XGrpInv_
|
||||||
"x.grp.acpt" -> XGrpAcpt_
|
"x.grp.acpt" -> XGrpAcpt_
|
||||||
"x.grp.mem.new" -> XGrpMemNew_
|
"x.grp.mem.new" -> XGrpMemNew_
|
||||||
@ -686,6 +690,7 @@ toCMEventTag msg = case msg of
|
|||||||
XFileCancel _ -> XFileCancel_
|
XFileCancel _ -> XFileCancel_
|
||||||
XInfo _ -> XInfo_
|
XInfo _ -> XInfo_
|
||||||
XContact _ _ -> XContact_
|
XContact _ _ -> XContact_
|
||||||
|
XDirectDel -> XDirectDel_
|
||||||
XGrpInv _ -> XGrpInv_
|
XGrpInv _ -> XGrpInv_
|
||||||
XGrpAcpt _ -> XGrpAcpt_
|
XGrpAcpt _ -> XGrpAcpt_
|
||||||
XGrpMemNew _ -> XGrpMemNew_
|
XGrpMemNew _ -> XGrpMemNew_
|
||||||
@ -782,6 +787,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do
|
|||||||
XFileCancel_ -> XFileCancel <$> p "msgId"
|
XFileCancel_ -> XFileCancel <$> p "msgId"
|
||||||
XInfo_ -> XInfo <$> p "profile"
|
XInfo_ -> XInfo <$> p "profile"
|
||||||
XContact_ -> XContact <$> p "profile" <*> opt "contactReqId"
|
XContact_ -> XContact <$> p "profile" <*> opt "contactReqId"
|
||||||
|
XDirectDel_ -> pure XDirectDel
|
||||||
XGrpInv_ -> XGrpInv <$> p "groupInvitation"
|
XGrpInv_ -> XGrpInv <$> p "groupInvitation"
|
||||||
XGrpAcpt_ -> XGrpAcpt <$> p "memberId"
|
XGrpAcpt_ -> XGrpAcpt <$> p "memberId"
|
||||||
XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo"
|
XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo"
|
||||||
@ -839,6 +845,7 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @
|
|||||||
XFileCancel sharedMsgId -> o ["msgId" .= sharedMsgId]
|
XFileCancel sharedMsgId -> o ["msgId" .= sharedMsgId]
|
||||||
XInfo profile -> o ["profile" .= profile]
|
XInfo profile -> o ["profile" .= profile]
|
||||||
XContact profile xContactId -> o $ ("contactReqId" .=? xContactId) ["profile" .= profile]
|
XContact profile xContactId -> o $ ("contactReqId" .=? xContactId) ["profile" .= profile]
|
||||||
|
XDirectDel -> JM.empty
|
||||||
XGrpInv groupInv -> o ["groupInvitation" .= groupInv]
|
XGrpInv groupInv -> o ["groupInvitation" .= groupInv]
|
||||||
XGrpAcpt memId -> o ["memberId" .= memId]
|
XGrpAcpt memId -> o ["memberId" .= memId]
|
||||||
XGrpMemNew memInfo -> o ["memberInfo" .= memInfo]
|
XGrpMemNew memInfo -> o ["memberInfo" .= memInfo]
|
||||||
|
@ -71,19 +71,19 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
|
|||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT
|
SELECT
|
||||||
c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.via_group, c.contact_used, c.enable_ntfs, c.send_rcpts, c.favorite,
|
c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.via_group, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
|
||||||
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent
|
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent
|
||||||
FROM contacts c
|
FROM contacts c
|
||||||
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
|
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
|
||||||
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
|
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
|
||||||
|]
|
|]
|
||||||
(userId, contactId)
|
(userId, contactId)
|
||||||
toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)] -> Either StoreError Contact
|
toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)] -> Either StoreError Contact
|
||||||
toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)] =
|
toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)] =
|
||||||
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
||||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
|
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
|
||||||
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
||||||
in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
|
in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
|
||||||
toContact' _ _ _ = Left $ SEInternalError "referenced contact not found"
|
toContact' _ _ _ = Left $ SEInternalError "referenced contact not found"
|
||||||
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
|
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
|
||||||
getGroupAndMember_ groupMemberId c = ExceptT $ do
|
getGroupAndMember_ groupMemberId c = ExceptT $ do
|
||||||
|
@ -42,6 +42,7 @@ module Simplex.Chat.Store.Direct
|
|||||||
deletePCCIncognitoProfile,
|
deletePCCIncognitoProfile,
|
||||||
updateContactUsed,
|
updateContactUsed,
|
||||||
updateContactUnreadChat,
|
updateContactUnreadChat,
|
||||||
|
updateContactStatus,
|
||||||
updateGroupUnreadChat,
|
updateGroupUnreadChat,
|
||||||
setConnectionVerified,
|
setConnectionVerified,
|
||||||
incConnectionAuthErrCounter,
|
incConnectionAuthErrCounter,
|
||||||
@ -147,7 +148,7 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
|
|||||||
[sql|
|
[sql|
|
||||||
SELECT
|
SELECT
|
||||||
-- Contact
|
-- Contact
|
||||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||||
@ -206,7 +207,7 @@ createDirectContact db user@User {userId} activeConn@Connection {connId, localAl
|
|||||||
let profile = toLocalProfile profileId p localAlias
|
let profile = toLocalProfile profileId p localAlias
|
||||||
userPreferences = emptyChatPrefs
|
userPreferences = emptyChatPrefs
|
||||||
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
||||||
pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False}
|
pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, contactUsed = False, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False}
|
||||||
|
|
||||||
deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO ()
|
deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO ()
|
||||||
deleteContactConnectionsAndFiles db userId Contact {contactId} = do
|
deleteContactConnectionsAndFiles db userId Contact {contactId} = do
|
||||||
@ -387,6 +388,19 @@ updateContactUnreadChat db User {userId} Contact {contactId} unreadChat = do
|
|||||||
updatedAt <- getCurrentTime
|
updatedAt <- getCurrentTime
|
||||||
DB.execute db "UPDATE contacts SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (unreadChat, updatedAt, userId, contactId)
|
DB.execute db "UPDATE contacts SET unread_chat = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (unreadChat, updatedAt, userId, contactId)
|
||||||
|
|
||||||
|
updateContactStatus :: DB.Connection -> User -> Contact -> ContactStatus -> IO Contact
|
||||||
|
updateContactStatus db User {userId} ct@Contact {contactId} contactStatus = do
|
||||||
|
currentTs <- getCurrentTime
|
||||||
|
DB.execute
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
UPDATE contacts
|
||||||
|
SET contact_status = ?, updated_at = ?
|
||||||
|
WHERE user_id = ? AND contact_id = ?
|
||||||
|
|]
|
||||||
|
(contactStatus, currentTs, userId, contactId)
|
||||||
|
pure ct {contactStatus}
|
||||||
|
|
||||||
updateGroupUnreadChat :: DB.Connection -> User -> GroupInfo -> Bool -> IO ()
|
updateGroupUnreadChat :: DB.Connection -> User -> GroupInfo -> Bool -> IO ()
|
||||||
updateGroupUnreadChat db User {userId} GroupInfo {groupId} unreadChat = do
|
updateGroupUnreadChat db User {userId} GroupInfo {groupId} unreadChat = do
|
||||||
updatedAt <- getCurrentTime
|
updatedAt <- getCurrentTime
|
||||||
@ -491,7 +505,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (Vers
|
|||||||
[sql|
|
[sql|
|
||||||
SELECT
|
SELECT
|
||||||
-- Contact
|
-- Contact
|
||||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||||
@ -637,7 +651,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
|
|||||||
contactId <- insertedRowId db
|
contactId <- insertedRowId db
|
||||||
activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode
|
activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode
|
||||||
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
||||||
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False}
|
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, contactUsed = False, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False}
|
||||||
|
|
||||||
getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64
|
getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64
|
||||||
getContactIdByName db User {userId} cName =
|
getContactIdByName db User {userId} cName =
|
||||||
@ -655,7 +669,7 @@ getContact_ db user@User {userId} contactId deleted =
|
|||||||
[sql|
|
[sql|
|
||||||
SELECT
|
SELECT
|
||||||
-- Contact
|
-- Contact
|
||||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||||
|
@ -700,7 +700,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
|
|||||||
[sql|
|
[sql|
|
||||||
SELECT
|
SELECT
|
||||||
-- Contact
|
-- Contact
|
||||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||||
@ -1044,7 +1044,7 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} =
|
|||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT
|
SELECT
|
||||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, ct.via_group, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, ct.via_group, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
||||||
p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||||
@ -1062,13 +1062,13 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} =
|
|||||||
|]
|
|]
|
||||||
(userId, groupMemberId)
|
(userId, groupMemberId)
|
||||||
where
|
where
|
||||||
toContact' :: ((ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)) :. ConnectionRow -> Contact
|
toContact' :: ((ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)) :. ConnectionRow -> Contact
|
||||||
toContact' (((contactId, profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
|
toContact' (((contactId, profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
|
||||||
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
||||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
|
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
|
||||||
activeConn = toConnection connRow
|
activeConn = toConnection connRow
|
||||||
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
||||||
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
|
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
|
||||||
|
|
||||||
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
|
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
|
||||||
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences}
|
updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences}
|
||||||
@ -1160,8 +1160,8 @@ getMatchingContacts :: DB.Connection -> User -> Contact -> IO [Contact]
|
|||||||
getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalProfile {displayName, fullName, image}} = do
|
getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalProfile {displayName, fullName, image}} = do
|
||||||
contactIds <-
|
contactIds <-
|
||||||
map fromOnly <$> case image of
|
map fromOnly <$> case image of
|
||||||
Just img -> DB.query db (q <> " AND p.image = ?") (userId, contactId, displayName, fullName, img)
|
Just img -> DB.query db (q <> " AND p.image = ?") (userId, contactId, CSActive, displayName, fullName, img)
|
||||||
Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, contactId, displayName, fullName)
|
Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, contactId, CSActive, displayName, fullName)
|
||||||
rights <$> mapM (runExceptT . getContact db user) contactIds
|
rights <$> mapM (runExceptT . getContact db user) contactIds
|
||||||
where
|
where
|
||||||
-- this query is different from one in getMatchingMemberContacts
|
-- this query is different from one in getMatchingMemberContacts
|
||||||
@ -1172,7 +1172,7 @@ getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalPro
|
|||||||
FROM contacts ct
|
FROM contacts ct
|
||||||
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
|
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
|
||||||
WHERE ct.user_id = ? AND ct.contact_id != ?
|
WHERE ct.user_id = ? AND ct.contact_id != ?
|
||||||
AND ct.deleted = 0
|
AND ct.contact_status = ? AND ct.deleted = 0
|
||||||
AND p.display_name = ? AND p.full_name = ?
|
AND p.display_name = ? AND p.full_name = ?
|
||||||
|]
|
|]
|
||||||
|
|
||||||
@ -1521,7 +1521,7 @@ createMemberContact
|
|||||||
connId <- insertedRowId db
|
connId <- insertedRowId db
|
||||||
let ctConn = Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
|
let ctConn = Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
|
||||||
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
|
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
|
||||||
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = ctConn, viaGroup = Nothing, contactUsed = True, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False}
|
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False}
|
||||||
|
|
||||||
getMemberContact :: DB.Connection -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
|
getMemberContact :: DB.Connection -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
|
||||||
getMemberContact db user contactId = do
|
getMemberContact db user contactId = do
|
||||||
@ -1558,7 +1558,7 @@ createMemberContactInvited
|
|||||||
contactId <- createContactUpdateMember currentTs userPreferences
|
contactId <- createContactUpdateMember currentTs userPreferences
|
||||||
ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode
|
ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode
|
||||||
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
|
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
|
||||||
mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = ctConn, viaGroup = Nothing, contactUsed = True, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False}
|
mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False}
|
||||||
m' = m {memberContactId = Just contactId}
|
m' = m {memberContactId = Just contactId}
|
||||||
pure (mCt', m')
|
pure (mCt', m')
|
||||||
where
|
where
|
||||||
@ -1586,8 +1586,9 @@ updateMemberContactInvited :: DB.Connection -> User -> (CommandId, ConnId) -> Gr
|
|||||||
updateMemberContactInvited db user connIds gInfo mConn ct@Contact {contactId, activeConn = oldContactConn} subMode = do
|
updateMemberContactInvited db user connIds gInfo mConn ct@Contact {contactId, activeConn = oldContactConn} subMode = do
|
||||||
updateConnectionStatus db oldContactConn ConnDeleted
|
updateConnectionStatus db oldContactConn ConnDeleted
|
||||||
activeConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode
|
activeConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode
|
||||||
ct' <- resetMemberContactFields db ct
|
ct' <- updateContactStatus db user ct CSActive
|
||||||
pure (ct' :: Contact) {activeConn}
|
ct'' <- resetMemberContactFields db ct'
|
||||||
|
pure (ct'' :: Contact) {activeConn}
|
||||||
|
|
||||||
resetMemberContactFields :: DB.Connection -> Contact -> IO Contact
|
resetMemberContactFields :: DB.Connection -> Contact -> IO Contact
|
||||||
resetMemberContactFields db ct@Contact {contactId} = do
|
resetMemberContactFields db ct@Contact {contactId} = do
|
||||||
|
@ -478,7 +478,7 @@ getDirectChatPreviews_ db user@User {userId} = do
|
|||||||
[sql|
|
[sql|
|
||||||
SELECT
|
SELECT
|
||||||
-- Contact
|
-- Contact
|
||||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
|
||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||||
|
@ -82,6 +82,7 @@ import Simplex.Chat.Migrations.M20230903_connections_to_subscribe
|
|||||||
import Simplex.Chat.Migrations.M20230913_member_contacts
|
import Simplex.Chat.Migrations.M20230913_member_contacts
|
||||||
import Simplex.Chat.Migrations.M20230914_member_probes
|
import Simplex.Chat.Migrations.M20230914_member_probes
|
||||||
import Simplex.Chat.Migrations.M20230922_remote_controller
|
import Simplex.Chat.Migrations.M20230922_remote_controller
|
||||||
|
import Simplex.Chat.Migrations.M20230926_contact_status
|
||||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||||
|
|
||||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||||
@ -163,7 +164,8 @@ schemaMigrations =
|
|||||||
("20230903_connections_to_subscribe", m20230903_connections_to_subscribe, Just down_m20230903_connections_to_subscribe),
|
("20230903_connections_to_subscribe", m20230903_connections_to_subscribe, Just down_m20230903_connections_to_subscribe),
|
||||||
("20230913_member_contacts", m20230913_member_contacts, Just down_m20230913_member_contacts),
|
("20230913_member_contacts", m20230913_member_contacts, Just down_m20230913_member_contacts),
|
||||||
("20230914_member_probes", m20230914_member_probes, Just down_m20230914_member_probes),
|
("20230914_member_probes", m20230914_member_probes, Just down_m20230914_member_probes),
|
||||||
("20230922_remote_controller", m20230922_remote_controller, Just down_m20230922_remote_controller)
|
("20230922_remote_controller", m20230922_remote_controller, Just down_m20230922_remote_controller),
|
||||||
|
("20230926_contact_status", m20230926_contact_status, Just down_m20230926_contact_status)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | The list of migrations in ascending order by date
|
-- | The list of migrations in ascending order by date
|
||||||
|
@ -241,24 +241,24 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|
|||||||
|]
|
|]
|
||||||
[":user_id" := userId, ":profile_id" := profileId]
|
[":user_id" := userId, ":profile_id" := profileId]
|
||||||
|
|
||||||
type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)
|
type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)
|
||||||
|
|
||||||
toContact :: User -> ContactRow :. ConnectionRow -> Contact
|
toContact :: User -> ContactRow :. ConnectionRow -> Contact
|
||||||
toContact user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
|
toContact user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
|
||||||
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
||||||
activeConn = toConnection connRow
|
activeConn = toConnection connRow
|
||||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
|
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
|
||||||
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
||||||
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
|
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
|
||||||
|
|
||||||
toContactOrError :: User -> ContactRow :. MaybeConnectionRow -> Either StoreError Contact
|
toContactOrError :: User -> ContactRow :. MaybeConnectionRow -> Either StoreError Contact
|
||||||
toContactOrError user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
|
toContactOrError user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) =
|
||||||
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
|
||||||
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
|
chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite}
|
||||||
in case toMaybeConnection connRow of
|
in case toMaybeConnection connRow of
|
||||||
Just activeConn ->
|
Just activeConn ->
|
||||||
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn
|
||||||
in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
|
in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent}
|
||||||
_ -> Left $ SEContactNotReady localDisplayName
|
_ -> Left $ SEContactNotReady localDisplayName
|
||||||
|
|
||||||
getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile
|
getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile
|
||||||
|
@ -169,6 +169,7 @@ data Contact = Contact
|
|||||||
activeConn :: Connection,
|
activeConn :: Connection,
|
||||||
viaGroup :: Maybe Int64,
|
viaGroup :: Maybe Int64,
|
||||||
contactUsed :: Bool,
|
contactUsed :: Bool,
|
||||||
|
contactStatus :: ContactStatus,
|
||||||
chatSettings :: ChatSettings,
|
chatSettings :: ChatSettings,
|
||||||
userPreferences :: Preferences,
|
userPreferences :: Preferences,
|
||||||
mergedPreferences :: ContactUserPreferences,
|
mergedPreferences :: ContactUserPreferences,
|
||||||
@ -185,7 +186,7 @@ instance ToJSON Contact where
|
|||||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||||
|
|
||||||
contactConn :: Contact -> Connection
|
contactConn :: Contact -> Connection
|
||||||
contactConn Contact{activeConn} = activeConn
|
contactConn Contact {activeConn} = activeConn
|
||||||
|
|
||||||
contactConnId :: Contact -> ConnId
|
contactConnId :: Contact -> ConnId
|
||||||
contactConnId = aConnId . contactConn
|
contactConnId = aConnId . contactConn
|
||||||
@ -205,9 +206,34 @@ directOrUsed ct@Contact {contactUsed} =
|
|||||||
anyDirectOrUsed :: Contact -> Bool
|
anyDirectOrUsed :: Contact -> Bool
|
||||||
anyDirectOrUsed Contact {contactUsed, activeConn = Connection {connLevel}} = connLevel == 0 || contactUsed
|
anyDirectOrUsed Contact {contactUsed, activeConn = Connection {connLevel}} = connLevel == 0 || contactUsed
|
||||||
|
|
||||||
|
contactActive :: Contact -> Bool
|
||||||
|
contactActive Contact {contactStatus} = contactStatus == CSActive
|
||||||
|
|
||||||
contactSecurityCode :: Contact -> Maybe SecurityCode
|
contactSecurityCode :: Contact -> Maybe SecurityCode
|
||||||
contactSecurityCode Contact {activeConn} = connectionCode activeConn
|
contactSecurityCode Contact {activeConn} = connectionCode activeConn
|
||||||
|
|
||||||
|
data ContactStatus
|
||||||
|
= CSActive
|
||||||
|
| CSDeleted -- contact deleted by contact
|
||||||
|
deriving (Eq, Show, Ord)
|
||||||
|
|
||||||
|
instance FromField ContactStatus where fromField = fromTextField_ textDecode
|
||||||
|
|
||||||
|
instance ToField ContactStatus where toField = toField . textEncode
|
||||||
|
|
||||||
|
instance ToJSON ContactStatus where
|
||||||
|
toJSON = J.String . textEncode
|
||||||
|
toEncoding = JE.text . textEncode
|
||||||
|
|
||||||
|
instance TextEncoding ContactStatus where
|
||||||
|
textDecode = \case
|
||||||
|
"active" -> Just CSActive
|
||||||
|
"deleted" -> Just CSDeleted
|
||||||
|
_ -> Nothing
|
||||||
|
textEncode = \case
|
||||||
|
CSActive -> "active"
|
||||||
|
CSDeleted -> "deleted"
|
||||||
|
|
||||||
data ContactRef = ContactRef
|
data ContactRef = ContactRef
|
||||||
{ contactId :: ContactId,
|
{ contactId :: ContactId,
|
||||||
connId :: Int64,
|
connId :: Int64,
|
||||||
|
@ -151,6 +151,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView
|
|||||||
CRSentConfirmation u -> ttyUser u ["confirmation sent!"]
|
CRSentConfirmation u -> ttyUser u ["confirmation sent!"]
|
||||||
CRSentInvitation u customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
|
CRSentInvitation u customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
|
||||||
CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"]
|
CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"]
|
||||||
|
CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"]
|
||||||
CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo
|
CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo
|
||||||
CRAcceptingContactRequest u c -> ttyUser u [ttyFullContact c <> ": accepting contact request..."]
|
CRAcceptingContactRequest u c -> ttyUser u [ttyFullContact c <> ": accepting contact request..."]
|
||||||
CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"]
|
CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"]
|
||||||
@ -1568,6 +1569,7 @@ viewChatError logLevel = \case
|
|||||||
]
|
]
|
||||||
CEContactNotFound cName m_ -> viewContactNotFound cName m_
|
CEContactNotFound cName m_ -> viewContactNotFound cName m_
|
||||||
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
|
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
|
||||||
|
CEContactNotActive c -> [ttyContact' c <> ": not active"]
|
||||||
CEContactDisabled Contact {localDisplayName = c} -> [ttyContact c <> ": disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)]
|
CEContactDisabled Contact {localDisplayName = c} -> [ttyContact c <> ": disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)]
|
||||||
CEConnectionDisabled Connection {connId, connType} -> [plain $ "connection " <> textEncode connType <> " (" <> tshow connId <> ") is disabled" | logLevel <= CLLWarning]
|
CEConnectionDisabled Connection {connId, connType} -> [plain $ "connection " <> textEncode connType <> " (" <> tshow connId <> ") is disabled" | logLevel <= CLLWarning]
|
||||||
CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"]
|
CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"]
|
||||||
|
@ -49,7 +49,7 @@ extra-deps:
|
|||||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||||
# - ../simplexmq
|
# - ../simplexmq
|
||||||
- github: simplex-chat/simplexmq
|
- github: simplex-chat/simplexmq
|
||||||
commit: 8d47f690838371bc848e4b31a4b09ef6bf67ccc5
|
commit: ec1b72cb8013a65a5d9783104a47ae44f5730089
|
||||||
- github: kazu-yamamoto/http2
|
- github: kazu-yamamoto/http2
|
||||||
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
||||||
# - ../direct-sqlcipher
|
# - ../direct-sqlcipher
|
||||||
|
@ -31,6 +31,7 @@ chatDirectTests = do
|
|||||||
describe "direct messages" $ do
|
describe "direct messages" $ do
|
||||||
describe "add contact and send/receive message" testAddContact
|
describe "add contact and send/receive message" testAddContact
|
||||||
it "deleting contact deletes profile" testDeleteContactDeletesProfile
|
it "deleting contact deletes profile" testDeleteContactDeletesProfile
|
||||||
|
it "unused contact is deleted silently" testDeleteUnusedContactSilent
|
||||||
it "direct message quoted replies" testDirectMessageQuotedReply
|
it "direct message quoted replies" testDirectMessageQuotedReply
|
||||||
it "direct message update" testDirectMessageUpdate
|
it "direct message update" testDirectMessageUpdate
|
||||||
it "direct message edit history" testDirectMessageEditHistory
|
it "direct message edit history" testDirectMessageEditHistory
|
||||||
@ -156,11 +157,12 @@ testAddContact = versionTestMatrix2 runTestAddContact
|
|||||||
-- test deleting contact
|
-- test deleting contact
|
||||||
alice ##> "/d bob_1"
|
alice ##> "/d bob_1"
|
||||||
alice <## "bob_1: contact is deleted"
|
alice <## "bob_1: contact is deleted"
|
||||||
|
bob <## "alice_1 (Alice) deleted contact with you"
|
||||||
alice ##> "@bob_1 hey"
|
alice ##> "@bob_1 hey"
|
||||||
alice <## "no contact bob_1"
|
alice <## "no contact bob_1"
|
||||||
alice @@@ [("@bob", "how are you?")]
|
alice @@@ [("@bob", "how are you?")]
|
||||||
alice `hasContactProfiles` ["alice", "bob"]
|
alice `hasContactProfiles` ["alice", "bob"]
|
||||||
bob @@@ [("@alice_1", "hi"), ("@alice", "how are you?")]
|
bob @@@ [("@alice_1", "contact deleted"), ("@alice", "how are you?")]
|
||||||
bob `hasContactProfiles` ["alice", "alice", "bob"]
|
bob `hasContactProfiles` ["alice", "alice", "bob"]
|
||||||
-- test clearing chat
|
-- test clearing chat
|
||||||
alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY")
|
alice #$> ("/clear bob", id, "bob: all messages are removed locally ONLY")
|
||||||
@ -202,6 +204,7 @@ testDeleteContactDeletesProfile =
|
|||||||
-- alice deletes contact, profile is deleted
|
-- alice deletes contact, profile is deleted
|
||||||
alice ##> "/d bob"
|
alice ##> "/d bob"
|
||||||
alice <## "bob: contact is deleted"
|
alice <## "bob: contact is deleted"
|
||||||
|
bob <## "alice (Alice) deleted contact with you"
|
||||||
alice ##> "/_contacts 1"
|
alice ##> "/_contacts 1"
|
||||||
(alice </)
|
(alice </)
|
||||||
alice `hasContactProfiles` ["alice"]
|
alice `hasContactProfiles` ["alice"]
|
||||||
@ -212,6 +215,42 @@ testDeleteContactDeletesProfile =
|
|||||||
(bob </)
|
(bob </)
|
||||||
bob `hasContactProfiles` ["bob"]
|
bob `hasContactProfiles` ["bob"]
|
||||||
|
|
||||||
|
testDeleteUnusedContactSilent :: HasCallStack => FilePath -> IO ()
|
||||||
|
testDeleteUnusedContactSilent =
|
||||||
|
testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $
|
||||||
|
\alice bob cath -> do
|
||||||
|
createGroup3 "team" alice bob cath
|
||||||
|
bob ##> "/contacts"
|
||||||
|
bob <### ["alice (Alice)", "cath (Catherine)"]
|
||||||
|
bob `hasContactProfiles` ["bob", "alice", "cath"]
|
||||||
|
cath ##> "/contacts"
|
||||||
|
cath <### ["alice (Alice)", "bob (Bob)"]
|
||||||
|
cath `hasContactProfiles` ["cath", "alice", "bob"]
|
||||||
|
-- bob deletes cath, cath's bob contact is deleted silently
|
||||||
|
bob ##> "/d cath"
|
||||||
|
bob <## "cath: contact is deleted"
|
||||||
|
bob ##> "/contacts"
|
||||||
|
bob <## "alice (Alice)"
|
||||||
|
threadDelay 50000
|
||||||
|
cath ##> "/contacts"
|
||||||
|
cath <## "alice (Alice)"
|
||||||
|
-- group messages work
|
||||||
|
alice #> "#team hello"
|
||||||
|
concurrentlyN_
|
||||||
|
[ bob <# "#team alice> hello",
|
||||||
|
cath <# "#team alice> hello"
|
||||||
|
]
|
||||||
|
bob #> "#team hi there"
|
||||||
|
concurrentlyN_
|
||||||
|
[ alice <# "#team bob> hi there",
|
||||||
|
cath <# "#team bob> hi there"
|
||||||
|
]
|
||||||
|
cath #> "#team hey"
|
||||||
|
concurrentlyN_
|
||||||
|
[ alice <# "#team cath> hey",
|
||||||
|
bob <# "#team cath> hey"
|
||||||
|
]
|
||||||
|
|
||||||
testDirectMessageQuotedReply :: HasCallStack => FilePath -> IO ()
|
testDirectMessageQuotedReply :: HasCallStack => FilePath -> IO ()
|
||||||
testDirectMessageQuotedReply =
|
testDirectMessageQuotedReply =
|
||||||
testChat2 aliceProfile bobProfile $
|
testChat2 aliceProfile bobProfile $
|
||||||
@ -514,7 +553,7 @@ testRepeatAuthErrorsDisableContact =
|
|||||||
connectUsers alice bob
|
connectUsers alice bob
|
||||||
alice <##> bob
|
alice <##> bob
|
||||||
threadDelay 500000
|
threadDelay 500000
|
||||||
bob ##> "/d alice"
|
bob ##> "/_delete @2 notify=off"
|
||||||
bob <## "alice: contact is deleted"
|
bob <## "alice: contact is deleted"
|
||||||
forM_ [1 .. authErrDisableCount] $ \_ -> sendAuth alice
|
forM_ [1 .. authErrDisableCount] $ \_ -> sendAuth alice
|
||||||
alice <## "[bob] connection is disabled, to enable: /enable bob, to delete: /d bob"
|
alice <## "[bob] connection is disabled, to enable: /enable bob, to delete: /d bob"
|
||||||
|
@ -575,6 +575,7 @@ testSendImage =
|
|||||||
-- deleting contact without files folder set should not remove file
|
-- deleting contact without files folder set should not remove file
|
||||||
bob ##> "/d alice"
|
bob ##> "/d alice"
|
||||||
bob <## "alice: contact is deleted"
|
bob <## "alice: contact is deleted"
|
||||||
|
alice <## "bob (Bob) deleted contact with you"
|
||||||
fileExists <- doesFileExist "./tests/tmp/test.jpg"
|
fileExists <- doesFileExist "./tests/tmp/test.jpg"
|
||||||
fileExists `shouldBe` True
|
fileExists `shouldBe` True
|
||||||
|
|
||||||
@ -637,6 +638,7 @@ testFilesFoldersSendImage =
|
|||||||
checkActionDeletesFile "./tests/tmp/app_files/test.jpg" $ do
|
checkActionDeletesFile "./tests/tmp/app_files/test.jpg" $ do
|
||||||
bob ##> "/d alice"
|
bob ##> "/d alice"
|
||||||
bob <## "alice: contact is deleted"
|
bob <## "alice: contact is deleted"
|
||||||
|
alice <## "bob (Bob) deleted contact with you"
|
||||||
|
|
||||||
testFilesFoldersImageSndDelete :: HasCallStack => FilePath -> IO ()
|
testFilesFoldersImageSndDelete :: HasCallStack => FilePath -> IO ()
|
||||||
testFilesFoldersImageSndDelete =
|
testFilesFoldersImageSndDelete =
|
||||||
@ -660,6 +662,7 @@ testFilesFoldersImageSndDelete =
|
|||||||
checkActionDeletesFile "./tests/tmp/alice_app_files/test_1MB.pdf" $ do
|
checkActionDeletesFile "./tests/tmp/alice_app_files/test_1MB.pdf" $ do
|
||||||
alice ##> "/d bob"
|
alice ##> "/d bob"
|
||||||
alice <## "bob: contact is deleted"
|
alice <## "bob: contact is deleted"
|
||||||
|
bob <## "alice (Alice) deleted contact with you"
|
||||||
bob ##> "/fs 1"
|
bob ##> "/fs 1"
|
||||||
bob <##. "receiving file 1 (test_1MB.pdf) progress"
|
bob <##. "receiving file 1 (test_1MB.pdf) progress"
|
||||||
-- deleting contact should remove cancelled file
|
-- deleting contact should remove cancelled file
|
||||||
@ -689,7 +692,10 @@ testFilesFoldersImageRcvDelete =
|
|||||||
checkActionDeletesFile "./tests/tmp/app_files/test.jpg" $ do
|
checkActionDeletesFile "./tests/tmp/app_files/test.jpg" $ do
|
||||||
bob ##> "/d alice"
|
bob ##> "/d alice"
|
||||||
bob <## "alice: contact is deleted"
|
bob <## "alice: contact is deleted"
|
||||||
alice <## "bob cancelled receiving file 1 (test.jpg)"
|
alice
|
||||||
|
<### [ "bob (Bob) deleted contact with you",
|
||||||
|
"bob cancelled receiving file 1 (test.jpg)"
|
||||||
|
]
|
||||||
alice ##> "/fs 1"
|
alice ##> "/fs 1"
|
||||||
alice <## "sending file 1 (test.jpg) cancelled: bob"
|
alice <## "sending file 1 (test.jpg) cancelled: bob"
|
||||||
alice <## "file transfer cancelled"
|
alice <## "file transfer cancelled"
|
||||||
|
@ -220,6 +220,7 @@ testGroupShared alice bob cath checkMessages = do
|
|||||||
-- delete contact
|
-- delete contact
|
||||||
alice ##> "/d bob"
|
alice ##> "/d bob"
|
||||||
alice <## "bob: contact is deleted"
|
alice <## "bob: contact is deleted"
|
||||||
|
bob <## "alice (Alice) deleted contact with you"
|
||||||
alice `send` "@bob hey"
|
alice `send` "@bob hey"
|
||||||
alice
|
alice
|
||||||
<### [ "@bob hey",
|
<### [ "@bob hey",
|
||||||
@ -234,7 +235,7 @@ testGroupShared alice bob cath checkMessages = do
|
|||||||
alice <# "#team bob> received"
|
alice <# "#team bob> received"
|
||||||
when checkMessages $ do
|
when checkMessages $ do
|
||||||
alice @@@ [("@cath", "sent invitation to join group team as admin"), ("#team", "received")]
|
alice @@@ [("@cath", "sent invitation to join group team as admin"), ("#team", "received")]
|
||||||
bob @@@ [("@alice", "received invitation to join group team as admin"), ("@cath", "hey"), ("#team", "received")]
|
bob @@@ [("@alice", "contact deleted"), ("@cath", "hey"), ("#team", "received")]
|
||||||
-- test clearing chat
|
-- test clearing chat
|
||||||
threadDelay 1000000
|
threadDelay 1000000
|
||||||
alice #$> ("/clear #team", id, "#team: all messages are removed locally ONLY")
|
alice #$> ("/clear #team", id, "#team: all messages are removed locally ONLY")
|
||||||
@ -629,6 +630,7 @@ testGroupDeleteInvitedContact =
|
|||||||
threadDelay 500000
|
threadDelay 500000
|
||||||
alice ##> "/d bob"
|
alice ##> "/d bob"
|
||||||
alice <## "bob: contact is deleted"
|
alice <## "bob: contact is deleted"
|
||||||
|
bob <## "alice (Alice) deleted contact with you"
|
||||||
bob ##> "/j team"
|
bob ##> "/j team"
|
||||||
concurrently_
|
concurrently_
|
||||||
(alice <## "#team: bob joined the group")
|
(alice <## "#team: bob joined the group")
|
||||||
@ -700,10 +702,11 @@ testDeleteGroupMemberProfileKept =
|
|||||||
-- delete contact
|
-- delete contact
|
||||||
alice ##> "/d bob"
|
alice ##> "/d bob"
|
||||||
alice <## "bob: contact is deleted"
|
alice <## "bob: contact is deleted"
|
||||||
|
bob <## "alice (Alice) deleted contact with you"
|
||||||
alice ##> "@bob hey"
|
alice ##> "@bob hey"
|
||||||
alice <## "no contact bob, use @#club bob <your message>"
|
alice <## "no contact bob, use @#club bob <your message>"
|
||||||
bob #> "@alice hey"
|
bob ##> "@alice hey"
|
||||||
bob <## "[alice, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection"
|
bob <## "alice: not ready"
|
||||||
(alice </)
|
(alice </)
|
||||||
-- delete group 1
|
-- delete group 1
|
||||||
alice ##> "/d #team"
|
alice ##> "/d #team"
|
||||||
@ -2785,6 +2788,8 @@ testMemberContactMessage =
|
|||||||
-- alice and bob delete contacts, connect
|
-- alice and bob delete contacts, connect
|
||||||
alice ##> "/d bob"
|
alice ##> "/d bob"
|
||||||
alice <## "bob: contact is deleted"
|
alice <## "bob: contact is deleted"
|
||||||
|
bob <## "alice (Alice) deleted contact with you"
|
||||||
|
|
||||||
bob ##> "/d alice"
|
bob ##> "/d alice"
|
||||||
bob <## "alice: contact is deleted"
|
bob <## "alice: contact is deleted"
|
||||||
|
|
||||||
@ -2893,6 +2898,7 @@ testMemberContactInvitedConnectionReplaced tmp = do
|
|||||||
|
|
||||||
alice ##> "/d bob"
|
alice ##> "/d bob"
|
||||||
alice <## "bob: contact is deleted"
|
alice <## "bob: contact is deleted"
|
||||||
|
bob <## "alice (Alice) deleted contact with you"
|
||||||
|
|
||||||
alice ##> "@#team bob hi"
|
alice ##> "@#team bob hi"
|
||||||
alice
|
alice
|
||||||
@ -2910,7 +2916,7 @@ testMemberContactInvitedConnectionReplaced tmp = do
|
|||||||
(alice <## "bob (Bob): contact is connected")
|
(alice <## "bob (Bob): contact is connected")
|
||||||
(bob <## "alice (Alice): contact is connected")
|
(bob <## "alice (Alice): contact is connected")
|
||||||
|
|
||||||
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "received invitation to join group team as admin"), (0, "hi"), (0, "security code changed")] <> chatFeatures)
|
bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "received invitation to join group team as admin"), (0, "contact deleted"), (0, "hi"), (0, "security code changed")] <> chatFeatures)
|
||||||
|
|
||||||
withTestChat tmp "bob" $ \bob -> do
|
withTestChat tmp "bob" $ \bob -> do
|
||||||
subscriptions bob 1
|
subscriptions bob 1
|
||||||
|
@ -558,6 +558,7 @@ testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfi
|
|||||||
-- alice deletes contact, incognito profile is deleted
|
-- alice deletes contact, incognito profile is deleted
|
||||||
alice ##> ("/d " <> bobIncognito)
|
alice ##> ("/d " <> bobIncognito)
|
||||||
alice <## (bobIncognito <> ": contact is deleted")
|
alice <## (bobIncognito <> ": contact is deleted")
|
||||||
|
bob <## (aliceIncognito <> " deleted contact with you")
|
||||||
alice ##> "/contacts"
|
alice ##> "/contacts"
|
||||||
alice <## "cath (Catherine)"
|
alice <## "cath (Catherine)"
|
||||||
alice `hasContactProfiles` ["alice", "cath"]
|
alice `hasContactProfiles` ["alice", "cath"]
|
||||||
@ -601,6 +602,7 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $
|
|||||||
-- delete contact, incognito profile is deleted
|
-- delete contact, incognito profile is deleted
|
||||||
bob ##> "/d alice"
|
bob ##> "/d alice"
|
||||||
bob <## "alice: contact is deleted"
|
bob <## "alice: contact is deleted"
|
||||||
|
alice <## (bobIncognito <> " deleted contact with you")
|
||||||
bob ##> "/contacts"
|
bob ##> "/contacts"
|
||||||
(bob </)
|
(bob </)
|
||||||
bob `hasContactProfiles` ["bob"]
|
bob `hasContactProfiles` ["bob"]
|
||||||
@ -633,6 +635,7 @@ testAcceptContactRequestIncognito = testChat3 aliceProfile bobProfile cathProfil
|
|||||||
-- delete contact, incognito profile is deleted
|
-- delete contact, incognito profile is deleted
|
||||||
alice ##> "/d bob"
|
alice ##> "/d bob"
|
||||||
alice <## "bob: contact is deleted"
|
alice <## "bob: contact is deleted"
|
||||||
|
bob <## (aliceIncognitoBob <> " deleted contact with you")
|
||||||
alice ##> "/contacts"
|
alice ##> "/contacts"
|
||||||
(alice </)
|
(alice </)
|
||||||
alice `hasContactProfiles` ["alice"]
|
alice `hasContactProfiles` ["alice"]
|
||||||
@ -1063,6 +1066,7 @@ testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobPr
|
|||||||
-- delete contact
|
-- delete contact
|
||||||
bob ##> "/d alice"
|
bob ##> "/d alice"
|
||||||
bob <## "alice: contact is deleted"
|
bob <## "alice: contact is deleted"
|
||||||
|
alice <## (bobIncognito <> " deleted contact with you")
|
||||||
bob ##> "/contacts"
|
bob ##> "/contacts"
|
||||||
(bob </)
|
(bob </)
|
||||||
bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
|
bob `hasContactProfiles` ["alice", "bob", T.pack bobIncognito]
|
||||||
@ -1125,6 +1129,7 @@ testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobPr
|
|||||||
-- delete contact
|
-- delete contact
|
||||||
bob ##> "/d alice"
|
bob ##> "/d alice"
|
||||||
bob <## "alice: contact is deleted"
|
bob <## "alice: contact is deleted"
|
||||||
|
alice <## (bobIncognito <> " deleted contact with you")
|
||||||
bob ##> "/contacts"
|
bob ##> "/contacts"
|
||||||
(bob </)
|
(bob </)
|
||||||
bob `hasContactProfiles` ["bob"]
|
bob `hasContactProfiles` ["bob"]
|
||||||
|
Loading…
Reference in New Issue
Block a user