Merge branch 'stable' into stable-android

This commit is contained in:
Evgeny Poberezkin
2024-12-13 09:37:44 +00:00
62 changed files with 619 additions and 276 deletions
+78 -29
View File
@@ -156,8 +156,8 @@ struct ChatInfoView: View {
HStack(alignment: .center, spacing: 8) {
let buttonWidth = g.size.width / 4
searchButton(width: buttonWidth)
AudioCallButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
VideoButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
AudioCallButton(chat: chat, contact: contact, connectionStats: $connectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
VideoButton(chat: chat, contact: contact, connectionStats: $connectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
muteButton(width: buttonWidth)
}
}
@@ -314,7 +314,15 @@ struct ChatInfoView: View {
case .networkStatusAlert: return networkStatusAlert()
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress)
case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) })
case .syncConnectionForceAlert:
return syncConnectionForceAlert({
Task {
if let stats = await syncContactConnection(contact, force: true, showAlert: { alert = .someAlert(alert: $0) }) {
connectionStats = stats
dismiss()
}
}
})
case let .queueInfo(info): return queueInfoAlert(info)
case let .someAlert(a): return a.alert
case let .error(title, error): return mkAlert(title: title, message: error)
@@ -493,7 +501,12 @@ struct ChatInfoView: View {
private func synchronizeConnectionButton() -> some View {
Button {
syncContactConnection(force: false)
Task {
if let stats = await syncContactConnection(contact, force: false, showAlert: { alert = .someAlert(alert: $0) }) {
connectionStats = stats
dismiss()
}
}
} label: {
Label("Fix connection", systemImage: "exclamationmark.arrow.triangle.2.circlepath")
.foregroundColor(.orange)
@@ -612,25 +625,6 @@ struct ChatInfoView: View {
}
}
private func syncContactConnection(force: Bool) {
Task {
do {
let stats = try apiSyncContactRatchet(contact.apiId, force)
connectionStats = stats
await MainActor.run {
chatModel.updateContactConnectionStats(contact, stats)
dismiss()
}
} catch let error {
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
alert = .error(title: a.title, error: a.message)
}
}
}
}
private func savePreferences() {
Task {
do {
@@ -649,9 +643,32 @@ struct ChatInfoView: View {
}
}
func syncContactConnection(_ contact: Contact, force: Bool, showAlert: (SomeAlert) -> Void) async -> ConnectionStats? {
do {
let stats = try apiSyncContactRatchet(contact.apiId, force)
await MainActor.run {
ChatModel.shared.updateContactConnectionStats(contact, stats)
}
return stats
} catch let error {
logger.error("syncContactConnection apiSyncContactRatchet error: \(responseError(error))")
let a = getErrorAlert(error, "Error synchronizing connection")
await MainActor.run {
showAlert(
SomeAlert(
alert: mkAlert(title: a.title, message: a.message),
id: "syncContactConnection error"
)
)
}
return nil
}
}
struct AudioCallButton: View {
var chat: Chat
var contact: Contact
@Binding var connectionStats: ConnectionStats?
var width: CGFloat
var showAlert: (SomeAlert) -> Void
@@ -659,6 +676,7 @@ struct AudioCallButton: View {
CallButton(
chat: chat,
contact: contact,
connectionStats: $connectionStats,
image: "phone.fill",
title: "call",
mediaType: .audio,
@@ -671,6 +689,7 @@ struct AudioCallButton: View {
struct VideoButton: View {
var chat: Chat
var contact: Contact
@Binding var connectionStats: ConnectionStats?
var width: CGFloat
var showAlert: (SomeAlert) -> Void
@@ -678,6 +697,7 @@ struct VideoButton: View {
CallButton(
chat: chat,
contact: contact,
connectionStats: $connectionStats,
image: "video.fill",
title: "video",
mediaType: .video,
@@ -690,6 +710,7 @@ struct VideoButton: View {
private struct CallButton: View {
var chat: Chat
var contact: Contact
@Binding var connectionStats: ConnectionStats?
var image: String
var title: LocalizedStringKey
var mediaType: CallMediaType
@@ -701,12 +722,40 @@ private struct CallButton: View {
InfoViewButton(image: image, title: title, disabledLook: !canCall, width: width) {
if canCall {
if CallController.useCallKit() {
CallController.shared.startCall(contact, mediaType)
} else {
// When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first
dismissAllSheets(animated: true) {
CallController.shared.startCall(contact, mediaType)
if let connStats = connectionStats {
if connStats.ratchetSyncState == .ok {
if CallController.useCallKit() {
CallController.shared.startCall(contact, mediaType)
} else {
// When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first
dismissAllSheets(animated: true) {
CallController.shared.startCall(contact, mediaType)
}
}
} else if connStats.ratchetSyncAllowed {
showAlert(SomeAlert(
alert: Alert(
title: Text("Fix connection?"),
message: Text("Connection requires encryption renegotiation."),
primaryButton: .default(Text("Fix")) {
Task {
if let stats = await syncContactConnection(contact, force: false, showAlert: showAlert) {
connectionStats = stats
}
}
},
secondaryButton: .cancel()
),
id: "can't call contact, fix connection"
))
} else {
showAlert(SomeAlert(
alert: mkAlert(
title: "Can't call contact",
message: "Encryption renegotiation in progress."
),
id: "can't call contact, encryption renegotiation in progress"
))
}
}
} else if contact.nextSendGrpInv {
+39 -21
View File
@@ -440,6 +440,7 @@ struct ChatView: View {
maxWidth: maxWidth,
composeState: $composeState,
selectedMember: $selectedMember,
showChatInfoSheet: $showChatInfoSheet,
revealedChatItem: $revealedChatItem,
selectedChatItems: $selectedChatItems,
forwardedChatItems: $forwardedChatItems
@@ -893,12 +894,14 @@ struct ChatView: View {
private struct ChatItemWithMenu: View {
@EnvironmentObject var m: ChatModel
@EnvironmentObject var theme: AppTheme
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileRadius = defaultProfileImageCorner
@Binding @ObservedObject var chat: Chat
@ObservedObject var dummyModel: ChatItemDummyModel = .shared
let chatItem: ChatItem
let maxWidth: CGFloat
@Binding var composeState: ComposeState
@Binding var selectedMember: GMember?
@Binding var showChatInfoSheet: Bool
@Binding var revealedChatItem: ChatItem?
@State private var deletingItem: ChatItem? = nil
@@ -1255,16 +1258,22 @@ struct ChatView: View {
setReaction(ci, add: !r.userReacted, reaction: r.reaction)
}
}
if case let .group(groupInfo) = chat.chatInfo {
switch chat.chatInfo {
case let .group(groupInfo):
v.contextMenu {
ReactionContextMenu(
groupInfo: groupInfo,
itemId: ci.id,
reactionCount: r,
selectedMember: $selectedMember
selectedMember: $selectedMember,
profileRadius: profileRadius
)
}
} else {
case let .direct(contact):
v.contextMenu {
contactReactionMenu(contact, r)
}
default:
v
}
}
@@ -1767,6 +1776,20 @@ struct ChatView: View {
}
}
}
@ViewBuilder private func contactReactionMenu(_ contact: Contact, _ r: CIReactionCount) -> some View {
if !r.userReacted || r.totalReacted > 1 {
Button { showChatInfoSheet = true } label: {
profileMenuItem(Text(contact.displayName), contact.image, radius: profileRadius)
}
}
if r.userReacted {
Button {} label: {
profileMenuItem(Text("you"), m.currentUser?.profile.image, radius: profileRadius)
}
.disabled(true)
}
}
private struct SelectedChatItem: View {
@EnvironmentObject var theme: AppTheme
@@ -1859,13 +1882,12 @@ struct ReactionContextMenu: View {
var itemId: Int64
var reactionCount: CIReactionCount
@Binding var selectedMember: GMember?
var profileRadius: CGFloat
@State private var memberReactions: [MemberReaction] = []
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
var body: some View {
groupMemberReactionList()
.task {
logger.debug("ReactionContextMenu task \(radius)")
await loadChatItemReaction()
}
}
@@ -1889,27 +1911,12 @@ struct ReactionContextMenu: View {
selectedMember = member
}
} label: {
HStack {
Text(mem.displayName)
if let img = cropImage(mem.image) {
Image(uiImage: img)
} else {
Image(systemName: "person.crop.circle")
}
}
profileMenuItem(Text(mem.displayName), mem.image, radius: profileRadius)
}
.disabled(userMember)
}
}
}
private func cropImage(_ img: String?) -> UIImage? {
return if let originalImage = imageFromBase64(img) {
maskToCustomShape(originalImage, size: 30, radius: radius)
} else {
nil
}
}
private func loadChatItemReaction() async {
do {
@@ -1927,6 +1934,17 @@ struct ReactionContextMenu: View {
}
}
func profileMenuItem(_ nameText: Text, _ image: String?, radius: CGFloat) -> some View {
HStack {
nameText
if let image, let img = imageFromBase64(image) {
Image(uiImage: maskToCustomShape(img, size: 30, radius: radius))
} else {
Image(systemName: "person.crop.circle")
}
}
}
func maskToCustomShape(_ image: UIImage, size: CGFloat, radius: CGFloat) -> UIImage {
let path = Path { path in
if radius >= 50 {
@@ -20,6 +20,9 @@ struct GroupMemberInfoView: View {
@State private var connectionStats: ConnectionStats? = nil
@State private var connectionCode: String? = nil
@State private var connectionLoaded: Bool = false
@State private var knownContactChat: Chat? = nil
@State private var knownContact: Contact? = nil
@State private var knownContactConnectionStats: ConnectionStats? = nil
@State private var newRole: GroupMemberRole = .member
@State private var alert: GroupMemberInfoViewAlert?
@State private var sheet: PlanAndConnectActionSheet?
@@ -119,8 +122,8 @@ struct GroupMemberInfoView: View {
} label: {
Label("Share address", systemImage: "square.and.arrow.up")
}
if let contactId = member.memberContactId {
if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
if member.memberContactId != nil {
if knownContactChat == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
connectViaAddressButton(contactLink)
}
} else {
@@ -229,6 +232,18 @@ struct GroupMemberInfoView: View {
}
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
}
if let contactId = member.memberContactId, let (contactChat, contact) = knownDirectChat(contactId) {
knownContactChat = contactChat
knownContact = contact
do {
let (stats, _) = try await apiContactInfo(contactChat.chatInfo.apiId)
await MainActor.run {
knownContactConnectionStats = stats
}
} catch let error {
logger.error("apiContactInfo error: \(responseError(error))")
}
}
}
.onChange(of: newRole) { newRole in
if newRole != member.memberRole {
@@ -274,10 +289,10 @@ struct GroupMemberInfoView: View {
GeometryReader { g in
let buttonWidth = g.size.width / 4
HStack(alignment: .center, spacing: 8) {
if let contactId = member.memberContactId, let (chat, contact) = knownDirectChat(contactId) {
if let chat = knownContactChat, let contact = knownContact {
knownDirectChatButton(chat, width: buttonWidth)
AudioCallButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
VideoButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) }
AudioCallButton(chat: chat, contact: contact, connectionStats: $knownContactConnectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
VideoButton(chat: chat, contact: contact, connectionStats: $knownContactConnectionStats, width: buttonWidth) { alert = .someAlert(alert: $0) }
} else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
if let contactId = member.memberContactId {
newDirectChatButton(contactId, width: buttonWidth)
@@ -366,25 +381,49 @@ struct GroupMemberInfoView: View {
func createMemberContactButton(width: CGFloat) -> some View {
InfoViewButton(image: "message.fill", title: "message", width: width) {
progressIndicator = true
Task {
do {
let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId)
await MainActor.run {
progressIndicator = false
chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact)))
ItemsModel.shared.loadOpenChat(memberContact.id) {
dismissAllSheets(animated: true)
if let connStats = connectionStats {
if connStats.ratchetSyncState == .ok {
progressIndicator = true
Task {
do {
let memberContact = try await apiCreateMemberContact(groupInfo.apiId, groupMember.groupMemberId)
await MainActor.run {
progressIndicator = false
chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact)))
ItemsModel.shared.loadOpenChat(memberContact.id) {
dismissAllSheets(animated: true)
}
NetworkModel.shared.setContactNetworkStatus(memberContact, .connected)
}
} catch let error {
logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error creating member contact")
await MainActor.run {
progressIndicator = false
alert = .error(title: a.title, error: a.message)
}
}
NetworkModel.shared.setContactNetworkStatus(memberContact, .connected)
}
} catch let error {
logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))")
let a = getErrorAlert(error, "Error creating member contact")
await MainActor.run {
progressIndicator = false
alert = .error(title: a.title, error: a.message)
}
} else if connStats.ratchetSyncAllowed {
alert = .someAlert(alert: SomeAlert(
alert: Alert(
title: Text("Fix connection?"),
message: Text("Connection requires encryption renegotiation."),
primaryButton: .default(Text("Fix")) {
syncMemberConnection(force: false)
},
secondaryButton: .cancel()
),
id: "can't message member, fix connection"
))
} else {
alert = .someAlert(alert: SomeAlert(
alert: mkAlert(
title: "Can't message member",
message: "Encryption renegotiation in progress."
),
id: "can't message contact, encryption renegotiation in progress"
))
}
}
}
@@ -587,7 +587,7 @@ struct SMPStatsView: View {
} header: {
Text("Statistics")
} footer: {
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.")
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is kept private on your device.")
}
}
}
@@ -703,7 +703,7 @@ struct XFTPStatsView: View {
} header: {
Text("Statistics")
} footer: {
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.")
Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is kept private on your device.")
}
}
}
@@ -731,8 +731,8 @@
<target>Всички данни се изтриват при въвеждане.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -712,8 +712,8 @@
<target>Všechna data se při zadání vymažou.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -760,8 +760,8 @@
<target>Alle Daten werden gelöscht, sobald dieser eingegeben wird.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Alle Daten werden nur auf Ihrem Gerät gespeichert.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -760,9 +760,9 @@
<target>All data is erased when it is entered.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<target>All data is private to your device.</target>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>All data is kept private on your device.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -760,8 +760,8 @@
<target>Al introducirlo todos los datos son eliminados.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Todos los datos son privados y están en tu dispositivo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -707,8 +707,8 @@
<target>Kaikki tiedot poistetaan, kun se syötetään.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -745,8 +745,8 @@
<target>Toutes les données sont effacées lorsqu'il est saisi.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Toutes les données restent confinées dans votre appareil.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -760,8 +760,8 @@
<target>A jelkód megadása után az összes adat törlésre kerül.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Az összes adat biztonságban van az eszközén.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -759,8 +759,8 @@
<target>Tutti i dati vengono cancellati quando inserito.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Tutti i dati sono privati, nel tuo dispositivo.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -724,8 +724,8 @@
<target>入力するとすべてのデータが消去されます。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -760,8 +760,8 @@
<target>Alle gegevens worden bij het invoeren gewist.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Alle gegevens zijn privé op uw apparaat.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -745,8 +745,8 @@
<target>Wszystkie dane są usuwane po jego wprowadzeniu.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Wszystkie dane są prywatne na Twoim urządzeniu.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -5425,8 +5425,8 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi
<source>Advanced settings</source>
<target state="translated">Configurações avançadas</target>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve" approved="no">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve" approved="no">
<source>All data is kept private on your device.</source>
<target state="translated">Toda informação é privada em seu dispositivo.</target>
</trans-unit>
<trans-unit id="All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." xml:space="preserve" approved="no">
@@ -760,8 +760,8 @@
<target>Все данные удаляются при его вводе.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Все данные хранятся только на вашем устройстве.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -699,8 +699,8 @@
<target>ข้อมูลทั้งหมดจะถูกลบเมื่อถูกป้อน</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All group members will remain connected." xml:space="preserve">
@@ -745,8 +745,8 @@
<target>Kullanıldığında bütün veriler silinir.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Tüm veriler cihazınıza özeldir.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -756,8 +756,8 @@
<target>Всі дані стираються при введенні.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>Всі дані є приватними для вашого пристрою.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
@@ -739,8 +739,8 @@
<target>所有数据在输入后将被删除。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="All data is private to your device." xml:space="preserve">
<source>All data is private to your device.</source>
<trans-unit id="All data is kept private on your device." xml:space="preserve">
<source>All data is kept private on your device.</source>
<target>所有数据都是您设备的私有数据.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
+20 -20
View File
@@ -1931,7 +1931,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1956,7 +1956,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1980,7 +1980,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -2005,7 +2005,7 @@
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -2021,11 +2021,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2041,11 +2041,11 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2066,7 +2066,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2081,7 +2081,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2103,7 +2103,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2118,7 +2118,7 @@
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2140,7 +2140,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2166,7 +2166,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2191,7 +2191,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2217,7 +2217,7 @@
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -2242,7 +2242,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2257,7 +2257,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
@@ -2276,7 +2276,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 254;
CURRENT_PROJECT_VERSION = 255;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2291,7 +2291,7 @@
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 6.2;
MARKETING_VERSION = 6.2.1;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
+1 -1
View File
@@ -479,7 +479,7 @@
"All data is erased when it is entered." = "Alle Daten werden gelöscht, sobald dieser eingegeben wird.";
/* No comment provided by engineer. */
"All data is private to your device." = "Alle Daten werden nur auf Ihrem Gerät gespeichert.";
"All data is kept private on your device." = "Alle Daten werden nur auf Ihrem Gerät gespeichert.";
/* No comment provided by engineer. */
"All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden.";
+1 -1
View File
@@ -479,7 +479,7 @@
"All data is erased when it is entered." = "Al introducirlo todos los datos son eliminados.";
/* No comment provided by engineer. */
"All data is private to your device." = "Todos los datos son privados y están en tu dispositivo.";
"All data is kept private on your device." = "Todos los datos son privados y están en tu dispositivo.";
/* No comment provided by engineer. */
"All group members will remain connected." = "Todos los miembros del grupo permanecerán conectados.";
+1 -1
View File
@@ -431,7 +431,7 @@
"All data is erased when it is entered." = "Toutes les données sont effacées lorsqu'il est saisi.";
/* No comment provided by engineer. */
"All data is private to your device." = "Toutes les données restent confinées dans votre appareil.";
"All data is kept private on your device." = "Toutes les données restent confinées dans votre appareil.";
/* No comment provided by engineer. */
"All group members will remain connected." = "Tous les membres du groupe resteront connectés.";
+1 -1
View File
@@ -479,7 +479,7 @@
"All data is erased when it is entered." = "A jelkód megadása után az összes adat törlésre kerül.";
/* No comment provided by engineer. */
"All data is private to your device." = "Az összes adat biztonságban van az eszközén.";
"All data is kept private on your device." = "Az összes adat biztonságban van az eszközén.";
/* No comment provided by engineer. */
"All group members will remain connected." = "Az összes csoporttag kapcsolatban marad.";
+1 -1
View File
@@ -473,7 +473,7 @@
"All data is erased when it is entered." = "Tutti i dati vengono cancellati quando inserito.";
/* No comment provided by engineer. */
"All data is private to your device." = "Tutti i dati sono privati, nel tuo dispositivo.";
"All data is kept private on your device." = "Tutti i dati sono privati, nel tuo dispositivo.";
/* No comment provided by engineer. */
"All group members will remain connected." = "Tutti i membri del gruppo resteranno connessi.";
+1 -1
View File
@@ -479,7 +479,7 @@
"All data is erased when it is entered." = "Alle gegevens worden bij het invoeren gewist.";
/* No comment provided by engineer. */
"All data is private to your device." = "Alle gegevens zijn privé op uw apparaat.";
"All data is kept private on your device." = "Alle gegevens zijn privé op uw apparaat.";
/* No comment provided by engineer. */
"All group members will remain connected." = "Alle groepsleden blijven verbonden.";
+1 -1
View File
@@ -431,7 +431,7 @@
"All data is erased when it is entered." = "Wszystkie dane są usuwane po jego wprowadzeniu.";
/* No comment provided by engineer. */
"All data is private to your device." = "Wszystkie dane są prywatne na Twoim urządzeniu.";
"All data is kept private on your device." = "Wszystkie dane są prywatne na Twoim urządzeniu.";
/* No comment provided by engineer. */
"All group members will remain connected." = "Wszyscy członkowie grupy pozostaną połączeni.";
+1 -1
View File
@@ -479,7 +479,7 @@
"All data is erased when it is entered." = "Все данные удаляются при его вводе.";
/* No comment provided by engineer. */
"All data is private to your device." = "Все данные хранятся только на вашем устройстве.";
"All data is kept private on your device." = "Все данные хранятся только на вашем устройстве.";
/* No comment provided by engineer. */
"All group members will remain connected." = "Все члены группы, которые соединились через эту ссылку, останутся в группе.";
+1 -1
View File
@@ -431,7 +431,7 @@
"All data is erased when it is entered." = "Kullanıldığında bütün veriler silinir.";
/* No comment provided by engineer. */
"All data is private to your device." = "Tüm veriler cihazınıza özeldir.";
"All data is kept private on your device." = "Tüm veriler cihazınıza özeldir.";
/* No comment provided by engineer. */
"All group members will remain connected." = "Tüm grup üyeleri bağlı kalacaktır.";
+1 -1
View File
@@ -464,7 +464,7 @@
"All data is erased when it is entered." = "Всі дані стираються при введенні.";
/* No comment provided by engineer. */
"All data is private to your device." = "Всі дані є приватними для вашого пристрою.";
"All data is kept private on your device." = "Всі дані є приватними для вашого пристрою.";
/* No comment provided by engineer. */
"All group members will remain connected." = "Всі учасники групи залишаться на зв'язку.";
+1 -1
View File
@@ -413,7 +413,7 @@
"All data is erased when it is entered." = "所有数据在输入后将被删除。";
/* No comment provided by engineer. */
"All data is private to your device." = "所有数据都是您设备的私有数据.";
"All data is kept private on your device." = "所有数据都是您设备的私有数据.";
/* No comment provided by engineer. */
"All group members will remain connected." = "所有群组成员将保持连接。";
@@ -1,10 +1,11 @@
package chat.simplex.common.platform
import android.util.Log
import chat.simplex.common.model.ChatController.appPrefs
actual object Log {
actual fun d(tag: String, text: String) = Log.d(tag, text).run{}
actual fun e(tag: String, text: String) = Log.e(tag, text).run{}
actual fun i(tag: String, text: String) = Log.i(tag, text).run{}
actual fun w(tag: String, text: String) = Log.w(tag, text).run{}
actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) Log.d(tag, text) }
actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) Log.e(tag, text) }
actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) Log.i(tag, text) }
actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) Log.w(tag, text) }
}
@@ -132,6 +132,7 @@ class AppPreferences {
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
val chatStopped = mkBoolPreference(SHARED_PREFS_CHAT_STOPPED, false)
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
val logLevel = mkEnumPreference(SHARED_PREFS_LOG_LEVEL, LogLevel.WARNING) { LogLevel.entries.firstOrNull { it.name == this } }
val showInternalErrors = mkBoolPreference(SHARED_PREFS_SHOW_INTERNAL_ERRORS, false)
val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false)
val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false)
@@ -393,6 +394,7 @@ class AppPreferences {
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
private const val SHARED_PREFS_LOG_LEVEL = "LogLevel"
private const val SHARED_PREFS_SHOW_INTERNAL_ERRORS = "ShowInternalErrors"
private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls"
private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible"
@@ -2,6 +2,10 @@ package chat.simplex.common.platform
const val TAG = "SIMPLEX"
enum class LogLevel {
DEBUG, INFO, WARNING, ERROR
}
expect object Log {
fun d(tag: String, text: String)
fun e(tag: String, text: String)
@@ -131,26 +131,14 @@ fun ChatInfoView(
},
syncContactConnection = {
withBGApi {
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
connStats.value = cStats
if (cStats != null) {
withChats {
updateContactConnectionStats(chatRh, contact, cStats)
}
}
syncContactConnection(chatRh, contact, connStats, force = false)
close.invoke()
}
},
syncContactConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = {
withBGApi {
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true)
connStats.value = cStats
if (cStats != null) {
withChats {
updateContactConnectionStats(chatRh, contact, cStats)
}
}
syncContactConnection(chatRh, contact, connStats, force = true)
close.invoke()
}
})
@@ -189,6 +177,16 @@ fun ChatInfoView(
}
}
suspend fun syncContactConnection(rhId: Long?, contact: Contact, connectionStats: MutableState<ConnectionStats?>, force: Boolean) {
val cStats = chatModel.controller.apiSyncContactRatchet(rhId, contact.contactId, force = force)
connectionStats.value = cStats
if (cStats != null) {
withChats {
updateContactConnectionStats(rhId, contact, cStats)
}
}
}
sealed class SendReceipts {
object Yes: SendReceipts()
object No: SendReceipts()
@@ -505,7 +503,7 @@ fun ChatInfoLayout(
currentUser: User,
sendReceipts: State<SendReceipts>,
setSendReceipts: (SendReceipts) -> Unit,
connStats: State<ConnectionStats?>,
connStats: MutableState<ConnectionStats?>,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?,
localAlias: String,
@@ -553,8 +551,8 @@ fun ChatInfoLayout(
verticalAlignment = Alignment.CenterVertically
) {
SearchButton(modifier = Modifier.fillMaxWidth(0.25f), chat, contact, close, onSearchClicked)
AudioCallButton(modifier = Modifier.fillMaxWidth(0.33f), chat, contact)
VideoButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact)
AudioCallButton(modifier = Modifier.fillMaxWidth(0.33f), chat, contact, connStats)
VideoButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact, connStats)
MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, contact)
}
}
@@ -825,12 +823,14 @@ fun MuteButton(
fun AudioCallButton(
modifier: Modifier,
chat: Chat,
contact: Contact
contact: Contact,
connectionStats: MutableState<ConnectionStats?>
) {
CallButton(
modifier = modifier,
chat,
contact,
connectionStats,
icon = painterResource(MR.images.ic_call),
title = generalGetString(MR.strings.info_view_call_button),
mediaType = CallMediaType.Audio
@@ -841,12 +841,14 @@ fun AudioCallButton(
fun VideoButton(
modifier: Modifier,
chat: Chat,
contact: Contact
contact: Contact,
connectionStats: MutableState<ConnectionStats?>
) {
CallButton(
modifier = modifier,
chat,
contact,
connectionStats,
icon = painterResource(MR.images.ic_videocam),
title = generalGetString(MR.strings.info_view_video_button),
mediaType = CallMediaType.Video
@@ -858,6 +860,7 @@ fun CallButton(
modifier: Modifier,
chat: Chat,
contact: Contact,
connectionStats: MutableState<ConnectionStats?>,
icon: Painter,
title: String,
mediaType: CallMediaType
@@ -879,7 +882,23 @@ fun CallButton(
disabledLook = !canCall,
onClick =
when {
canCall -> { { startChatCall(chat.remoteHostId, chat.chatInfo, mediaType) } }
canCall -> { {
val connStats = connectionStats.value
if (connStats != null) {
if (connStats.ratchetSyncState == RatchetSyncState.Ok) {
startChatCall(chat.remoteHostId, chat.chatInfo, mediaType)
} else if (connStats.ratchetSyncAllowed) {
showFixConnectionAlert(syncConnection = {
withBGApi { syncContactConnection(chat.remoteHostId, contact, connectionStats, force = false) }
})
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.cant_call_contact_alert_title),
generalGetString(MR.strings.encryption_renegotiation_in_progress)
)
}
}
} }
contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } }
!contact.active -> { { showCantCallContactDeletedAlert() } }
!contact.ready -> { { showCantCallContactConnectingAlert() } }
@@ -1265,6 +1284,15 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
)
}
fun showFixConnectionAlert(syncConnection: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.sync_connection_question),
text = generalGetString(MR.strings.sync_connection_desc),
confirmText = generalGetString(MR.strings.sync_connection_confirm),
onConfirm = syncConnection,
)
}
fun queueInfoText(info: Pair<RcvMsgInfo?, ServerQueueInfo>): String {
val (rcvMsgInfo, qInfo) = info
val msgInfo: String = if (rcvMsgInfo != null) json.encodeToString(rcvMsgInfo) else generalGetString(MR.strings.message_queue_info_none)
@@ -296,6 +296,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -309,6 +310,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
QuotedMsgView(qi)
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -324,6 +326,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
ForwardedFromView(forwardedFromItem)
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -395,6 +398,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
SectionBottomSpacer()
SectionBottomSpacer()
}
}
@@ -433,12 +437,11 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
Column {
if (numTabs() > 1) {
Column(
Box(
Modifier
.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
.fillMaxHeight()
) {
Column(Modifier.weight(1f)) {
Column {
when (val sel = selection.value) {
is CIInfoTab.Delivery -> {
DeliveryTab(sel.memberDeliveryStatuses)
@@ -479,7 +482,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools
}
}
val oneHandUI = remember { appPrefs.oneHandUI.state }
Box(Modifier.offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) {
Box(Modifier.align(Alignment.BottomCenter).navigationBarsPadding().offset(x = 0.dp, y = if (oneHandUI.value) -AppBarHeight * fontSizeSqrtMultiplier else 0.dp)) {
TabRow(
selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class },
Modifier.height(AppBarHeight * fontSizeSqrtMultiplier),
@@ -665,13 +665,18 @@ fun ChatLayout(
AdaptingBottomPaddingLayout(Modifier, CHAT_COMPOSE_LAYOUT_ID, composeViewHeight) {
if (chatInfo != null) {
Box(Modifier.fillMaxSize()) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
)
// disables scrolling to top of chat item on click inside the bubble
CompositionLocalProvider(LocalBringIntoViewSpec provides object : BringIntoViewSpec {
override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f
}) {
ChatItemsList(
remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue,
useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy,
)
}
}
}
Box(
@@ -939,6 +944,7 @@ fun BoxScope.ChatItemsList(
linkMode: SimplexLinkMode,
selectedChatItems: MutableState<Set<Long>?>,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
showChatInfo: () -> Unit,
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit,
@@ -984,6 +990,7 @@ fun BoxScope.ChatItemsList(
})
val maxHeight = remember { derivedStateOf { listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value } }
val loadingMoreItems = remember { mutableStateOf(false) }
val animatedScrollingInProgress = remember { mutableStateOf(false) }
val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf<Long>() }
if (!loadingMoreItems.value) {
PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination ->
@@ -1004,7 +1011,7 @@ fun BoxScope.ChatItemsList(
val chatInfoUpdated = rememberUpdatedState(chatInfo)
val highlightedItems = remember { mutableStateOf(setOf<Long>()) }
val scope = rememberCoroutineScope()
val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) }
val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) }
val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) }
LoadLastItems(loadingMoreItems, remoteHostId, chatInfo)
@@ -1065,7 +1072,7 @@ fun BoxScope.ChatItemsList(
highlightedItems.value = setOf()
}
}
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp)
}
}
@@ -1314,7 +1321,7 @@ fun BoxScope.ChatItemsList(
}
}
}
FloatingButtons(loadingMoreItems, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState)
FloatingButtons(loadingMoreItems, animatedScrollingInProgress, mergedItems, unreadCount, maxHeight, composeViewHeight, searchValue, markChatRead, listState)
FloatingDate(Modifier.padding(top = 10.dp + topPaddingToContent(true)).align(Alignment.TopCenter), mergedItems, listState)
LaunchedEffect(Unit) {
@@ -1323,6 +1330,15 @@ fun BoxScope.ChatItemsList(
chatViewScrollState.value = it
}
}
LaunchedEffect(Unit) {
snapshotFlow { listState.value.isScrollInProgress }
.filter { !it }
.collect {
if (animatedScrollingInProgress.value) {
animatedScrollingInProgress.value = false
}
}
}
}
@Composable
@@ -1400,6 +1416,7 @@ private fun NotifyChatListOnFinishingComposition(
@Composable
fun BoxScope.FloatingButtons(
loadingMoreItems: MutableState<Boolean>,
animatedScrollingInProgress: MutableState<Boolean>,
mergedItems: State<MergedItems>,
unreadCount: State<Int>,
maxHeight: State<Int>,
@@ -1439,8 +1456,14 @@ fun BoxScope.FloatingButtons(
bottomUnreadCount,
showBottomButtonWithCounter,
showBottomButtonWithArrow,
animatedScrollingInProgress,
composeViewHeight,
onClick = { scope.launch { tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) } } }
onClick = {
scope.launch {
animatedScrollingInProgress.value = true
tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(0) }
}
}
)
// Don't show top FAB if is in search
if (searchValue.value.isNotEmpty()) return
@@ -1451,11 +1474,15 @@ fun BoxScope.FloatingButtons(
TopEndFloatingButton(
Modifier.padding(end = DEFAULT_PADDING, top = 24.dp + topPaddingToContent(true)).align(Alignment.TopEnd),
topUnreadCount,
animatedScrollingInProgress,
onClick = {
val index = mergedItems.value.items.indexOfLast { it.hasUnread() }
if (index != -1) {
// scroll to the top unread item
scope.launch { tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(index + 1, -maxHeight.value) } }
scope.launch {
animatedScrollingInProgress.value = true
tryBlockAndSetLoadingMore(loadingMoreItems) { listState.value.animateScrollToItem(index + 1, -maxHeight.value) }
}
}
},
onLongClick = { showDropDown.value = true }
@@ -1595,10 +1622,11 @@ fun MemberImage(member: GroupMember) {
private fun TopEndFloatingButton(
modifier: Modifier = Modifier,
unreadCount: State<Int>,
animatedScrollingInProgress: State<Boolean>,
onClick: () -> Unit,
onLongClick: () -> Unit
) {
if (unreadCount.value > 0) {
if (remember { derivedStateOf { unreadCount.value > 0 && !animatedScrollingInProgress.value } }.value) {
val interactionSource = interactionSourceWithDetection(onClick, onLongClick)
FloatingActionButton(
{}, // no action here
@@ -1839,6 +1867,7 @@ private fun lastFullyVisibleIemInListState(topPaddingToContentPx: State<Int>, de
private fun scrollToItem(
searchValue: State<String>,
loadingMoreItems: MutableState<Boolean>,
animatedScrollingInProgress: MutableState<Boolean>,
highlightedItems: MutableState<Set<Long>>,
chatInfo: State<ChatInfo>,
maxHeight: State<Int>,
@@ -1876,6 +1905,7 @@ private fun scrollToItem(
highlightedItems.value = setOf(itemId)
} else {
withContext(scope.coroutineContext) {
animatedScrollingInProgress.value = true
listState.value.animateScrollToItem(min(reversedChatItems.value.lastIndex, index + 1), -maxHeight.value)
highlightedItems.value = setOf(itemId)
}
@@ -1937,10 +1967,11 @@ private fun BoxScope.BottomEndFloatingButton(
unreadCount: State<Int>,
showButtonWithCounter: State<Boolean>,
showButtonWithArrow: State<Boolean>,
animatedScrollingInProgress: State<Boolean>,
composeViewHeight: State<Dp>,
onClick: () -> Unit
) = when {
showButtonWithCounter.value -> {
showButtonWithCounter.value && !animatedScrollingInProgress.value -> {
FloatingActionButton(
onClick = onClick,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
@@ -1954,7 +1985,7 @@ private fun BoxScope.BottomEndFloatingButton(
)
}
}
showButtonWithArrow.value -> {
showButtonWithArrow.value && !animatedScrollingInProgress.value -> {
FloatingActionButton(
onClick = onClick,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
@@ -8,8 +8,6 @@ import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.desktop.ui.tooling.preview.Preview
import java.net.URI
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
@@ -58,6 +56,19 @@ fun GroupMemberInfoView(
val developerTools = chatModel.controller.appPrefs.developerTools.get()
var progressIndicator by remember { mutableStateOf(false) }
fun syncMemberConnection() {
withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
connStats.value = r.second
withChats {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
close.invoke()
}
}
}
if (chat != null) {
val newRole = remember { mutableStateOf(member.memberRole) }
GroupMemberInfoLayout(
@@ -78,19 +89,30 @@ fun GroupMemberInfoView(
}
},
createMemberContact = {
withBGApi {
progressIndicator = true
val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId)
if (memberContact != null) {
val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf())
withChats {
addChat(memberChat)
openLoadedChat(memberChat)
if (connectionStats != null) {
if (connectionStats.ratchetSyncState == RatchetSyncState.Ok) {
withBGApi {
progressIndicator = true
val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId)
if (memberContact != null) {
val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf())
withChats {
addChat(memberChat)
openLoadedChat(memberChat)
}
closeAll()
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
}
progressIndicator = false
}
closeAll()
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
} else if (connectionStats.ratchetSyncAllowed) {
showFixConnectionAlert(syncConnection = { syncMemberConnection() })
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.cant_send_message_to_member_alert_title),
generalGetString(MR.strings.encryption_renegotiation_in_progress)
)
}
progressIndicator = false
}
},
connectViaAddress = { connReqUri ->
@@ -149,16 +171,7 @@ fun GroupMemberInfoView(
})
},
syncMemberConnection = {
withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
connStats.value = r.second
withChats {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
close.invoke()
}
}
syncMemberConnection()
},
syncMemberConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = {
@@ -335,9 +348,20 @@ fun GroupMemberInfoLayout(
val knownChat = if (contactId != null) knownDirectChat(contactId) else null
if (knownChat != null) {
val (chat, contact) = knownChat
val knownContactConnectionStats: MutableState<ConnectionStats?> = remember { mutableStateOf(null) }
LaunchedEffect(contact.contactId) {
withBGApi {
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, chat.chatInfo.apiId)
if (contactInfo != null) {
knownContactConnectionStats.value = contactInfo.first
}
}
}
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contact.contactId) })
AudioCallButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact)
VideoButton(modifier = Modifier.fillMaxWidth(1f), chat, contact)
AudioCallButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact, knownContactConnectionStats)
VideoButton(modifier = Modifier.fillMaxWidth(1f), chat, contact, knownContactConnectionStats)
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
if (contactId != null) {
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group
@@ -24,12 +24,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.currentUser
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlin.math.*
@@ -51,6 +51,12 @@ fun chatEventText(eventText: String, ts: String): AnnotatedString =
withStyle(chatEventStyle) { append("$eventText $ts") }
}
data class ChatItemReactionMenuItem (
val name: String,
val image: String?,
val onClick: (() -> Unit)?
)
@Composable
fun ChatItemView(
rhId: Long?,
@@ -87,6 +93,7 @@ fun ChatItemView(
showItemDetails: (ChatInfo, ChatItem) -> Unit,
reveal: (Boolean) -> Unit,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
showChatInfo: () -> Unit,
developerTools: Boolean,
showViaProxy: Boolean,
showTimestamp: Boolean,
@@ -120,7 +127,7 @@ fun ChatItemView(
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.chatItemOffset(cItem, itemSeparation.largeGap, inverted = true, revealed = true)) {
cItem.reactions.forEach { r ->
val showReactionMenu = remember { mutableStateOf(false) }
val reactionMembers = remember { mutableStateOf(emptyList<MemberReaction>()) }
val reactionMenuItems = remember { mutableStateOf(emptyList<ChatItemReactionMenuItem>()) }
val interactionSource = remember { MutableInteractionSource() }
val enterInteraction = remember { HoverInteraction.Enter() }
KeyChangeEffect(highlighted.value) {
@@ -134,18 +141,39 @@ fun ChatItemView(
var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp))
if (cInfo.featureEnabled(ChatFeature.Reactions)) {
fun showReactionsMenu() {
if (cInfo is ChatInfo.Group) {
withBGApi {
try {
val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction)
if (members != null) {
showReactionMenu.value = true
reactionMembers.value = members
when (cInfo) {
is ChatInfo.Group -> {
withBGApi {
try {
val members = controller.apiGetReactionMembers(rhId, cInfo.groupInfo.groupId, cItem.id, r.reaction)
if (members != null) {
showReactionMenu.value = true
reactionMenuItems.value = members.map {
val enabled = cInfo.groupInfo.membership.groupMemberId != it.groupMember.groupMemberId
val click = if (enabled) ({ showMemberInfo(cInfo.groupInfo, it.groupMember) }) else null
ChatItemReactionMenuItem(it.groupMember.displayName, it.groupMember.image, click)
}
}
} catch (e: Exception) {
Log.d(TAG, "chatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}")
}
} catch (e: Exception) {
Log.d(TAG, "hatItemView ChatItemReactions onLongClick: unexpected exception: ${e.stackTraceToString()}")
}
}
is ChatInfo.Direct -> {
showReactionMenu.value = true
val reactions = mutableListOf<ChatItemReactionMenuItem>()
if (!r.userReacted || r.totalReacted > 1) {
val contact = cInfo.contact
reactions.add(ChatItemReactionMenuItem(contact.displayName, contact.image, showChatInfo))
}
if (r.userReacted) {
reactions.add(ChatItemReactionMenuItem(generalGetString(MR.strings.sender_you_pronoun), currentUser.value?.image, null))
}
reactionMenuItems.value = reactions
}
else -> {}
}
}
modifier = modifier
@@ -166,19 +194,19 @@ fun ChatItemView(
Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) {
ReactionIcon(r.reaction.text, fontSize = 12.sp)
DefaultDropdownMenu(showMenu = showReactionMenu) {
reactionMembers.value.forEach { m ->
reactionMenuItems.value.forEach { m ->
ItemAction(
text = m.groupMember.displayName,
composable = { ProfileImage(44.dp, m.groupMember.image) },
text = m.name,
composable = { ProfileImage(44.dp, m.image) },
onClick = {
if (cInfo is ChatInfo.Group && cInfo.groupInfo.membership.groupMemberId != m.groupMember.groupMemberId) {
showMemberInfo(cInfo.groupInfo, m.groupMember)
showReactionMenu.value = false
} else {
val click = m.onClick
if (click != null) {
click()
showReactionMenu.value = false
}
},
lineLimit = 1
lineLimit = 1,
color = if (m.onClick == null) MaterialTheme.colors.secondary else MenuTextColor
)
}
}
@@ -1188,6 +1216,7 @@ fun PreviewChatItemView(
showItemDetails = { _, _ -> },
reveal = {},
showMemberInfo = { _, _ ->},
showChatInfo = {},
developerTools = false,
showViaProxy = false,
showTimestamp = true,
@@ -1233,6 +1262,7 @@ fun PreviewChatItemViewDeletedContent() {
showItemDetails = { _, _ -> },
reveal = {},
showMemberInfo = { _, _ ->},
showChatInfo = {},
developerTools = false,
showViaProxy = false,
preview = true,
@@ -30,6 +30,7 @@ import kotlinx.datetime.*
import java.io.*
import java.net.URI
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
@@ -44,11 +45,14 @@ fun DatabaseView() {
val chatArchiveFile = remember { mutableStateOf<String?>(null) }
val stopped = remember { m.chatRunning }.value == false
val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? ->
val file = chatArchiveFile.value
if (file != null && to != null) {
copyFileToFile(File(file), to) {
chatArchiveFile.value = null
}
val archive = chatArchiveFile.value
if (archive != null && to != null) {
copyFileToFile(File(archive), to) {}
}
// delete no matter the database was exported or canceled the export process
if (archive != null) {
File(archive).delete()
chatArchiveFile.value = null
}
}
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) }
@@ -680,6 +684,8 @@ suspend fun importArchive(
} finally {
File(archivePath).delete()
}
} else {
progressIndicator.value = false
}
return false
}
@@ -691,14 +697,15 @@ private fun saveArchiveFromURI(importedArchiveURI: URI): String? {
if (inputStream != null && archiveName != null) {
val archivePath = "$databaseExportDir${File.separator}$archiveName"
val destFile = File(archivePath)
Files.copy(inputStream, destFile.toPath())
Files.copy(inputStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
archivePath
} else {
Log.e(TAG, "saveArchiveFromURI null inputStream")
null
}
} catch (e: Exception) {
Log.e(TAG, "saveArchiveFromURI error: ${e.message}")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_database), e.stackTraceToString())
Log.e(TAG, "saveArchiveFromURI error: ${e.stackTraceToString()}")
null
}
}
@@ -14,6 +14,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -44,6 +45,12 @@ fun DeveloperView(withAuth: (title: String, desc: String, block: () -> Unit) ->
if (devTools.value) {
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(MR.strings.developer_options_section).uppercase()) {
SettingsActionItemWithContent(painterResource(MR.images.ic_breaking_news), stringResource(MR.strings.debug_logs)) {
DefaultSwitch(
checked = remember { appPrefs.logLevel.state }.value <= LogLevel.DEBUG,
onCheckedChange = { appPrefs.logLevel.set(if (it) LogLevel.DEBUG else LogLevel.WARNING) }
)
}
SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
if (appPlatform.isDesktop) {
TerminalAlwaysVisibleItem(m.controller.appPrefs.terminalAlwaysVisible) { checked ->
@@ -735,7 +735,10 @@ private fun ConditionsAppliedToOtherOperatorsText(userServers: List<UserOperator
}
if (otherOperatorsToApply.value.isNotEmpty()) {
ReadableText(MR.strings.operator_conditions_will_be_applied)
ReadableText(
MR.strings.operator_conditions_will_be_applied,
args = otherOperatorsToApply.value.joinToString(", ") { it.legalName_ }
)
}
}
@@ -524,6 +524,10 @@
<string name="sync_connection_force_question">Renegotiate encryption?</string>
<string name="sync_connection_force_desc">The encryption is working and the new encryption agreement is not required. It may result in connection errors!</string>
<string name="sync_connection_force_confirm">Renegotiate</string>
<string name="sync_connection_question">Fix connection?</string>
<string name="sync_connection_desc">Connection requires encryption renegotiation.</string>
<string name="sync_connection_confirm">Fix</string>
<string name="encryption_renegotiation_in_progress">Encryption renegotiation in progress.</string>
<string name="view_security_code">View security code</string>
<string name="verify_security_code">Verify security code</string>
@@ -903,6 +907,7 @@
<string name="show_dev_options">Show:</string>
<string name="hide_dev_options">Hide:</string>
<string name="show_developer_options">Show developer options</string>
<string name="debug_logs">Enable logs</string>
<string name="developer_options">Database IDs and Transport isolation option.</string>
<string name="developer_options_section">Developer options</string>
<string name="show_internal_errors">Show internal errors</string>
@@ -1334,6 +1339,7 @@
<string name="chat_database_exported_migrate">You may migrate the exported database.</string>
<string name="chat_database_exported_not_all_files">Some file(s) were not exported</string>
<string name="chat_database_exported_continue">Continue</string>
<string name="error_saving_database">Error saving database</string>
<!-- DatabaseEncryptionView.kt -->
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
@@ -2402,7 +2408,7 @@
<string name="servers_info_messages_sent">Messages sent</string>
<string name="servers_info_messages_received">Messages received</string>
<string name="servers_info_details">Details</string>
<string name="servers_info_private_data_disclaimer">Starting from %s.\nAll data is private to your device.</string>
<string name="servers_info_private_data_disclaimer">Starting from %s.\nAll data is kept private on your device..</string>
<string name="servers_info_subscriptions_section_header">Message reception</string>
<string name="servers_info_subscriptions_connections_subscribed">Active connections</string>
<string name="servers_info_subscriptions_connections_pending">Pending</string>
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22px" viewBox="0 -960 960 960" width="22px" fill="#5f6368"><path d="M262.5-285q11.5 0 20.25-8.5t8.75-20q0-11.5-8.75-20.25t-20.25-8.75q-11.5 0-20 8.75T234-313.5q0 11.5 8.5 20t20 8.5Zm-29-168.5H291v-227h-57.5v227Zm217.5 174h275.5V-337H451v57.5Zm0-174h275.5V-511H451v57.5Zm0-169.5h275.5v-57.5H451v57.5ZM134.5-124.5q-22.97 0-40.23-17.27Q77-159.03 77-182v-596q0-22.97 17.27-40.23 17.26-17.27 40.23-17.27h691q22.97 0 40.23 17.27Q883-800.97 883-778v596q0 22.97-17.27 40.23-17.26 17.27-40.23 17.27h-691Zm0-57.5h691v-596h-691v596Zm0 0v-596 596Z"/></svg>

After

Width:  |  Height:  |  Size: 592 B

@@ -67,7 +67,7 @@ object NtfManager {
ntf.second.close()
} catch (e: Exception) {
// Can be java.lang.UnsupportedOperationException, for example. May do nothing
println("Failed to close notification: ${e.stackTraceToString()}")
Log.e(TAG, "Failed to close notification: ${e.stackTraceToString()}")
}*/
}
}
@@ -85,7 +85,8 @@ object NtfManager {
}
fun cancelAllNotifications() {
// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { println("Failed to close notification: ${e.stackTraceToString()}") } }
// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { Log.e(TAG, "Failed to close notification: ${e
// .stackTraceToString()}") } }
withBGApi {
prevNtfsMutex.withLock {
prevNtfs.clear()
@@ -153,7 +154,7 @@ object NtfManager {
ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream())
newFile.absolutePath
} catch (e: Exception) {
println("Failed to write an icon to tmpDir: ${e.stackTraceToString()}")
Log.e(TAG, "Failed to write an icon to tmpDir: ${e.stackTraceToString()}")
null
}
} else null
@@ -1,8 +1,10 @@
package chat.simplex.common.platform
import chat.simplex.common.model.ChatController.appPrefs
actual object Log {
actual fun d(tag: String, text: String) = println("D: $text")
actual fun e(tag: String, text: String) = println("E: $text")
actual fun i(tag: String, text: String) = println("I: $text")
actual fun w(tag: String, text: String) = println("W: $text")
actual fun d(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.DEBUG && appPrefs.developerTools.get()) println("D: $text") }
actual fun e(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.ERROR || !appPrefs.developerTools.get()) println("E: $text") }
actual fun i(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.INFO && appPrefs.developerTools.get()) println("I: $text") }
actual fun w(tag: String, text: String) { if (appPrefs.logLevel.get() <= LogLevel.WARNING || !appPrefs.developerTools.get()) println("W: $text") }
}
+4 -4
View File
@@ -24,11 +24,11 @@ android.nonTransitiveRClass=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=6.2
android.version_code=259
android.version_name=6.2.1
android.version_code=261
desktop.version_name=6.2
desktop.version_code=82
desktop.version_name=6.2.1
desktop.version_code=83
kotlin.version=1.9.23
gradle.plugin.version=8.2.0