Compare commits

..

3 Commits

Author SHA1 Message Date
Alexander Bondarenko 58ad6098aa fixup! 2024-03-29 16:20:51 +03:00
Alexander Bondarenko 20b7d6623b WIP 2024-03-29 15:36:22 +03:00
Alexander Bondarenko fe4176357e ios: limit RTS memory for NSE 2024-03-29 13:56:41 +03:00
184 changed files with 1767 additions and 6681 deletions
+4 -4
View File
@@ -55,22 +55,22 @@ jobs:
ghc: "8.10.7"
cache_path: ~/.cabal/store
- os: ubuntu-20.04
ghc: "9.6.4"
ghc: "9.6.3"
cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-20_04-x86-64
desktop_asset_name: simplex-desktop-ubuntu-20_04-x86_64.deb
- os: ubuntu-22.04
ghc: "9.6.4"
ghc: "9.6.3"
cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-22_04-x86-64
desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb
- os: macos-latest
ghc: "9.6.4"
ghc: "9.6.3"
cache_path: ~/.cabal/store
asset_name: simplex-chat-macos-x86-64
desktop_asset_name: simplex-desktop-macos-x86_64.dmg
- os: windows-latest
ghc: "9.6.4"
ghc: "9.6.3"
cache_path: C:/cabal
asset_name: simplex-chat-windows-x86-64
desktop_asset_name: simplex-desktop-windows-x86_64.msi
+1 -1
View File
@@ -8,7 +8,7 @@ FROM ubuntu:${TAG} AS build
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev libssl-dev
# Specify bootstrap Haskell versions
ENV BOOTSTRAP_HASKELL_GHC_VERSION=9.6.4
ENV BOOTSTRAP_HASKELL_GHC_VERSION=9.6.3
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.1.0
# Install ghcup
+1 -1
View File
@@ -4,7 +4,7 @@
[![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat)
<a rel="me" href="https://mastodon.social/@simplex">![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)</a>
| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md), [PL](/docs/lang/pl/README.md) |
| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) |
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
-19
View File
@@ -103,7 +103,6 @@ final class ChatModel: ObservableObject {
// tracks keyboard height via subscription in AppDelegate
@Published var keyboardHeight: CGFloat = 0
@Published var pasteboardHasStrings: Bool = UIPasteboard.general.hasStrings
@Published var networkInfo = UserNetworkInfo(networkType: .other, online: true)
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
@@ -779,24 +778,6 @@ final class Chat: ObservableObject, Identifiable {
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
func groupFeatureEnabled(_ feature: GroupFeature) -> Bool {
if case let .group(groupInfo) = self.chatInfo {
let p = groupInfo.fullGroupPreferences
return switch feature {
case .timedMessages: p.timedMessages.on
case .directMessages: p.directMessages.on(for: groupInfo.membership)
case .fullDelete: p.fullDelete.on
case .reactions: p.reactions.on
case .voice: p.voice.on(for: groupInfo.membership)
case .files: p.files.on(for: groupInfo.membership)
case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership)
case .history: p.history.on
}
} else {
return true
}
}
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
}
@@ -1,73 +0,0 @@
//
// NetworkObserver.swift
// SimpleX (iOS)
//
// Created by Avently on 05.04.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import Foundation
import Network
import SimpleXChat
class NetworkObserver {
static let shared = NetworkObserver()
private let queue: DispatchQueue = DispatchQueue(label: "chat.simplex.app.NetworkObserver")
private var prevInfo: UserNetworkInfo? = nil
private var monitor: NWPathMonitor?
private let monitorLock: DispatchQueue = DispatchQueue(label: "chat.simplex.app.monitorLock")
func restartMonitor() {
monitorLock.sync {
monitor?.cancel()
let mon = NWPathMonitor()
mon.pathUpdateHandler = { [weak self] path in
self?.networkPathChanged(path: path)
}
mon.start(queue: queue)
monitor = mon
}
}
private func networkPathChanged(path: NWPath) {
let info = UserNetworkInfo(
networkType: networkTypeFromPath(path),
online: path.status == .satisfied
)
if (prevInfo != info) {
prevInfo = info
setNetworkInfo(info)
}
}
private func networkTypeFromPath(_ path: NWPath) -> UserNetworkType {
if path.usesInterfaceType(.wiredEthernet) {
.ethernet
} else if path.usesInterfaceType(.wifi) {
.wifi
} else if path.usesInterfaceType(.cellular) {
.cellular
} else if path.usesInterfaceType(.other) {
.other
} else {
.none
}
}
private static var networkObserver: NetworkObserver? = nil
private func setNetworkInfo(_ info: UserNetworkInfo) {
logger.debug("setNetworkInfo Network changed: \(String(describing: info))")
DispatchQueue.main.sync {
ChatModel.shared.networkInfo = info
}
if !hasChatCtrl() { return }
self.monitorLock.sync {
do {
try apiSetNetworkInfo(info)
} catch let err {
logger.error("setNetworkInfo error: \(responseError(err))")
}
}
}
}
+43 -33
View File
@@ -21,8 +21,6 @@ public let CURRENT_CHAT_VERSION: Int = 2
// version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core)
public let CREATE_MEMBER_CONTACT_VRANGE = VersionRange(minVersion: 2, maxVersion: CURRENT_CHAT_VERSION)
private let networkStatusesLock = DispatchQueue(label: "chat.simplex.app.network-statuses.lock")
enum TerminalItem: Identifiable {
case cmd(Date, ChatCommand)
case resp(Date, ChatResponse)
@@ -534,12 +532,6 @@ func setNetworkConfig(_ cfg: NetCfg, ctrl: chat_ctrl? = nil) throws {
throw r
}
func apiSetNetworkInfo(_ networkInfo: UserNetworkInfo) throws {
let r = chatSendCmdSync(.apiSetNetworkInfo(networkInfo: networkInfo))
if case .cmdOk = r { return }
throw r
}
func reconnectAllServers() async throws {
try await sendCommandOkResp(.reconnectAllServers)
}
@@ -1303,7 +1295,6 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
defer { m.ctrlInitInProgress = false }
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey, confirmMigrations: confirmMigrations)
if m.chatDbStatus != .ok { return }
NetworkObserver.shared.restartMonitor()
// If we migrated successfully means previous re-encryption process on database level finished successfully too
if encryptionStartedDefault.get() {
encryptionStartedDefault.set(false)
@@ -1469,8 +1460,6 @@ class ChatReceiver {
private var receiveMessages = true
private var _lastMsgTime = Date.now
var messagesChannel: ((ChatResponse) -> Void)? = nil
static let shared = ChatReceiver()
var lastMsgTime: Date { get { _lastMsgTime } }
@@ -1488,9 +1477,6 @@ class ChatReceiver {
if let msg = await chatRecvMsg() {
self._lastMsgTime = .now
await processReceivedMsg(msg)
if let messagesChannel {
messagesChannel(msg)
}
}
_ = try? await Task.sleep(nanoseconds: 7_500_000)
}
@@ -1580,30 +1566,34 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.removeChat(mergedContact.id)
}
}
case let .networkStatus(status, connections):
// dispatch queue to synchronize access
networkStatusesLock.sync {
var ns = m.networkStatuses
// slow loop is on the background thread
for cId in connections {
ns[cId] = status
case let .contactsSubscribed(_, contactRefs):
await updateContactsStatus(contactRefs, status: .connected)
case let .contactsDisconnected(_, contactRefs):
await updateContactsStatus(contactRefs, status: .disconnected)
case let .contactSubSummary(_, contactSubscriptions):
await MainActor.run {
for sub in contactSubscriptions {
// no need to update contact here, and it is slow
// if active(user) {
// m.updateContact(sub.contact)
// }
if let err = sub.contactError {
processContactSubError(sub.contact, err)
} else {
m.setContactNetworkStatus(sub.contact, .connected)
}
}
// fast model update is on the main thread
DispatchQueue.main.sync {
m.networkStatuses = ns
}
case let .networkStatus(status, connections):
await MainActor.run {
for cId in connections {
m.networkStatuses[cId] = status
}
}
case let .networkStatuses(_, statuses): ()
// dispatch queue to synchronize access
networkStatusesLock.sync {
var ns = m.networkStatuses
// slow loop is on the background thread
await MainActor.run {
for s in statuses {
ns[s.agentConnId] = s.networkStatus
}
// fast model update is on the main thread
DispatchQueue.main.sync {
m.networkStatuses = ns
m.networkStatuses[s.agentConnId] = s.networkStatus
}
}
case let .newChatItem(user, aChatItem):
@@ -1954,6 +1944,26 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async {
}
}
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async {
let m = ChatModel.shared
await MainActor.run {
for c in contactRefs {
m.networkStatuses[c.agentConnId] = status
}
}
}
func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
let m = ChatModel.shared
var err: String
switch chatError {
case .errorAgent(agentError: .BROKER(_, .NETWORK)): err = "network"
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
default: err = String(describing: chatError)
}
m.setContactNetworkStatus(contact, .error(connectionError: err))
}
func refreshCallInvitations() throws {
let m = ChatModel.shared
let callInvitations = try justRefreshCallInvitations()
@@ -9,7 +9,6 @@
import SwiftUI
import WebKit
import SimpleXChat
import AVFoundation
struct ActiveCallView: View {
@EnvironmentObject var m: ChatModel
@@ -22,7 +21,6 @@ struct ActiveCallView: View {
@Binding var canConnectCall: Bool
@State var prevColorScheme: ColorScheme = .dark
@State var pipShown = false
@State var wasConnected = false
var body: some View {
ZStack(alignment: .topLeading) {
@@ -71,11 +69,6 @@ struct ActiveCallView: View {
Task { await m.callCommand.setClient(nil) }
AppDelegate.keepScreenOn(false)
client?.endCall()
CallSoundsPlayer.shared.stop()
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient)
if (wasConnected) {
CallSoundsPlayer.shared.vibrate(long: true)
}
}
.background(m.activeCallViewIsCollapsed ? .clear : .black)
// Quite a big delay when opening/closing the view when a scheme changes (globally) this way. It's not needed when CallKit is used since status bar is green with white text on it
@@ -110,11 +103,6 @@ struct ActiveCallView: View {
call.callState = .invitationSent
call.localCapabilities = capabilities
}
if call.supportsVideo {
try? AVAudioSession.sharedInstance().setCategory(.playback, options: .defaultToSpeaker)
}
CallSoundsPlayer.shared.startConnectingCallSound()
activeCallWaitDeliveryReceipt()
}
case let .offer(offer, iceCandidates, capabilities):
Task {
@@ -138,8 +126,6 @@ struct ActiveCallView: View {
}
await MainActor.run {
call.callState = .negotiated
CallSoundsPlayer.shared.stop()
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient)
}
}
case let .ice(iceCandidates):
@@ -158,10 +144,6 @@ struct ActiveCallView: View {
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
call.callState = .connected
call.connectedAt = .now
if !wasConnected {
CallSoundsPlayer.shared.vibrate(long: false)
wasConnected = true
}
}
if state.connectionState == "closed" {
closeCallView(client)
@@ -179,10 +161,6 @@ struct ActiveCallView: View {
call.callState = .connected
call.connectionInfo = connectionInfo
call.connectedAt = .now
if !wasConnected {
CallSoundsPlayer.shared.vibrate(long: false)
wasConnected = true
}
case .ended:
closeCallView(client)
call.callState = .ended
@@ -209,22 +187,6 @@ struct ActiveCallView: View {
}
}
private func activeCallWaitDeliveryReceipt() {
ChatReceiver.shared.messagesChannel = { msg in
guard let call = ChatModel.shared.activeCall, call.callState == .invitationSent else {
ChatReceiver.shared.messagesChannel = nil
return
}
if case let .chatItemStatusUpdated(_, msg) = msg,
msg.chatInfo.id == call.contact.id,
case .sndCall = msg.chatItem.content,
case .sndRcvd = msg.chatItem.meta.itemStatus {
CallSoundsPlayer.shared.startInCallSound()
ChatReceiver.shared.messagesChannel = nil
}
}
}
private func closeCallView(_ client: WebRTCClient) {
if m.activeCall != nil {
m.showCallView = false
@@ -8,7 +8,6 @@
import Foundation
import AVFoundation
import UIKit
class SoundPlayer {
static let shared = SoundPlayer()
@@ -44,63 +43,3 @@ class SoundPlayer {
audioPlayer = nil
}
}
class CallSoundsPlayer {
static let shared = CallSoundsPlayer()
private var audioPlayer: AVAudioPlayer?
private var playerTask: Task = Task {}
private func start(_ soundName: String, delayMs: Double) {
audioPlayer?.stop()
playerTask.cancel()
logger.debug("start \(soundName)")
guard let path = Bundle.main.path(forResource: soundName, ofType: "mp3", inDirectory: "sounds") else {
logger.debug("start: file not found")
return
}
do {
let player = try AVAudioPlayer(contentsOf: URL(fileURLWithPath: path))
if player.prepareToPlay() {
audioPlayer = player
}
} catch {
logger.debug("start: AVAudioPlayer error \(error.localizedDescription)")
}
playerTask = Task {
while let player = audioPlayer {
player.play()
do {
try await Task.sleep(nanoseconds: UInt64((player.duration * 1_000_000_000) + delayMs * 1_000_000))
} catch {
break
}
}
}
}
func startConnectingCallSound() {
start("connecting_call", delayMs: 0)
}
func startInCallSound() {
// Taken from https://github.com/TelegramOrg/Telegram-Android
// https://github.com/TelegramOrg/Telegram-Android/blob/master/LICENSE
start("in_call", delayMs: 1000)
}
func stop() {
playerTask.cancel()
audioPlayer?.stop()
audioPlayer = nil
}
func vibrate(long: Bool) {
// iOS just don't want to vibrate more than once after a short period of time, and all 'styles' feel the same
if long {
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
} else {
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
}
}
}
+3 -4
View File
@@ -431,18 +431,17 @@ struct RTCIceServer: Codable, Equatable {
}
// the servers are expected in this format:
// stuns:stun.simplex.im:443?transport=tcp
// turns:private2:Hxuq2QxUjnhj96Zq2r4HjqHRj@turn.simplex.im:443?transport=tcp
// stun:stun.simplex.im:443?transport=tcp
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp
func parseRTCIceServer(_ str: String) -> RTCIceServer? {
var s = replaceScheme(str, "stun:")
s = replaceScheme(s, "stuns:")
s = replaceScheme(s, "turn:")
s = replaceScheme(s, "turns:")
if let u: URL = URL(string: s),
let scheme = u.scheme,
let host = u.host,
let port = u.port,
u.path == "" && (scheme == "stun" || scheme == "stuns" || scheme == "turn" || scheme == "turns") {
u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns") {
let query = u.query == nil || u.query == "" ? "" : "?" + (u.query ?? "")
return RTCIceServer(
urls: ["\(scheme):\(host):\(port)\(query)"],
@@ -49,7 +49,7 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
}
private let rtcAudioSession = RTCAudioSession.sharedInstance()
private let audioQueue = DispatchQueue(label: "chat.simplex.app.audio")
private let audioQueue = DispatchQueue(label: "audio")
private var sendCallResponse: (WVAPIMessage) async -> Void
var activeCall: Binding<Call?>
private var localRendererAspectRatio: Binding<CGFloat?>
@@ -65,14 +65,14 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
self.localRendererAspectRatio = localRendererAspectRatio
rtcAudioSession.useManualAudio = CallController.useCallKit()
rtcAudioSession.isAudioEnabled = !CallController.useCallKit()
logger.debug("WebRTCClient: rtcAudioSession has manual audio \(self.rtcAudioSession.useManualAudio) and audio enabled \(self.rtcAudioSession.isAudioEnabled)")
logger.debug("WebRTCClient: rtcAudioSession has manual audio \(self.rtcAudioSession.useManualAudio) and audio enabled \(self.rtcAudioSession.isAudioEnabled)}")
super.init()
}
let defaultIceServers: [WebRTC.RTCIceServer] = [
WebRTC.RTCIceServer(urlStrings: ["stuns:stun.simplex.im:443"]),
//WebRTC.RTCIceServer(urlStrings: ["turns:turn.simplex.im:443?transport=udp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"),
WebRTC.RTCIceServer(urlStrings: ["turns:turn.simplex.im:443?transport=tcp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj"),
WebRTC.RTCIceServer(urlStrings: ["stun:stun.simplex.im:443"]),
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
]
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
@@ -11,7 +11,6 @@ import SimpleXChat
struct CIChatFeatureView: View {
@EnvironmentObject var m: ChatModel
@ObservedObject var chat: Chat
var chatItem: ChatItem
@Binding var revealed: Bool
var feature: Feature
@@ -19,7 +18,7 @@ struct CIChatFeatureView: View {
var iconColor: Color
var body: some View {
if !revealed, let fs = mergedFeatures() {
if !revealed, let fs = mergedFeautures() {
HStack {
ForEach(fs, content: featureIconView)
}
@@ -48,7 +47,7 @@ struct CIChatFeatureView: View {
}
}
private func mergedFeatures() -> [FeatureInfo]? {
private func mergedFeautures() -> [FeatureInfo]? {
var fs: [FeatureInfo] = []
var icons: Set<String> = []
if var i = m.getChatItemIndex(chatItem) {
@@ -68,8 +67,8 @@ struct CIChatFeatureView: View {
switch ci.content {
case let .rcvChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param)
case let .sndChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param)
case let .rcvGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param)
case let .sndGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param)
case let .rcvGroupFeature(feature, preference, param): FeatureInfo(feature, preference.enable.iconColor, param)
case let .sndGroupFeature(feature, preference, param): FeatureInfo(feature, preference.enable.iconColor, param)
default: nil
}
}
@@ -104,6 +103,6 @@ struct CIChatFeatureView: View {
struct CIChatFeatureView_Previews: PreviewProvider {
static var previews: some View {
let enabled = FeatureEnabled(forUser: false, forContact: false)
CIChatFeatureView(chat: Chat.sampleData, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor)
CIChatFeatureView(chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor)
}
}
@@ -70,14 +70,14 @@ struct CIImageView: View {
}
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : img.imageData == nil ? .infinity : maxWidth
DispatchQueue.main.async { imgWidth = w }
return ZStack(alignment: .topTrailing) {
if img.imageData == nil {
Image(uiImage: img)
.resizable()
.scaledToFit()
.frame(width: w)
.frame(maxWidth: w)
} else {
SwiftyGif(image: img)
.frame(width: w, height: w * img.size.height / img.size.width)
@@ -126,7 +126,7 @@ struct CIVideoView: View {
if !decryptionInProgress {
Button {
decrypt(file: file) {
if urlDecrypted != nil {
if let decrypted = urlDecrypted {
videoPlaying = true
player?.play()
}
@@ -243,13 +243,13 @@ struct CIVideoView: View {
}
private func imageView(_ img: UIImage) -> some View {
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : .infinity
DispatchQueue.main.async { videoWidth = w }
return ZStack(alignment: .topTrailing) {
Image(uiImage: img)
.resizable()
.scaledToFit()
.frame(width: w)
.frame(maxWidth: w)
loadingIndicator()
}
}
@@ -65,8 +65,6 @@ struct FramedItemView: View {
}
}
}
} else if chatItem.meta.itemForwarded != nil {
framedItemHeader(icon: "arrowshape.turn.up.forward", caption: Text("forwarded").italic())
}
ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: framedMsgContentView)
@@ -355,9 +353,9 @@ private struct MetaColorPreferenceKey: PreferenceKey {
func onlyImageOrVideo(_ ci: ChatItem) -> Bool {
if case let .image(text, _) = ci.content.msgContent {
return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && ci.meta.itemForwarded == nil && text == ""
return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && text == ""
} else if case let .video(text, _, _) = ci.content.msgContent {
return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && ci.meta.itemForwarded == nil && text == ""
return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && text == ""
}
return false
}
@@ -46,7 +46,7 @@ struct ChatItemView: View {
let ci = chatItem
if chatItem.meta.itemDeleted != nil && (!revealed || chatItem.isDeletedContent) {
MarkedDeletedItemView(chat: chat, chatItem: chatItem, revealed: $revealed)
} else if ci.quotedItem == nil && ci.meta.itemForwarded == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
} else if ci.quotedItem == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive {
if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) {
EmojiItemView(chat: chat, chatItem: ci)
} else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent {
@@ -102,9 +102,9 @@ struct ChatItemContentView<Content: View>: View {
case let .rcvChatPreference(feature, allowed, param):
CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param)
case let .sndChatPreference(feature, _, _):
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary)
case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor)
case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor)
CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary)
case let .rcvGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
case let .sndGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red)
case .sndModerated: deletedItemView()
@@ -149,7 +149,7 @@ struct ChatItemContentView<Content: View>: View {
}
private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View {
CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor)
CIChatFeatureView(chatItem: chatItem, revealed: $revealed, feature: feature, iconColor: iconColor)
}
private var mergedGroupEventText: Text? {
+4 -9
View File
@@ -17,7 +17,6 @@ struct ChatView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
@Environment(\.scenePhase) var scenePhase
@State @ObservedObject var chat: Chat
@State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false
@@ -235,9 +234,7 @@ struct ChatView: View {
private func initChatView() {
let cInfo = chat.chatInfo
// This check prevents the call to apiContactInfo after the app is suspended, and the database is closed.
if case .active = scenePhase,
case let .direct(contact) = cInfo {
if case let .direct(contact) = cInfo {
Task {
do {
let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId)
@@ -296,7 +293,7 @@ struct ChatView: View {
}
private func voiceWithoutFrame(_ ci: ChatItem) -> Bool {
ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil && ci.meta.itemForwarded == nil
ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil
}
private func chatItemsList() -> some View {
@@ -514,7 +511,6 @@ struct ChatView: View {
chat: chat,
chatItem: ci,
maxWidth: maxWidth,
itemWidth: maxWidth,
composeState: $composeState,
selectedMember: $selectedMember,
chatView: self
@@ -527,7 +523,6 @@ struct ChatView: View {
@ObservedObject var chat: Chat
var chatItem: ChatItem
var maxWidth: CGFloat
@State var itemWidth: CGFloat
@Binding var composeState: ComposeState
@Binding var selectedMember: GMember?
var chatView: ChatView
@@ -656,7 +651,7 @@ struct ChatView: View {
playbackState: $playbackState,
playbackTime: $playbackTime
)
.uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu)
.uiKitContextMenu(maxWidth: maxWidth, menu: uiMenu, allowMenu: $allowMenu)
.accessibilityLabel("")
if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 {
chatItemReactions(ci)
@@ -667,7 +662,7 @@ struct ChatView: View {
Button("Delete for me", role: .destructive) {
deleteMessage(.cidmInternal)
}
if let di = deletingItem, di.meta.deletable && !di.localNote {
if let di = deletingItem, di.meta.editable && !di.localNote {
Button(broadcastDeleteButtonText, role: .destructive) {
deleteMessage(.cidmBroadcast)
}
@@ -161,16 +161,6 @@ struct ComposeState {
}
}
var attachmentPreview: Bool {
switch preview {
case .noPreview: false
case .linkPreview: false
case let .mediaPreviews(mediaPreviews): !mediaPreviews.isEmpty
case .voicePreview: false
case .filePreview: true
}
}
var empty: Bool {
message == "" && noPreview
}
@@ -244,7 +234,6 @@ struct ComposeView: View {
@Binding var keyboardVisible: Bool
@State var linkUrl: URL? = nil
@State var hasSimplexLink: Bool = false
@State var prevLinkUrl: URL? = nil
@State var pendingLinkUrl: URL? = nil
@State var cancelledLinks: Set<String> = []
@@ -271,16 +260,6 @@ struct ComposeView: View {
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
ContextInvitingContactMemberView()
}
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
if simplexLinkProhibited {
msgNotAllowedView("SimpleX links not allowed", icon: "link")
} else if fileProhibited {
msgNotAllowedView("Files and media not allowed", icon: "doc")
} else if voiceProhibited {
msgNotAllowedView("Voice messages not allowed", icon: "mic")
}
contextItemView()
switch (composeState.editing, composeState.preview) {
case (true, .filePreview): EmptyView()
@@ -299,7 +278,7 @@ struct ComposeView: View {
.padding(.bottom, 12)
.padding(.leading, 12)
if case let .group(g) = chat.chatInfo,
!g.fullGroupPreferences.files.on(for: g.membership) {
!g.fullGroupPreferences.files.on {
b.disabled(true).onTapGesture {
AlertManager.shared.showAlertMsg(
title: "Files and media prohibited!",
@@ -324,7 +303,6 @@ struct ComposeView: View {
},
nextSendGrpInv: chat.chatInfo.contact?.nextSendGrpInv ?? false,
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
disableSendButton: simplexLinkProhibited || fileProhibited || voiceProhibited,
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
startVoiceMessageRecording: {
Task {
@@ -359,18 +337,13 @@ struct ComposeView: View {
}
}
}
.onChange(of: composeState.message) { msg in
.onChange(of: composeState.message) { _ in
if composeState.linkPreviewAllowed {
if msg.count > 0 {
showLinkPreview(msg)
if composeState.message.count > 0 {
showLinkPreview(composeState.message)
} else {
resetLinkPreview()
hasSimplexLink = false
}
} else if msg.count > 0 && !chat.groupFeatureEnabled(.simplexLinks) {
(_, hasSimplexLink) = parseMessage(msg)
} else {
hasSimplexLink = false
}
}
.onChange(of: chat.userCanSend) { canSend in
@@ -637,18 +610,6 @@ struct ComposeView: View {
}
}
private func msgNotAllowedView(_ reason: LocalizedStringKey, icon: String) -> some View {
HStack {
Image(systemName: icon).foregroundColor(.secondary)
Text(reason).italic()
}
.padding(12)
.frame(minHeight: 50)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.padding(.top, 8)
}
@ViewBuilder private func contextItemView() -> some View {
switch composeState.contextItem {
case .noContextItem:
@@ -857,7 +818,7 @@ struct ComposeView: View {
func checkLinkPreview() -> MsgContent {
switch (composeState.preview) {
case let .linkPreview(linkPreview: linkPreview):
if let url = parseMessage(msgText).url,
if let url = parseMessage(msgText),
let linkPreview = linkPreview,
url == linkPreview.uri {
return .link(text: msgText, preview: linkPreview)
@@ -986,7 +947,7 @@ struct ComposeView: View {
private func showLinkPreview(_ s: String) {
prevLinkUrl = linkUrl
(linkUrl, hasSimplexLink) = parseMessage(s)
linkUrl = parseMessage(s)
if let url = linkUrl {
if url != composeState.linkPreview?.uri && url != pendingLinkUrl {
pendingLinkUrl = url
@@ -1003,17 +964,13 @@ struct ComposeView: View {
}
}
private func parseMessage(_ msg: String) -> (url: URL?, hasSimplexLink: Bool) {
guard let parsedMsg = parseSimpleXMarkdown(msg) else { return (nil, false) }
let url: URL? = if let uri = parsedMsg.first(where: { ft in
private func parseMessage(_ msg: String) -> URL? {
let parsedMsg = parseSimpleXMarkdown(msg)
let uri = parsedMsg?.first(where: { ft in
ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text)
}) {
URL(string: uri.text)
} else {
nil
}
let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
return (url, simplexLink)
})
if let uri = uri { return URL(string: uri.text) }
else { return nil }
}
private func isSimplexLink(_ link: String) -> Bool {
@@ -20,7 +20,6 @@ struct SendMessageView: View {
var nextSendGrpInv: Bool = false
var showVoiceMessageButton: Bool = true
var voiceMessageAllowed: Bool = true
var disableSendButton = false
var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other
var startVoiceMessageRecording: (() -> Void)? = nil
var finishVoiceMessageRecording: (() -> Void)? = nil
@@ -185,8 +184,7 @@ struct SendMessageView: View {
!composeState.sendEnabled ||
composeState.inProgress ||
(!voiceMessageAllowed && composeState.voicePreview) ||
composeState.endLiveDisabled ||
disableSendButton
composeState.endLiveDisabled
)
.frame(width: 29, height: 29)
.contextMenu{
@@ -83,7 +83,7 @@ struct GroupMemberInfoView: View {
Section {
if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) {
knownDirectChatButton(chat)
} else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) {
} else if groupInfo.fullGroupPreferences.directMessages.on {
if let contactId = member.memberContactId {
newDirectChatButton(contactId)
} else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false {
@@ -110,7 +110,7 @@ struct GroupMemberInfoView: View {
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 knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on {
connectViaAddressButton(contactLink)
}
} else {
@@ -9,12 +9,6 @@
import SwiftUI
import SimpleXChat
private let featureRoles: [(role: GroupMemberRole?, text: LocalizedStringKey)] = [
(nil, "all members"),
(.admin, "admins"),
(.owner, "owners")
]
struct GroupPreferencesView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject var chatModel: ChatModel
@@ -30,12 +24,10 @@ struct GroupPreferencesView: View {
List {
featureSection(.timedMessages, $preferences.timedMessages.enable)
featureSection(.fullDelete, $preferences.fullDelete.enable)
featureSection(.directMessages, $preferences.directMessages.enable, $preferences.directMessages.role)
featureSection(.directMessages, $preferences.directMessages.enable)
featureSection(.reactions, $preferences.reactions.enable)
featureSection(.voice, $preferences.voice.enable, $preferences.voice.role)
featureSection(.files, $preferences.files.enable, $preferences.files.role)
// TODO enable simplexLinks preference in 5.8
// featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
featureSection(.voice, $preferences.voice.enable)
featureSection(.files, $preferences.files.enable)
featureSection(.history, $preferences.history.enable)
if groupInfo.canEdit {
@@ -72,7 +64,7 @@ struct GroupPreferencesView: View {
}
}
private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>, _ enableForRole: Binding<GroupMemberRole?>? = nil) -> some View {
private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding<GroupFeatureEnabled>) -> some View {
Section {
let color: Color = enableFeature.wrappedValue == .on ? .green : .secondary
let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon
@@ -95,16 +87,6 @@ struct GroupPreferencesView: View {
)
.frame(height: 36)
}
if enableFeature.wrappedValue == .on, let enableForRole {
Picker("Enabled for", selection: enableForRole) {
ForEach(featureRoles, id: \.role) { fr in
Text(fr.text)
}
}
.frame(height: 36)
// remove in v5.8
.disabled(true)
}
} else {
settingsRow(icon, color: color) {
infoRow(Text(feature.text), enableFeature.wrappedValue.text)
@@ -112,26 +94,10 @@ struct GroupPreferencesView: View {
if timedOn {
infoRow("Delete after", timeText(preferences.timedMessages.ttl))
}
if enableFeature.wrappedValue == .on, let enableForRole {
HStack {
Text("Enabled for").foregroundColor(.secondary)
Spacer()
Text(
featureRoles.first(where: { fr in fr.role == enableForRole.wrappedValue })?.text
?? "all members"
)
.foregroundColor(.secondary)
}
}
}
} footer: {
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit))
}
.onChange(of: enableFeature.wrappedValue) { enabled in
if case .off = enabled {
enableForRole?.wrappedValue = nil
}
}
}
private func savePreferences() {
@@ -12,7 +12,6 @@ private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue
struct UserPicker: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@Environment(\.scenePhase) var scenePhase
@Binding var showSettings: Bool
@Binding var showConnectDesktop: Bool
@Binding var userPickerVisible: Bool
@@ -92,10 +91,7 @@ struct UserPicker: View {
.opacity(userPickerVisible ? 1.0 : 0.0)
.onAppear {
do {
// This check prevents the call of listUsers after the app is suspended, and the database is closed.
if case .active = scenePhase {
m.users = try listUsers()
}
m.users = try listUsers()
} catch let error {
logger.error("Error loading users \(responseError(error))")
}
@@ -11,20 +11,11 @@ import UIKit
import SwiftUI
extension View {
func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding<CGFloat>, menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some View {
func uiKitContextMenu(maxWidth: CGFloat, menu: Binding<UIMenu>, allowMenu: Binding<Bool>) -> some View {
Group {
if allowMenu.wrappedValue {
if hasImageOrVideo {
InteractionView(content:
self.environmentObject(ChatModel.shared)
.overlay(DetermineWidthImageVideoItem())
.onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 }
, maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
.frame(maxWidth: itemWidth.wrappedValue)
} else {
InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu)
.fixedSize(horizontal: true, vertical: false)
}
InteractionView(content: self, maxWidth: maxWidth, menu: menu)
.fixedSize(horizontal: true, vertical: false)
} else {
self
}
@@ -40,14 +31,13 @@ private class HostingViewHolder: UIView {
struct InteractionView<Content: View>: UIViewRepresentable {
let content: Content
var maxWidth: CGFloat
var itemWidth: Binding<CGFloat>
@Binding var menu: UIMenu
func makeUIView(context: Context) -> UIView {
let view = HostingViewHolder()
view.contentSize = CGSizeMake(maxWidth, .infinity)
view.backgroundColor = .clear
let hostView = UIHostingController(rootView: content)
view.contentSize = hostView.view.intrinsicContentSize
hostView.view.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
hostView.view.topAnchor.constraint(equalTo: view.topAnchor),
@@ -67,11 +57,7 @@ struct InteractionView<Content: View>: UIViewRepresentable {
}
func updateUIView(_ uiView: UIView, context: Context) {
let was = (uiView as! HostingViewHolder).contentSize
(uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(itemWidth.wrappedValue, .infinity))
if was != (uiView as! HostingViewHolder).contentSize {
uiView.invalidateIntrinsicContentSize()
}
(uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(maxWidth, .infinity))
}
func makeCoordinator() -> Coordinator {
@@ -21,19 +21,6 @@ struct DetermineWidth: View {
}
}
struct DetermineWidthImageVideoItem: View {
typealias Key = MaximumWidthImageVideoPreferenceKey
var body: some View {
GeometryReader { proxy in
Color.clear
.preference(
key: MaximumWidthImageVideoPreferenceKey.self,
value: proxy.size.width
)
}
}
}
struct MaximumWidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
@@ -41,13 +28,6 @@ struct MaximumWidthPreferenceKey: PreferenceKey {
}
}
struct MaximumWidthImageVideoPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct DetermineWidth_Previews: PreviewProvider {
static var previews: some View {
DetermineWidth()
@@ -448,9 +448,6 @@ struct MigrateToDevice: View {
case .rcvFileError:
alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
case .chatError(_, .error(.noRcvFileUser)):
alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
default:
logger.debug("unsupported event: \(msg.responseType)")
}
@@ -53,7 +53,7 @@ struct AdvancedNetworkSettings: View {
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [2_500, 5_000, 10_000, 15_000, 20_000, 30_000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [15_000, 30_000, 45_000, 60_000, 90_000, 120_000], label: secondsLabel)
timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel)
intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "")
Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive)
@@ -24,7 +24,6 @@ private enum NetworkAlert: Identifiable {
}
struct NetworkAndServers: View {
@EnvironmentObject var m: ChatModel
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State private var cfgLoaded = false
@State private var currentNetCfg = NetCfg.defaults
@@ -83,14 +82,6 @@ struct NetworkAndServers: View {
Text("WebRTC ICE servers")
}
}
Section("Network connection") {
HStack {
Text(m.networkInfo.networkType.text)
Spacer()
Image(systemName: "circle.fill").foregroundColor(m.networkInfo.online ? .green : .red)
}
}
}
}
.onAppear {
+41 -51
View File
@@ -65,24 +65,20 @@ class NSEThreads {
}
func processNotification(_ id: ChatId, _ ntf: NSENotification) async -> Void {
var waitTime: Int64 = 5_000_000000
while waitTime > 0 {
if let (_, nse) = rcvEntityThread(id),
nse.shouldProcessNtf && nse.processReceivedNtf(ntf) {
var timeoutOfWaiting: Int64 = 5_000_000000
while timeoutOfWaiting > 0 {
let activeThread = NSEThreads.queue.sync {
activeThreads.first(where: { (_, nse) in nse.receiveEntityId == id })
}
if activeThread?.1.processReceivedNtf?(ntf) == true {
break
} else {
try? await Task.sleep(nanoseconds: 10_000000)
waitTime -= 10_000000
timeoutOfWaiting -= 10_000000
}
}
}
private func rcvEntityThread(_ id: ChatId) -> (UUID, NotificationService)? {
NSEThreads.queue.sync {
activeThreads.first(where: { (_, nse) in nse.receiveEntityId == id })
}
}
func endThread(_ t: UUID) -> Bool {
NSEThreads.queue.sync {
let tActive: UUID? = if let index = activeThreads.firstIndex(where: { $0.0 == t }) {
@@ -117,11 +113,9 @@ class NotificationService: UNNotificationServiceExtension {
// thread is added to allThreads here - if thread did not start chat,
// chat does not need to be suspended but NSE state still needs to be set to "suspended".
var threadId: UUID? = NSEThreads.shared.newThread()
var notificationInfo: NtfMessages?
var receiveEntityId: String?
var expectedMessages: Set<String> = []
// return true if the message is taken - it prevents sending it to another NotificationService instance for processing
var shouldProcessNtf = false
var processReceivedNtf: ((NSENotification) -> Bool)?
var appSubscriber: AppSubscriber?
var returnedSuspension = false
@@ -198,11 +192,39 @@ class NotificationService: UNNotificationServiceExtension {
? .nse(createConnectionEventNtf(ntfInfo.user, connEntity))
: .empty
)
if let id = connEntity.id, ntfInfo.msgTs != nil {
notificationInfo = ntfInfo
if let id = connEntity.id, let msgTs = ntfInfo.msgTs {
var expected = Set(ntfInfo.ntfMessages.map { $0.msgId })
processReceivedNtf = { ntf in
if !ntfInfo.user.showNotifications {
self.setBestAttemptNtf(.empty)
}
if case let .msgInfo(info) = ntf {
let found = expected.remove(info.msgId)
if found != nil {
logger.debug("NotificationService processNtf: msgInfo, last: \(expected.isEmpty)")
if expected.isEmpty {
self.deliverBestAttemptNtf()
}
return true
} else if info.msgTs > msgTs {
logger.debug("NotificationService processNtf: unexpected msgInfo, let other instance to process it, stopping this one")
self.deliverBestAttemptNtf()
return false
} else {
logger.debug("NotificationService processNtf: unknown message, let other instance to process it")
return false
}
} else if ntfInfo.user.showNotifications {
logger.debug("NotificationService processNtf: setting best attempt")
self.setBestAttemptNtf(ntf)
if ntf.isCallInvitation {
self.deliverBestAttemptNtf()
}
return true
}
return false
}
receiveEntityId = id
expectedMessages = Set(ntfInfo.ntfMessages.map { $0.msgId })
shouldProcessNtf = true
return
}
}
@@ -218,38 +240,6 @@ class NotificationService: UNNotificationServiceExtension {
deliverBestAttemptNtf(urgent: true)
}
func processReceivedNtf(_ ntf: NSENotification) -> Bool {
guard let ntfInfo = notificationInfo, let msgTs = ntfInfo.msgTs else { return false }
if !ntfInfo.user.showNotifications {
self.setBestAttemptNtf(.empty)
}
if case let .msgInfo(info) = ntf {
let found = expectedMessages.remove(info.msgId)
if found != nil {
logger.debug("NotificationService processNtf: msgInfo, last: \(self.expectedMessages.isEmpty)")
if expectedMessages.isEmpty {
self.deliverBestAttemptNtf()
}
return true
} else if info.msgTs > msgTs {
logger.debug("NotificationService processNtf: unexpected msgInfo, let other instance to process it, stopping this one")
self.deliverBestAttemptNtf()
return false
} else {
logger.debug("NotificationService processNtf: unknown message, let other instance to process it")
return false
}
} else if ntfInfo.user.showNotifications {
logger.debug("NotificationService processNtf: setting best attempt")
self.setBestAttemptNtf(ntf)
if ntf.isCallInvitation {
self.deliverBestAttemptNtf()
}
return true
}
return false
}
func setBadgeCount() {
badgeCount = ntfBadgeCountGroupDefault.get() + 1
ntfBadgeCountGroupDefault.set(badgeCount)
@@ -272,7 +262,7 @@ class NotificationService: UNNotificationServiceExtension {
private func deliverBestAttemptNtf(urgent: Bool = false) {
logger.debug("NotificationService.deliverBestAttemptNtf")
// stop processing other messages
shouldProcessNtf = false
processReceivedNtf = nil
let suspend: Bool
if let t = threadId {
+33 -70
View File
@@ -36,6 +36,11 @@
5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFC727B2782E00FB6C6D /* BGManager.swift */; };
5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */; };
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C36027227F47AD5009F19D9 /* AppDelegate.swift */; };
5C371E742BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C371E6F2BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a */; };
5C371E752BACC5D600100AD3 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C371E702BACC5D600100AD3 /* libgmpxx.a */; };
5C371E762BACC5D600100AD3 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C371E712BACC5D600100AD3 /* libffi.a */; };
5C371E772BACC5D600100AD3 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C371E722BACC5D600100AD3 /* libgmp.a */; };
5C371E782BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C371E732BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a */; };
5C3A88CE27DF50170060F1C2 /* DetermineWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */; };
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3A88D027DF57800060F1C2 /* FramedItemView.swift */; };
5C3CCFCC2AE6BD3100C3F0C3 /* ConnectDesktopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */; };
@@ -110,11 +115,6 @@
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; };
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; };
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
5CC83D1A2BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC83D152BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a */; };
5CC83D1B2BCC504B00A0C558 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC83D162BCC504B00A0C558 /* libgmpxx.a */; };
5CC83D1C2BCC504B00A0C558 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC83D172BCC504B00A0C558 /* libgmp.a */; };
5CC83D1D2BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC83D182BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a */; };
5CC83D1E2BCC504B00A0C558 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC83D192BCC504B00A0C558 /* libffi.a */; };
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -187,7 +187,6 @@
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; };
8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */; };
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */; };
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; };
@@ -291,6 +290,11 @@
5C371E4E2BA9AAA200100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = "<group>"; };
5C371E4F2BA9AB6400100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = "hu.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5C371E502BA9AB6400100AD3 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = "<group>"; };
5C371E6F2BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a"; sourceTree = "<group>"; };
5C371E702BACC5D600100AD3 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C371E712BACC5D600100AD3 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C371E722BACC5D600100AD3 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C371E732BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a"; sourceTree = "<group>"; };
5C3A88CD27DF50170060F1C2 /* DetermineWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineWidth.swift; sourceTree = "<group>"; };
5C3A88D027DF57800060F1C2 /* FramedItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FramedItemView.swift; sourceTree = "<group>"; };
5C3CCFCB2AE6BD3100C3F0C3 /* ConnectDesktopView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectDesktopView.swift; sourceTree = "<group>"; };
@@ -402,11 +406,6 @@
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; };
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CC83D152BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a"; sourceTree = "<group>"; };
5CC83D162BCC504B00A0C558 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CC83D172BCC504B00A0C558 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CC83D182BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a"; sourceTree = "<group>"; };
5CC83D192BCC504B00A0C558 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = "<group>"; };
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; };
5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; };
@@ -481,7 +480,6 @@
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToDevice.swift; sourceTree = "<group>"; };
8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateFromDevice.swift; sourceTree = "<group>"; };
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
@@ -523,12 +521,12 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5CC83D1D2BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a in Frameworks */,
5CC83D1B2BCC504B00A0C558 /* libgmpxx.a in Frameworks */,
5CC83D1C2BCC504B00A0C558 /* libgmp.a in Frameworks */,
5CC83D1A2BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a in Frameworks */,
5CC83D1E2BCC504B00A0C558 /* libffi.a in Frameworks */,
5C371E752BACC5D600100AD3 /* libgmpxx.a in Frameworks */,
5C371E742BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C371E782BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a in Frameworks */,
5C371E762BACC5D600100AD3 /* libffi.a in Frameworks */,
5C371E772BACC5D600100AD3 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -592,11 +590,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
5CC83D192BCC504B00A0C558 /* libffi.a */,
5CC83D172BCC504B00A0C558 /* libgmp.a */,
5CC83D162BCC504B00A0C558 /* libgmpxx.a */,
5CC83D182BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv-ghc9.6.4.a */,
5CC83D152BCC504B00A0C558 /* libHSsimplex-chat-5.7.0.0-AhbVfRKDsEZ5w5ND1HSTLv.a */,
5C371E712BACC5D600100AD3 /* libffi.a */,
5C371E722BACC5D600100AD3 /* libgmp.a */,
5C371E702BACC5D600100AD3 /* libgmpxx.a */,
5C371E6F2BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv-ghc9.6.3.a */,
5C371E732BACC5D600100AD3 /* libHSsimplex-chat-5.6.0.4-E0iWSIg8fcR48og4na41Dv.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -625,7 +623,6 @@
5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */,
5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */,
5CBD2859295711D700EC2CF4 /* ImageUtils.swift */,
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */,
);
path = Model;
sourceTree = "<group>";
@@ -1179,7 +1176,6 @@
5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */,
5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */,
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */,
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */,
5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */,
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */,
5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */,
@@ -1445,7 +1441,6 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -1507,7 +1502,6 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -1536,16 +1530,12 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 207;
DEAD_CODE_STRIPPING = YES;
CURRENT_PROJECT_VERSION = 204;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
GCC_OPTIMIZATION_LEVEL = s;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "SimpleX--iOS--Info.plist";
INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
@@ -1564,13 +1554,11 @@
"$(inherited)",
"@executable_path/Frameworks",
);
LLVM_LTO = YES_THIN;
MARKETING_VERSION = 5.7;
MARKETING_VERSION = 5.6;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
@@ -1585,16 +1573,12 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 207;
DEAD_CODE_STRIPPING = YES;
CURRENT_PROJECT_VERSION = 204;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
ENABLE_PREVIEWS = NO;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "SimpleX--iOS--Info.plist";
INFOPLIST_KEY_NSCameraUsageDescription = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
@@ -1613,8 +1597,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.7;
MARKETING_VERSION = 5.6;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos;
@@ -1670,15 +1653,12 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 207;
CURRENT_PROJECT_VERSION = 204;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "SimpleX NSE/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "SimpleX NSE";
@@ -1689,15 +1669,13 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.7;
MARKETING_VERSION = 5.6;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Osize";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
@@ -1707,15 +1685,12 @@
isa = XCBuildConfiguration;
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 207;
CURRENT_PROJECT_VERSION = 204;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "SimpleX NSE/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "SimpleX NSE";
@@ -1726,15 +1701,13 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.7;
MARKETING_VERSION = 5.6;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Osize";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
VALIDATE_PRODUCT = YES;
@@ -1746,17 +1719,14 @@
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 207;
CURRENT_PROJECT_VERSION = 204;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved.";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -1774,8 +1744,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.7;
MARKETING_VERSION = 5.6;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1784,7 +1753,6 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_INCLUDE_PATHS = "";
SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
@@ -1797,17 +1765,14 @@
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 207;
CURRENT_PROJECT_VERSION = 204;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 SimpleX Chat. All rights reserved.";
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
@@ -1825,8 +1790,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Libraries/sim",
);
LLVM_LTO = YES;
MARKETING_VERSION = 5.7;
MARKETING_VERSION = 5.6;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos;
@@ -1835,7 +1799,6 @@
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_INCLUDE_PATHS = "";
SWIFT_OBJC_BRIDGING_HEADER = ./SimpleXChat/SimpleX.h;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
+13 -34
View File
@@ -77,7 +77,6 @@ public enum ChatCommand {
case apiGetChatItemTTL(userId: Int64)
case apiSetNetworkConfig(networkConfig: NetCfg)
case apiGetNetworkConfig
case apiSetNetworkInfo(networkInfo: UserNetworkInfo)
case reconnectAllServers
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings)
@@ -222,7 +221,6 @@ public enum ChatCommand {
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
case .apiGetNetworkConfig: return "/network"
case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))"
case .reconnectAllServers: return "/reconnect"
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))"
@@ -370,7 +368,6 @@ public enum ChatCommand {
case .apiGetChatItemTTL: return "apiGetChatItemTTL"
case .apiSetNetworkConfig: return "apiSetNetworkConfig"
case .apiGetNetworkConfig: return "apiGetNetworkConfig"
case .apiSetNetworkInfo: return "apiSetNetworkInfo"
case .reconnectAllServers: return "reconnectAllServers"
case .apiSetChatSettings: return "apiSetChatSettings"
case .apiSetMemberSettings: return "apiSetMemberSettings"
@@ -560,6 +557,11 @@ public enum ChatResponse: Decodable, Error {
case contactRequestRejected(user: UserRef)
case contactUpdated(user: UserRef, toContact: Contact)
case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember)
// TODO remove events below
case contactsSubscribed(server: String, contactRefs: [ContactRef])
case contactsDisconnected(server: String, contactRefs: [ContactRef])
case contactSubSummary(user: UserRef, contactSubscriptions: [ContactSubStatus])
// TODO remove events above
case networkStatus(networkStatus: NetworkStatus, connections: [String])
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
case groupSubscribed(user: UserRef, groupInfo: GroupRef)
@@ -722,6 +724,9 @@ public enum ChatResponse: Decodable, Error {
case .contactRequestRejected: return "contactRequestRejected"
case .contactUpdated: return "contactUpdated"
case .groupMemberUpdated: return "groupMemberUpdated"
case .contactsSubscribed: return "contactsSubscribed"
case .contactsDisconnected: return "contactsDisconnected"
case .contactSubSummary: return "contactSubSummary"
case .networkStatus: return "networkStatus"
case .networkStatuses: return "networkStatuses"
case .groupSubscribed: return "groupSubscribed"
@@ -880,6 +885,9 @@ public enum ChatResponse: Decodable, Error {
case .contactRequestRejected: return noDetails
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)")
case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions))
case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))"
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo))
@@ -1264,7 +1272,7 @@ public struct NetCfg: Codable, Equatable {
sessionMode: TransportSessionMode.user,
tcpConnectTimeout: 20_000_000,
tcpTimeout: 15_000_000,
tcpTimeoutPerKb: 10_000,
tcpTimeoutPerKb: 45_000,
tcpKeepAlive: KeepAliveOpts.defaults,
smpPingInterval: 1200_000_000,
smpPingCount: 3,
@@ -1276,7 +1284,7 @@ public struct NetCfg: Codable, Equatable {
sessionMode: TransportSessionMode.user,
tcpConnectTimeout: 30_000_000,
tcpTimeout: 20_000_000,
tcpTimeoutPerKb: 15_000,
tcpTimeoutPerKb: 60_000,
tcpKeepAlive: KeepAliveOpts.defaults,
smpPingInterval: 1200_000_000,
smpPingCount: 3,
@@ -1717,7 +1725,6 @@ public enum ChatErrorType: Decodable {
case fallbackToSMPProhibited(fileId: Int64)
case inlineFileProhibited(fileId: Int64)
case invalidQuote
case invalidForward
case invalidChatItemUpdate
case invalidChatItemDelete
case hasCurrentCall
@@ -2090,31 +2097,3 @@ public enum AppSettingsLockScreenCalls: String, Codable {
case show
case accept
}
public struct UserNetworkInfo: Codable, Equatable {
public let networkType: UserNetworkType
public let online: Bool
public init(networkType: UserNetworkType, online: Bool) {
self.networkType = networkType
self.online = online
}
}
public enum UserNetworkType: String, Codable {
case none
case cellular
case wifi
case ethernet
case other
public var text: LocalizedStringKey {
switch self {
case .none: "No network connection"
case .cellular: "Cellular"
case .wifi: "WiFi"
case .ethernet: "Wired ethernet"
case .other: "Other"
}
}
}
+32 -138
View File
@@ -543,7 +543,6 @@ public protocol Feature {
var iconFilled: String { get }
var iconScale: CGFloat { get }
var hasParam: Bool { get }
var hasRole: Bool { get }
var text: String { get }
}
@@ -570,8 +569,6 @@ public enum ChatFeature: String, Decodable, Feature {
}
}
public var hasRole: Bool { false }
public var text: String {
switch self {
case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature")
@@ -697,7 +694,6 @@ public enum GroupFeature: String, Decodable, Feature {
case reactions
case voice
case files
case simplexLinks
case history
public var id: Self { self }
@@ -709,19 +705,6 @@ public enum GroupFeature: String, Decodable, Feature {
}
}
public var hasRole: Bool {
switch self {
case .timedMessages: false
case .directMessages: true
case .fullDelete: false
case .reactions: false
case .voice: true
case .files: true
case .simplexLinks: true
case .history: false
}
}
public var text: String {
switch self {
case .timedMessages: return NSLocalizedString("Disappearing messages", comment: "chat feature")
@@ -730,7 +713,6 @@ public enum GroupFeature: String, Decodable, Feature {
case .reactions: return NSLocalizedString("Message reactions", comment: "chat feature")
case .voice: return NSLocalizedString("Voice messages", comment: "chat feature")
case .files: return NSLocalizedString("Files and media", comment: "chat feature")
case .simplexLinks: return NSLocalizedString("SimpleX links", comment: "chat feature")
case .history: return NSLocalizedString("Visible history", comment: "chat feature")
}
}
@@ -743,7 +725,6 @@ public enum GroupFeature: String, Decodable, Feature {
case .reactions: return "face.smiling"
case .voice: return "mic"
case .files: return "doc"
case .simplexLinks: return "link.circle"
case .history: return "clock"
}
}
@@ -756,7 +737,6 @@ public enum GroupFeature: String, Decodable, Feature {
case .reactions: return "face.smiling.fill"
case .voice: return "mic.fill"
case .files: return "doc.fill"
case .simplexLinks: return "link.circle.fill"
case .history: return "clock.fill"
}
}
@@ -801,11 +781,6 @@ public enum GroupFeature: String, Decodable, Feature {
case .on: return "Allow to send files and media."
case .off: return "Prohibit sending files and media."
}
case .simplexLinks:
switch enabled {
case .on: return "Allow to send SimpleX links."
case .off: return "Prohibit sending SimpleX links."
}
case .history:
switch enabled {
case .on: return "Send up to 100 last messages to new members."
@@ -844,11 +819,6 @@ public enum GroupFeature: String, Decodable, Feature {
case .on: return "Group members can send files and media."
case .off: return "Files and media are prohibited in this group."
}
case .simplexLinks:
switch enabled {
case .on: return "Group members can send SimpleX links."
case .off: return "SimpleX links are prohibited in this group."
}
case .history:
switch enabled {
case .on: return "Up to 100 last messages are sent to new members."
@@ -988,22 +958,20 @@ public enum FeatureAllowed: String, Codable, Identifiable {
public struct FullGroupPreferences: Decodable, Equatable {
public var timedMessages: TimedMessagesGroupPreference
public var directMessages: RoleGroupPreference
public var directMessages: GroupPreference
public var fullDelete: GroupPreference
public var reactions: GroupPreference
public var voice: RoleGroupPreference
public var files: RoleGroupPreference
public var simplexLinks: RoleGroupPreference
public var voice: GroupPreference
public var files: GroupPreference
public var history: GroupPreference
public init(
timedMessages: TimedMessagesGroupPreference,
directMessages: RoleGroupPreference,
directMessages: GroupPreference,
fullDelete: GroupPreference,
reactions: GroupPreference,
voice: RoleGroupPreference,
files: RoleGroupPreference,
simplexLinks: RoleGroupPreference,
voice: GroupPreference,
files: GroupPreference,
history: GroupPreference
) {
self.timedMessages = timedMessages
@@ -1012,40 +980,36 @@ public struct FullGroupPreferences: Decodable, Equatable {
self.reactions = reactions
self.voice = voice
self.files = files
self.simplexLinks = simplexLinks
self.history = history
}
public static let sampleData = FullGroupPreferences(
timedMessages: TimedMessagesGroupPreference(enable: .off),
directMessages: RoleGroupPreference(enable: .off, role: nil),
directMessages: GroupPreference(enable: .off),
fullDelete: GroupPreference(enable: .off),
reactions: GroupPreference(enable: .on),
voice: RoleGroupPreference(enable: .on, role: nil),
files: RoleGroupPreference(enable: .on, role: nil),
simplexLinks: RoleGroupPreference(enable: .on, role: nil),
voice: GroupPreference(enable: .on),
files: GroupPreference(enable: .on),
history: GroupPreference(enable: .on)
)
}
public struct GroupPreferences: Codable {
public var timedMessages: TimedMessagesGroupPreference?
public var directMessages: RoleGroupPreference?
public var directMessages: GroupPreference?
public var fullDelete: GroupPreference?
public var reactions: GroupPreference?
public var voice: RoleGroupPreference?
public var files: RoleGroupPreference?
public var simplexLinks: RoleGroupPreference?
public var voice: GroupPreference?
public var files: GroupPreference?
public var history: GroupPreference?
public init(
timedMessages: TimedMessagesGroupPreference? = nil,
directMessages: RoleGroupPreference? = nil,
directMessages: GroupPreference? = nil,
fullDelete: GroupPreference? = nil,
reactions: GroupPreference? = nil,
voice: RoleGroupPreference? = nil,
files: RoleGroupPreference? = nil,
simplexLinks: RoleGroupPreference? = nil,
voice: GroupPreference? = nil,
files: GroupPreference? = nil,
history: GroupPreference? = nil
) {
self.timedMessages = timedMessages
@@ -1054,18 +1018,16 @@ public struct GroupPreferences: Codable {
self.reactions = reactions
self.voice = voice
self.files = files
self.simplexLinks = simplexLinks
self.history = history
}
public static let sampleData = GroupPreferences(
timedMessages: TimedMessagesGroupPreference(enable: .off),
directMessages: RoleGroupPreference(enable: .off, role: nil),
directMessages: GroupPreference(enable: .off),
fullDelete: GroupPreference(enable: .off),
reactions: GroupPreference(enable: .on),
voice: RoleGroupPreference(enable: .on, role: nil),
files: RoleGroupPreference(enable: .on, role: nil),
simplexLinks: RoleGroupPreference(enable: .on, role: nil),
voice: GroupPreference(enable: .on),
files: GroupPreference(enable: .on),
history: GroupPreference(enable: .on)
)
}
@@ -1078,7 +1040,6 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group
reactions: fullPreferences.reactions,
voice: fullPreferences.voice,
files: fullPreferences.files,
simplexLinks: fullPreferences.simplexLinks,
history: fullPreferences.history
)
}
@@ -1090,37 +1051,11 @@ public struct GroupPreference: Codable, Equatable {
enable == .on
}
public func enabled(_ role: GroupMemberRole?, for m: GroupMember?) -> GroupFeatureEnabled {
switch enable {
case .off: .off
case .on:
if let role, let m {
m.memberRole >= role ? .on : .off
} else {
.on
}
}
}
public init(enable: GroupFeatureEnabled) {
self.enable = enable
}
}
public struct RoleGroupPreference: Codable, Equatable {
public var enable: GroupFeatureEnabled
public var role: GroupMemberRole?
public func on(for m: GroupMember) -> Bool {
enable == .on && m.memberRole >= (role ?? .observer)
}
public init(enable: GroupFeatureEnabled, role: GroupMemberRole?) {
self.enable = enable
self.role = role
}
}
public struct TimedMessagesGroupPreference: Codable, Equatable {
public var enable: GroupFeatureEnabled
public var ttl: Int?
@@ -1345,7 +1280,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
case .timedMessages: return prefs.timedMessages.on
case .fullDelete: return prefs.fullDelete.on
case .reactions: return prefs.reactions.on
case .voice: return prefs.voice.on(for: groupInfo.membership)
case .voice: return prefs.voice.on
case .calls: return false
}
case .local:
@@ -1388,7 +1323,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
return .other
}
case let .group(groupInfo):
if !groupInfo.fullGroupPreferences.voice.on(for: groupInfo.membership) {
if !groupInfo.fullGroupPreferences.voice.on {
return .groupOwnerCan
} else {
return .other
@@ -2040,7 +1975,7 @@ public struct GroupMemberIds: Decodable {
var groupId: Int64
}
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Codable {
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
case observer = "observer"
case author = "author"
case member = "member"
@@ -2437,10 +2372,10 @@ public struct ChatItem: Identifiable, Decodable {
}
}
public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> ChatItem {
public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, editable: Bool = true) -> ChatItem {
ChatItem(
chatDir: dir,
meta: CIMeta.getSample(id, ts, text, status, itemDeleted: itemDeleted, itemEdited: itemEdited, itemLive: itemLive, deletable: deletable, editable: editable),
meta: CIMeta.getSample(id, ts, text, status, itemDeleted: itemDeleted, itemEdited: itemEdited, itemLive: itemLive, editable: editable),
content: .sndMsgContent(msgContent: .text(text)),
quotedItem: quotedItem,
file: file
@@ -2531,7 +2466,6 @@ public struct ChatItem: Identifiable, Decodable {
itemDeleted: nil,
itemEdited: false,
itemLive: false,
deletable: false,
editable: false
),
content: .rcvDeleted(deleteMode: .cidmBroadcast),
@@ -2553,7 +2487,6 @@ public struct ChatItem: Identifiable, Decodable {
itemDeleted: nil,
itemEdited: false,
itemLive: true,
deletable: false,
editable: false
),
content: .sndMsgContent(msgContent: .text("")),
@@ -2613,12 +2546,10 @@ public struct CIMeta: Decodable {
public var itemStatus: CIStatus
public var createdAt: Date
public var updatedAt: Date
public var itemForwarded: CIForwardedFrom?
public var itemDeleted: CIDeleted?
public var itemEdited: Bool
public var itemTimed: CITimed?
public var itemLive: Bool?
public var deletable: Bool
public var editable: Bool
public var timestampText: Text { get { formatTimestampText(itemTs) } }
@@ -2635,7 +2566,7 @@ public struct CIMeta: Decodable {
itemStatus.statusIcon(metaColor)
}
public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> CIMeta {
public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, editable: Bool = true) -> CIMeta {
CIMeta(
itemId: id,
itemTs: ts,
@@ -2646,7 +2577,6 @@ public struct CIMeta: Decodable {
itemDeleted: itemDeleted,
itemEdited: itemEdited,
itemLive: itemLive,
deletable: deletable,
editable: editable
)
}
@@ -2662,7 +2592,6 @@ public struct CIMeta: Decodable {
itemDeleted: nil,
itemEdited: false,
itemLive: false,
deletable: false,
editable: false
)
}
@@ -2784,17 +2713,6 @@ public enum CIDeleted: Decodable {
}
}
public enum MsgDirection: Decodable {
case rcv
case snd
}
public enum CIForwardedFrom: Decodable {
case unknown
case contact(chatName: String, msgDir: MsgDirection, contactId: Int64?, chatItemId: Int64?)
case group(chatName: String, msgDir: MsgDirection, groupId: Int64?, chatItemId: Int64?)
}
public enum CIDeleteMode: String, Decodable {
case cidmBroadcast = "broadcast"
case cidmInternal = "internal"
@@ -2824,8 +2742,8 @@ public enum CIContent: Decodable, ItemContent {
case sndChatFeature(feature: ChatFeature, enabled: FeatureEnabled, param: Int?)
case rcvChatPreference(feature: ChatFeature, allowed: FeatureAllowed, param: Int?)
case sndChatPreference(feature: ChatFeature, allowed: FeatureAllowed, param: Int?)
case rcvGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?, memberRole_: GroupMemberRole?)
case sndGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?, memberRole_: GroupMemberRole?)
case rcvGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?)
case sndGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?)
case rcvChatFeatureRejected(feature: ChatFeature)
case rcvGroupFeatureRejected(groupFeature: GroupFeature)
case sndModerated
@@ -2859,8 +2777,8 @@ public enum CIContent: Decodable, ItemContent {
case let .sndChatFeature(feature, enabled, param): return CIContent.featureText(feature, enabled.text, param)
case let .rcvChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param)
case let .sndChatPreference(feature, allowed, param): return CIContent.preferenceText(feature, allowed, param)
case let .rcvGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role)
case let .sndGroupFeature(feature, preference, param, role): return CIContent.featureText(feature, preference.enable.text, param, role)
case let .rcvGroupFeature(feature, preference, param): return CIContent.featureText(feature, preference.enable.text, param)
case let .sndGroupFeature(feature, preference, param): return CIContent.featureText(feature, preference.enable.text, param)
case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text)
case let .rcvGroupFeatureRejected(groupFeature): return String.localizedStringWithFormat("%@: received, prohibited", groupFeature.text)
case .sndModerated: return NSLocalizedString("moderated", comment: "moderated chat item")
@@ -2885,25 +2803,10 @@ public enum CIContent: Decodable, ItemContent {
NSLocalizedString("This chat is protected by end-to-end encryption.", comment: "E2EE info chat item")
}
static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?, _ role: GroupMemberRole? = nil) -> String {
(
feature.hasParam
? "\(feature.text): \(timeText(param))"
: "\(feature.text): \(enabled)"
) +
(
feature.hasRole && role != nil
? " (\(roleText(role)))"
: ""
)
}
private static func roleText(_ role: GroupMemberRole?) -> String {
switch role {
case .owner: NSLocalizedString("owners", comment: "feature role")
case .admin: NSLocalizedString("admins", comment: "feature role")
default: NSLocalizedString("all members", comment: "feature role")
}
static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String {
feature.hasParam
? "\(feature.text): \(timeText(param))"
: "\(feature.text): \(enabled)"
}
public static func preferenceText(_ feature: Feature, _ allowed: FeatureAllowed, _ param: Int?) -> String {
@@ -3306,14 +3209,6 @@ public enum MsgContent: Equatable {
}
}
public var isImageOrVideo: Bool {
switch self {
case .image: true
case .video: true
default: false
}
}
var cmdString: String {
"json \(encodeJSON(self))"
}
@@ -3848,7 +3743,6 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable {
public struct ChatItemInfo: Decodable {
public var itemVersions: [ChatItemVersion]
public var memberDeliveryStatuses: [MemberDeliveryStatus]?
public var forwardedFromChatItem: AChatItem?
}
public struct ChatItemVersion: Decodable {
+3 -1
View File
@@ -25,12 +25,14 @@ void haskell_init(void) {
}
void haskell_init_nse(void) {
int argc = 7;
int argc = 9;
char *argv[] = {
"simplex",
"+RTS", // requires `hs_init_with_rtsopts`
"-A1m", // chunk size for new allocations
"-H1m", // initial heap size
"-M12m", // maximum heap size (25 total - 1 grace - 12 for swift), make GC work harder when approaching the limit
"-Mgrace=1m", // (default, just to make explicit) extra memory to handle "memory exhausted" exception and fail cleanly
"-F0.5", // heap growth triggering GC
"-Fd1", // memory return
"-c", // compacting garbage collector
Binary file not shown.
Binary file not shown.
+5 -3
View File
@@ -48,7 +48,12 @@ android {
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs += "-opt-in=kotlinx.coroutines.DelicateCoroutinesApi"
freeCompilerArgs += "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi"
freeCompilerArgs += "-opt-in=androidx.compose.ui.text.ExperimentalTextApi"
@@ -138,9 +143,6 @@ dependencies {
implementation("com.jakewharton:process-phoenix:2.2.0")
//Camera Permission
implementation("com.google.accompanist:accompanist-permissions:0.23.0")
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
//implementation("androidx.compose.ui:ui-util:$compose_version")
@@ -16,7 +16,8 @@ import androidx.work.*
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.app.views.call.CallActivity
import chat.simplex.common.helpers.*
import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.updatingChatsMutex
@@ -290,10 +291,6 @@ class SimplexApp: Application(), LifecycleEventObserver {
activeCallDestroyWebView()
}
override fun androidRestartNetworkObserver() {
NetworkObserver.shared.restartNetworkObserver()
}
@SuppressLint("SourceLockedOrientationActivity")
@Composable
override fun androidLockPortraitOrientation() {
@@ -1,9 +1,7 @@
package chat.simplex.app.views.call
import android.Manifest
import android.app.*
import android.content.*
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Rect
import android.os.*
@@ -30,7 +28,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import chat.simplex.app.*
import chat.simplex.app.R
@@ -39,12 +36,10 @@ import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
@@ -114,20 +109,9 @@ class CallActivity: ComponentActivity(), ServiceConnection {
m.callCommand.add(WCallCommand.Layout(layoutType))
}
private fun hasGrantedPermissions(): Boolean {
val grantedAudio = ContextCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
val grantedCamera = !callSupportsVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
return grantedAudio && grantedCamera
}
override fun onBackPressed() {
if (isOnLockScreenNow()) {
super.onBackPressed()
} else if (!hasGrantedPermissions() && !callSupportsVideo()) {
val call = m.activeCall.value
if (call != null) {
withBGApi { chatModel.callManager.endCall(call) }
}
} else {
m.activeCallViewIsCollapsed.value = true
}
@@ -239,21 +223,8 @@ fun CallActivityView() {
}
Box(Modifier.background(Color.Black)) {
if (call != null) {
val permissionsState = rememberMultiplePermissionsState(
permissions = if (callSupportsVideo()) {
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
} else {
listOf(Manifest.permission.RECORD_AUDIO)
}
)
if (permissionsState.allPermissionsGranted) {
ActiveCallView()
} else {
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
withBGApi { chatModel.callManager.endCall(call) }
}
}
val view = LocalView.current
ActiveCallView()
if (callSupportsVideo()) {
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
@@ -281,9 +252,6 @@ fun CallActivityView() {
}
}
}
if (!m.activeCallViewIsCollapsed.value) {
AlertManager.shared.showInView()
}
}
LaunchedEffect(call == null) {
if (call != null) {
-24
View File
@@ -83,30 +83,6 @@ plugins {
id("org.jetbrains.kotlin.plugin.serialization") apply false
}
// https://raymondctc.medium.com/configuring-your-sourcecompatibility-targetcompatibility-and-kotlinoptions-jvmtarget-all-at-once-66bf2198145f
val jvmVersion: Provider<String> = providers.gradleProperty("kotlin.jvm.target")
configure(subprojects) {
// Apply compileOptions to subprojects
plugins.withType<com.android.build.gradle.BasePlugin>().configureEach {
extensions.findByType<com.android.build.gradle.BaseExtension>()?.apply {
jvmVersion.map { JavaVersion.toVersion(it) }.orNull?.let {
compileOptions {
sourceCompatibility = it
targetCompatibility = it
}
}
}
}
// Apply kotlinOptions.jvmTarget to subprojects
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
if (jvmVersion.isPresent) jvmTarget = jvmVersion.get()
}
}
}
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
}
+7 -4
View File
@@ -12,7 +12,9 @@ version = extra["android.version_name"] as String
kotlin {
androidTarget()
jvm("desktop")
jvm("desktop") {
jvmToolchain(11)
}
applyDefaultHierarchyTemplate()
sourceSets {
all {
@@ -88,9 +90,6 @@ kotlin {
implementation("androidx.camera:camera-camera2:${cameraXVersion}")
implementation("androidx.camera:camera-lifecycle:${cameraXVersion}")
implementation("androidx.camera:camera-view:${cameraXVersion}")
// Calls lifecycle listener
implementation("androidx.lifecycle:lifecycle-process:2.4.1")
}
}
val desktopMain by getting {
@@ -117,6 +116,10 @@ android {
}
testOptions.targetSdk = 33
lint.targetSdk = 33
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
val isAndroid = gradle.startParameter.taskNames.find {
val lower = it.lowercase()
lower.contains("release") || lower.startsWith("assemble") || lower.startsWith("install")
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="chat.simplex.common">
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
@@ -1,104 +0,0 @@
package chat.simplex.common.helpers
import android.net.*
import android.util.Log
import androidx.core.content.getSystemService
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.UserNetworkInfo
import chat.simplex.common.model.UserNetworkType
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.withBGApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
class NetworkObserver {
private var prevInfo: UserNetworkInfo? = null
// When having both mobile and Wi-Fi networks enabled with Wi-Fi being active, then disabling Wi-Fi, network reports its offline (which is true)
// but since it will be online after switching to mobile, there is no need to inform backend about such temporary change.
// But if it will not be online after some seconds, report it and apply required measures
private var noNetworkJob = Job() as Job
private val networkCallback = object: ConnectivityManager.NetworkCallback() {
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) = networkCapabilitiesChanged(networkCapabilities)
override fun onLost(network: Network) = networkLost()
}
private val connectivityManager: ConnectivityManager? = androidAppContext.getSystemService()
fun restartNetworkObserver() {
if (connectivityManager == null) {
Log.e(TAG, "Connectivity manager is unavailable, network observer is disabled")
val info = UserNetworkInfo(
networkType = UserNetworkType.OTHER,
online = true,
)
prevInfo = info
setNetworkInfo(info)
return
}
try {
connectivityManager.unregisterNetworkCallback(networkCallback)
} catch (e: Exception) {
// do nothing
}
val initialCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
if (initialCapabilities != null) {
networkCapabilitiesChanged(initialCapabilities)
} else {
networkLost()
}
try {
connectivityManager.registerDefaultNetworkCallback(networkCallback)
} catch (e: Exception) {
Log.e(TAG, "Error registering network callback: ${e.stackTraceToString()}")
}
}
private fun networkCapabilitiesChanged(capabilities: NetworkCapabilities) {
connectivityManager ?: return
val info = UserNetworkInfo(
networkType = networkTypeFromCapabilities(capabilities),
online = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED),
)
if (prevInfo != info) {
prevInfo = info
setNetworkInfo(info)
}
}
private fun networkLost() {
Log.d(TAG, "Network changed: lost")
val none = UserNetworkInfo(networkType = UserNetworkType.NONE, false)
prevInfo = none
setNetworkInfo(none)
}
private fun setNetworkInfo(info: UserNetworkInfo) {
Log.d(TAG, "Network changed: $info")
noNetworkJob.cancel()
if (info.online) {
withBGApi {
if (controller.hasChatCtrl() && controller.apiSetNetworkInfo(info)) {
chatModel.networkInfo.value = info
}
}
} else {
noNetworkJob = withBGApi {
delay(3000)
if (controller.hasChatCtrl() && controller.apiSetNetworkInfo(info)) {
chatModel.networkInfo.value = info
}
}
}
}
private fun networkTypeFromCapabilities(capabilities: NetworkCapabilities): UserNetworkType = when {
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> UserNetworkType.ETHERNET
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> UserNetworkType.WIFI
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> UserNetworkType.CELLULAR
else -> UserNetworkType.OTHER
}
companion object {
val shared = NetworkObserver()
}
}
@@ -1,30 +0,0 @@
package chat.simplex.common.helpers
import android.content.*
import android.net.Uri
import android.provider.Settings
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
fun Context.openAppSettingsInSystem() {
Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.parse("package:${androidAppContext.packageName}")
try {
startActivity(this)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, e.stackTraceToString())
}
}
}
fun Context.showAllowPermissionInSettingsAlert(action: () -> Unit = ::openAppSettingsInSystem) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.permissions_grant_in_settings),
text = generalGetString(MR.strings.permissions_find_in_settings_and_grant),
confirmText = generalGetString(MR.strings.permissions_open_settings),
onConfirm = action,
)
}
@@ -2,16 +2,17 @@ package chat.simplex.common.helpers
import android.media.*
import android.net.Uri
import android.os.*
import android.os.VibrationEffect
import android.os.Vibrator
import androidx.core.content.ContextCompat
import chat.simplex.common.R
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.withApi
import chat.simplex.common.platform.SoundPlayerInterface
import chat.simplex.common.platform.androidAppContext
import kotlinx.coroutines.*
object SoundPlayer: SoundPlayerInterface {
private var player: MediaPlayer? = null
private var playing = false
var playing = false
override fun start(scope: CoroutineScope, sound: Boolean) {
player?.reset()
@@ -31,7 +32,7 @@ object SoundPlayer: SoundPlayerInterface {
scope.launch {
while (playing) {
if (sound) player?.start()
vibrator?.vibrateApiVersionAware(effect)
vibrator?.vibrate(effect)
delay(3500)
}
}
@@ -42,82 +43,3 @@ object SoundPlayer: SoundPlayerInterface {
player?.stop()
}
}
object CallSoundsPlayer: CallSoundsPlayerInterface {
private var player: MediaPlayer? = null
private var playingJob: Job? = null
private fun start(soundPath: String, delay: Long, scope: CoroutineScope) {
playingJob?.cancel()
player?.reset()
player = MediaPlayer().apply {
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
.build()
)
setDataSource(androidAppContext, Uri.parse(soundPath))
prepare()
}
if (delay < 1000) {
player?.isLooping = true
player?.start()
return
}
playingJob = scope.launch {
while (isActive) {
player?.start()
delay(delay)
}
}
}
override fun startConnectingCallSound(scope: CoroutineScope) {
// Taken from https://github.com/TelegramOrg/Telegram-Android
// https://github.com/TelegramOrg/Telegram-Android/blob/master/LICENSE
start("android.resource://" + androidAppContext.packageName + "/" + R.raw.connecting_call, 0, scope)
}
override fun startInCallSound(scope: CoroutineScope) {
start("android.resource://" + androidAppContext.packageName + "/" + R.raw.in_call, 2000, scope)
}
override fun vibrate(times: Int) {
val vibrator = ContextCompat.getSystemService(androidAppContext, Vibrator::class.java)
val effect = VibrationEffect.createOneShot(20, VibrationEffect.DEFAULT_AMPLITUDE)
vibrator?.vibrateApiVersionAware(effect)
repeat(times - 1) {
withApi {
delay(50)
vibrator?.vibrateApiVersionAware(effect)
}
}
}
override fun stop() {
playingJob?.cancel()
player?.stop()
}
}
private fun Vibrator.vibrateApiVersionAware(effect: VibrationEffect) {
if (Build.VERSION.SDK_INT >= 33) {
vibrate(
effect,
VibrationAttributes.Builder()
.setUsage(VibrationAttributes.USAGE_ALARM)
.build()
)
} else if (Build.VERSION.SDK_INT >= 29) {
vibrate(
effect,
AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ALARM)
.build()
)
} else {
vibrate(effect)
}
}
@@ -296,7 +296,6 @@ actual object AudioPlayer: AudioPlayerInterface {
}
actual typealias SoundPlayer = chat.simplex.common.helpers.SoundPlayer
actual typealias CallSoundsPlayer = chat.simplex.common.helpers.CallSoundsPlayer
class CryptoMediaSource(val data: ByteArray) : MediaDataSource() {
override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int {
@@ -1,7 +1,5 @@
package chat.simplex.common.views.call
import SectionSpacer
import SectionView
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
@@ -26,18 +24,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.*
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.webkit.WebViewAssetLoader
import androidx.webkit.WebViewClientCompat
import chat.simplex.common.helpers.showAllowPermissionInSettingsAlert
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.model.ChatModel
@@ -45,12 +42,13 @@ import chat.simplex.common.model.Contact
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import com.google.accompanist.permissions.*
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.datetime.Clock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
// Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak
@@ -69,8 +67,6 @@ fun activeCallDestroyWebView() = withApi {
@SuppressLint("SourceLockedOrientationActivity")
@Composable
actual fun ActiveCallView() {
val call = remember { chatModel.activeCall }.value
val scope = rememberCoroutineScope()
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
val proximityLock = remember {
val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
@@ -80,13 +76,6 @@ actual fun ActiveCallView() {
null
}
}
val wasConnected = rememberSaveable { mutableStateOf(false) }
LaunchedEffect(call) {
if (call?.callState == CallState.Connected && !wasConnected.value) {
CallSoundsPlayer.vibrate(2)
wasConnected.value = true
}
}
DisposableEffect(Unit) {
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
var btDeviceCount = 0
@@ -116,10 +105,6 @@ actual fun ActiveCallView() {
}
am.registerAudioDeviceCallback(audioCallback, null)
onDispose {
CallSoundsPlayer.stop()
if (wasConnected.value) {
CallSoundsPlayer.vibrate()
}
dropAudioManagerOverrides()
am.unregisterAudioDeviceCallback(audioCallback)
if (proximityLock?.isHeld == true) {
@@ -135,6 +120,8 @@ actual fun ActiveCallView() {
if (proximityLock?.isHeld == false) proximityLock.acquire()
}
}
val scope = rememberCoroutineScope()
val call = chatModel.activeCall.value
Box(Modifier.fillMaxSize()) {
WebRTCView(chatModel.callCommand) { apiMsg ->
Log.d(TAG, "received from WebRTCView: $apiMsg")
@@ -147,9 +134,6 @@ actual fun ActiveCallView() {
val callType = CallType(call.localMedia, r.capabilities)
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) }
setCallSound(call.soundSpeaker, audioViaBluetooth)
CallSoundsPlayer.startConnectingCallSound(scope)
activeCallWaitDeliveryReceipt(scope)
}
is WCallResponse.Offer -> withBGApi {
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
@@ -158,7 +142,6 @@ actual fun ActiveCallView() {
is WCallResponse.Answer -> withBGApi {
chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates)
updateActiveCall(call) { it.copy(callState = CallState.Negotiated) }
CallSoundsPlayer.stop()
}
is WCallResponse.Ice -> withBGApi {
chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates)
@@ -226,6 +209,7 @@ actual fun ActiveCallView() {
ActiveCallOverlay(call, chatModel, audioViaBluetooth)
}
}
val context = LocalContext.current
DisposableEffect(Unit) {
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
@@ -526,138 +510,34 @@ private fun DisabledBackgroundCallsButton() {
// }
//}
@Composable
fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Unit) {
val audioPermission = rememberPermissionState(Manifest.permission.RECORD_AUDIO)
val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA)
val permissionsState = rememberMultiplePermissionsState(
permissions = if (hasVideo) {
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
} else {
listOf(Manifest.permission.RECORD_AUDIO)
}
)
val context = LocalContext.current
val buttonEnabled = remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
if (!pipActive) {
permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert)
}
}
if (pipActive) {
Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly) {
if (audioPermission.status is PermissionStatus.Denied) {
Icon(
painterResource(MR.images.ic_call_500),
stringResource(MR.strings.permissions_record_audio),
Modifier.size(24.dp),
tint = Color(0xFFFFFFD8)
)
}
if (hasVideo && cameraPermission.status is PermissionStatus.Denied) {
Icon(
painterResource(MR.images.ic_videocam),
stringResource(MR.strings.permissions_camera),
Modifier.size(24.dp),
tint = Color(0xFFFFFFD8)
)
}
}
} else {
ColumnWithScrollBar(Modifier.fillMaxSize()) {
Spacer(Modifier.height(AppBarHeight))
AppBarTitle(stringResource(MR.strings.permissions_required))
Spacer(Modifier.weight(1f))
val onClick = {
if (permissionsState.shouldShowRationale) {
context.showAllowPermissionInSettingsAlert()
} else {
permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert)
}
}
Text(stringResource(MR.strings.permissions_grant), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), textAlign = TextAlign.Center, color = Color(0xFFFFFFD8))
SectionSpacer()
SectionView {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
val text = if (hasVideo && audioPermission.status is PermissionStatus.Denied && cameraPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_camera_and_record_audio)
} else if (audioPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_record_audio)
} else if (hasVideo && cameraPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_camera)
} else ""
GrantPermissionButton(text, buttonEnabled.value, onClick)
}
}
Spacer(Modifier.weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = if (hasVideo) 0.dp else DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.Center) {
SimpleButtonFrame(cancel, Modifier.height(64.dp)) {
Text(stringResource(MR.strings.call_service_notification_end_call), fontSize = 20.sp, color = Color(0xFFFFFFD8))
}
}
}
}
}
@Composable
private fun GrantPermissionButton(text: String, enabled: Boolean, onClick: () -> Unit) {
Row(
Modifier
.clickable(enabled = enabled, onClick = onClick)
.heightIn(min = 30.dp)
.background(WarningOrange.copy(0.3f), RoundedCornerShape(50)),
verticalAlignment = Alignment.CenterVertically
) {
Text(text, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), fontSize = 20.sp, color = WarningOrange)
}
}
/**
* The idea of this function is to ask system to show permission dialog and to see if it's really doing it.
* Otherwise, show alert with a button that opens settings for manual permission granting
* */
private fun MultiplePermissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled: MutableState<Boolean>, fallback: () -> Unit) {
buttonEnabled.value = false
val lifecycleOwner = ProcessLifecycleOwner.get().lifecycle
var useFallback = true
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_PAUSE) {
useFallback = false
buttonEnabled.value = true
}
}
lifecycleOwner.addObserver(observer)
withBGApi {
delay(2000)
if (useFallback && chatModel.activeCall.value != null) {
fallback()
}
buttonEnabled.value = true
}.invokeOnCompletion {
// Main thread only
withApi {
lifecycleOwner.removeObserver(observer)
}
}
launchMultiplePermissionRequest()
}
@Composable
fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) {
val webView = remember { mutableStateOf<WebView?>(null) }
val permissionsState = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.MODIFY_AUDIO_SETTINGS,
Manifest.permission.INTERNET
)
)
fun processCommand(wv: WebView, cmd: WCallCommand) {
val apiCall = WVAPICall(command = cmd)
wv.evaluateJavascript("processCommand(${json.encodeToString(apiCall)})", null)
}
DisposableEffect(Unit) {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME || event == Lifecycle.Event.ON_START) {
permissionsState.launchMultiplePermissionRequest()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
// val wv = webView.value
// if (wv != null) processCommand(wv, WCallCommand.End)
// webView.value?.destroy()
lifecycleOwner.lifecycle.removeObserver(observer)
// val wv = webView.value
// if (wv != null) processCommand(wv, WCallCommand.End)
// webView.value?.destroy()
webView.value = null
}
}
@@ -680,42 +560,44 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
.addPathHandler("/assets/www/", WebViewAssetLoader.AssetsPathHandler(LocalContext.current))
.build()
Box(Modifier.fillMaxSize()) {
AndroidView(
factory = { AndroidViewContext ->
(staticWebView ?: WebView(androidAppContext)).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
this.webChromeClient = object: WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
if (request.origin.toString().startsWith("file:/")) {
request.grant(request.resources)
} else {
Log.d(TAG, "Permission request from webview denied.")
request.deny()
if (permissionsState.allPermissionsGranted) {
Box(Modifier.fillMaxSize()) {
AndroidView(
factory = { AndroidViewContext ->
(staticWebView ?: WebView(androidAppContext)).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
this.webChromeClient = object: WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
if (request.origin.toString().startsWith("file:/")) {
request.grant(request.resources)
} else {
Log.d(TAG, "Permission request from webview denied.")
request.deny()
}
}
}
}
this.webViewClient = LocalContentWebViewClient(webView, assetLoader)
this.clearHistory()
this.clearCache(true)
this.addJavascriptInterface(WebRTCInterface(onResponse), "WebRTCInterface")
val webViewSettings = this.settings
webViewSettings.allowFileAccess = true
webViewSettings.allowContentAccess = true
webViewSettings.javaScriptEnabled = true
webViewSettings.mediaPlaybackRequiresUserGesture = false
webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE
if (staticWebView == null) {
this.loadUrl("file:android_asset/www/android/call.html")
} else {
webView.value = this
this.webViewClient = LocalContentWebViewClient(webView, assetLoader)
this.clearHistory()
this.clearCache(true)
this.addJavascriptInterface(WebRTCInterface(onResponse), "WebRTCInterface")
val webViewSettings = this.settings
webViewSettings.allowFileAccess = true
webViewSettings.allowContentAccess = true
webViewSettings.javaScriptEnabled = true
webViewSettings.mediaPlaybackRequiresUserGesture = false
webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE
if (staticWebView == null) {
this.loadUrl("file:android_asset/www/android/call.html")
} else {
webView.value = this
}
}
}
}
) { /* WebView */ }
) { /* WebView */ }
}
}
}
@@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import chat.simplex.common.helpers.toUri
import chat.simplex.common.model.CIFile
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.ModalManager
@@ -14,6 +15,7 @@ import coil.compose.rememberAsyncImagePainter
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest
import java.net.URI
@Composable
actual fun SimpleAndAnimatedImageView(
@@ -41,7 +43,6 @@ actual fun SimpleAndAnimatedImageView(
}
private val imageLoader = ImageLoader.Builder(androidAppContext)
.networkObserverEnabled(false)
.components {
if (SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
@@ -3,7 +3,7 @@ package chat.simplex.common.views.chat.item
import android.os.Build
import android.view.View
import androidx.compose.foundation.Image
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.BitmapPainter
@@ -11,8 +11,8 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible
import chat.simplex.common.helpers.toUri
import chat.simplex.common.platform.VideoPlayer
import chat.simplex.common.platform.androidAppContext
import chat.simplex.res.MR
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
@@ -23,11 +23,21 @@ import coil.size.Size
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView
import dev.icerock.moko.resources.compose.stringResource
import java.net.URI
@Composable
actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) {
// I'm using a new private instance of imageLoader here because if I use one instance in multiple places
// I'm making a new instance of imageLoader here because if I use one instance in multiple places
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want
val imageLoader = ImageLoader.Builder(LocalContext.current)
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.build()
Image(
rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = data).size(Size.ORIGINAL).build(),
@@ -63,14 +73,3 @@ actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: (
modifier
)
}
private val imageLoader = ImageLoader.Builder(androidAppContext)
.networkObserverEnabled(false)
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.build()
@@ -118,7 +118,6 @@ object ChatModel {
val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) }
val clipboardHasText = mutableStateOf(false)
val networkInfo = mutableStateOf(UserNetworkInfo(networkType = UserNetworkType.OTHER, online = true))
val updatingChatsMutex: Mutex = Mutex()
val changingActiveUserMutex: Mutex = Mutex()
@@ -805,24 +804,6 @@ data class Chat(
val id: String get() = chatInfo.id
fun groupFeatureEnabled(feature: GroupFeature): Boolean =
if (chatInfo is ChatInfo.Group) {
val groupInfo = chatInfo.groupInfo
val p = groupInfo.fullGroupPreferences
when (feature) {
GroupFeature.TimedMessages -> p.timedMessages.on
GroupFeature.DirectMessages -> p.directMessages.on(groupInfo.membership)
GroupFeature.FullDelete -> p.fullDelete.on
GroupFeature.Reactions -> p.reactions.on
GroupFeature.Voice -> p.voice.on(groupInfo.membership)
GroupFeature.Files -> p.files.on(groupInfo.membership)
GroupFeature.SimplexLinks -> p.simplexLinks.on(groupInfo.membership)
GroupFeature.History -> p.history.on
}
} else {
true
}
@Serializable
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false)
@@ -1258,7 +1239,7 @@ data class GroupInfo (
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on
ChatFeature.Reactions -> fullGroupPreferences.reactions.on
ChatFeature.Voice -> fullGroupPreferences.voice.on(membership)
ChatFeature.Voice -> fullGroupPreferences.voice.on
ChatFeature.Calls -> false
}
override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null }
@@ -1902,16 +1883,14 @@ data class ChatItem (
status: CIStatus = CIStatus.SndNew(),
quotedItem: CIQuote? = null,
file: CIFile? = null,
itemForwarded: CIForwardedFrom? = null,
itemDeleted: CIDeleted? = null,
itemEdited: Boolean = false,
itemTimed: CITimed? = null,
deletable: Boolean = true,
editable: Boolean = true
) =
ChatItem(
chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, editable),
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, itemTimed, editable),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
quotedItem = quotedItem,
reactions = listOf(),
@@ -1995,12 +1974,10 @@ data class ChatItem (
itemStatus = CIStatus.RcvRead(),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
itemForwarded = null,
itemDeleted = null,
itemEdited = false,
itemTimed = null,
itemLive = false,
deletable = false,
editable = false
),
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
@@ -2018,12 +1995,10 @@ data class ChatItem (
itemStatus = CIStatus.RcvRead(),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
itemForwarded = null,
itemDeleted = null,
itemEdited = false,
itemTimed = null,
itemLive = true,
deletable = false,
editable = false
),
content = CIContent.SndMsgContent(MsgContent.MCText("")),
@@ -2120,12 +2095,10 @@ data class CIMeta (
val itemStatus: CIStatus,
val createdAt: Instant,
val updatedAt: Instant,
val itemForwarded: CIForwardedFrom?,
val itemDeleted: CIDeleted?,
val itemEdited: Boolean,
val itemTimed: CITimed?,
val itemLive: Boolean?,
val deletable: Boolean,
val editable: Boolean
) {
val timestampText: String get() = getTimestampText(itemTs)
@@ -2145,8 +2118,7 @@ data class CIMeta (
companion object {
fun getSample(
id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(),
itemForwarded: CIForwardedFrom? = null, itemDeleted: CIDeleted? = null, itemEdited: Boolean = false,
itemTimed: CITimed? = null, itemLive: Boolean = false, deletable: Boolean = true, editable: Boolean = true
itemDeleted: CIDeleted? = null, itemEdited: Boolean = false, itemTimed: CITimed? = null, itemLive: Boolean = false, editable: Boolean = true
): CIMeta =
CIMeta(
itemId = id,
@@ -2155,12 +2127,10 @@ data class CIMeta (
itemStatus = status,
createdAt = ts,
updatedAt = ts,
itemForwarded = itemForwarded,
itemDeleted = itemDeleted,
itemEdited = itemEdited,
itemTimed = itemTimed,
itemLive = itemLive,
deletable = deletable,
editable = editable
)
@@ -2173,12 +2143,10 @@ data class CIMeta (
itemStatus = CIStatus.SndNew(),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
itemForwarded = null,
itemDeleted = null,
itemEdited = false,
itemTimed = null,
itemLive = false,
deletable = false,
editable = false
)
}
@@ -2289,19 +2257,6 @@ sealed class CIDeleted {
@Serializable @SerialName("moderated") class Moderated(val deletedTs: Instant?, val byGroupMember: GroupMember): CIDeleted()
}
@Serializable
enum class MsgDirection {
@SerialName("rcv") Rcv,
@SerialName("snd") Snd;
}
@Serializable
sealed class CIForwardedFrom {
@Serializable @SerialName("unknown") object Unknown: CIForwardedFrom()
@Serializable @SerialName("contact") class Contact(val chatName: String, val msgDir: MsgDirection, val contactId: Long? = null, val chatItemId: Long? = null): CIForwardedFrom()
@Serializable @SerialName("group") class Group(val chatName: String, val msgDir: MsgDirection, val groupId: Long? = null, val chatItemId: Long? = null): CIForwardedFrom()
}
@Serializable
enum class CIDeleteMode(val deleteMode: String) {
@SerialName("internal") cidmInternal("internal"),
@@ -2337,8 +2292,8 @@ sealed class CIContent: ItemContent {
@Serializable @SerialName("sndChatFeature") class SndChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvChatPreference") class RcvChatPreference(val feature: ChatFeature, val allowed: FeatureAllowed, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndChatPreference") class SndChatPreference(val feature: ChatFeature, val allowed: FeatureAllowed, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null, val memberRole_: GroupMemberRole?): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null, val memberRole_: GroupMemberRole?): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: ChatFeature): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndModerated") object SndModerated: CIContent() { override val msgContent: MsgContent? get() = null }
@@ -2370,8 +2325,8 @@ sealed class CIContent: ItemContent {
is SndChatFeature -> featureText(feature, enabled.text, param)
is RcvChatPreference -> preferenceText(feature, allowed, param)
is SndChatPreference -> preferenceText(feature, allowed, param)
is RcvGroupFeature -> featureText(groupFeature, preference.enable.text, param, memberRole_)
is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param, memberRole_)
is RcvGroupFeature -> featureText(groupFeature, preference.enable.text, param)
is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param)
is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(MR.strings.feature_received_prohibited)}"
is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(MR.strings.feature_received_prohibited)}"
is SndModerated -> generalGetString(MR.strings.moderated_description)
@@ -2408,23 +2363,11 @@ sealed class CIContent: ItemContent {
private val e2eeInfoNoPQStr: String = generalGetString(MR.strings.e2ee_info_no_pq_short)
fun featureText(feature: Feature, enabled: String, param: Int?, role: GroupMemberRole? = null): String =
(if (feature.hasParam) {
fun featureText(feature: Feature, enabled: String, param: Int?): String =
if (feature.hasParam) {
"${feature.text}: ${timeText(param)}"
} else {
"${feature.text}: $enabled"
}) + (
if (feature.hasRole && role != null)
" (${roleText(role)})"
else
""
)
private fun roleText(role: GroupMemberRole?): String =
when (role) {
GroupMemberRole.Owner -> generalGetString(MR.strings.feature_roles_owners)
GroupMemberRole.Admin -> generalGetString(MR.strings.feature_roles_admins)
else -> generalGetString(MR.strings.feature_roles_all_members)
}
fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when {
@@ -3308,8 +3251,7 @@ sealed class ChatItemTTL: Comparable<ChatItemTTL?> {
@Serializable
class ChatItemInfo(
val itemVersions: List<ChatItemVersion>,
val memberDeliveryStatuses: List<MemberDeliveryStatus>?,
val forwardedFromChatItem: AChatItem?
val memberDeliveryStatuses: List<MemberDeliveryStatus>?
)
@Serializable
@@ -20,7 +20,6 @@ import com.charleskorn.kaml.YamlConfiguration
import chat.simplex.res.MR
import com.russhwolf.settings.Settings
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
@@ -351,15 +350,11 @@ object ChatController {
var ctrl: ChatCtrl? = -1
val appPrefs: AppPreferences by lazy { AppPreferences() }
val messagesChannel: Channel<APIResponse> = Channel()
val chatModel = ChatModel
private var receiverStarted = false
var lastMsgReceivedTimestamp: Long = System.currentTimeMillis()
private set
fun hasChatCtrl() = ctrl != -1L && ctrl != null
private suspend fun currentUserId(funcName: String): Long = changingActiveUserMutex.withLock {
val userId = chatModel.currentUser.value?.userId
if (userId == null) {
@@ -486,7 +481,6 @@ object ChatController {
if (msg != null) {
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
processReceivedMsg(msg)
messagesChannel.trySend(msg)
}
if (finishedWithoutTimeout == null) {
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
@@ -881,9 +875,6 @@ object ChatController {
}
}
suspend fun apiSetNetworkInfo(networkInfo: UserNetworkInfo): Boolean =
sendCommandOkResp(null, CC.APISetNetworkInfo(networkInfo))
suspend fun apiSetMemberSettings(rh: Long?, groupId: Long, groupMemberId: Long, memberSettings: GroupMemberSettings): Boolean =
sendCommandOkResp(rh, CC.ApiSetMemberSettings(groupId, groupMemberId, memberSettings))
@@ -1762,8 +1753,6 @@ object ChatController {
chatModel.removeChat(rhId, r.mergedContact.id)
}
}
// ContactsSubscribed, ContactsDisconnected and ContactSubSummary are only used in CLI,
// They have to be used here for remote desktop to process these status updates.
is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected())
is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected())
is CR.ContactSubSummary -> {
@@ -2128,7 +2117,7 @@ object ChatController {
if (active(r.user)) {
chatModel.updateContact(rhId, r.contact)
}
is CR.ChatRespError -> when {
is CR.ChatCmdError -> when {
r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> {
chatModel.processedCriticalError.newError(r.chatError.agentError, r.chatError.agentError.offerRestart)
}
@@ -2418,7 +2407,6 @@ sealed class CC {
class APIGetChatItemTTL(val userId: Long): CC()
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
class APIGetNetworkConfig: CC()
class APISetNetworkInfo(val networkInfo: UserNetworkInfo): CC()
class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC()
class ApiSetMemberSettings(val groupId: Long, val groupMemberId: Long, val memberSettings: GroupMemberSettings): CC()
class APIContactInfo(val contactId: Long): CC()
@@ -2561,7 +2549,6 @@ sealed class CC {
is APIGetChatItemTTL -> "/_ttl $userId"
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
is APIGetNetworkConfig -> "/network"
is APISetNetworkInfo -> "/_network info ${json.encodeToString(networkInfo)}"
is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}"
is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}"
is APIContactInfo -> "/_info @$contactId"
@@ -2699,7 +2686,6 @@ sealed class CC {
is APIGetChatItemTTL -> "apiGetChatItemTTL"
is APISetNetworkConfig -> "apiSetNetworkConfig"
is APIGetNetworkConfig -> "apiGetNetworkConfig"
is APISetNetworkInfo -> "apiSetNetworkInfo"
is APISetChatSettings -> "apiSetChatSettings"
is ApiSetMemberSettings -> "apiSetMemberSettings"
is APIContactInfo -> "apiContactInfo"
@@ -3047,7 +3033,7 @@ data class NetCfg(
sessionMode = TransportSessionMode.User,
tcpConnectTimeout = 20_000_000,
tcpTimeout = 15_000_000,
tcpTimeoutPerKb = 10_000,
tcpTimeoutPerKb = 45_000,
tcpKeepAlive = KeepAliveOpts.defaults,
smpPingInterval = 1200_000_000,
smpPingCount = 3
@@ -3061,7 +3047,7 @@ data class NetCfg(
sessionMode = TransportSessionMode.User,
tcpConnectTimeout = 30_000_000,
tcpTimeout = 20_000_000,
tcpTimeoutPerKb = 15_000,
tcpTimeoutPerKb = 60_000,
tcpKeepAlive = KeepAliveOpts.defaults,
smpPingInterval = 1200_000_000,
smpPingCount = 3
@@ -3430,7 +3416,6 @@ interface Feature {
@Composable
fun iconFilled(): Painter
val hasParam: Boolean
val hasRole: Boolean
}
@Serializable
@@ -3450,7 +3435,6 @@ enum class ChatFeature: Feature {
TimedMessages -> true
else -> false
}
override val hasRole: Boolean = false
override val text: String
get() = when(this) {
@@ -3551,7 +3535,6 @@ enum class GroupFeature: Feature {
@SerialName("reactions") Reactions,
@SerialName("voice") Voice,
@SerialName("files") Files,
@SerialName("simplexLinks") SimplexLinks,
@SerialName("history") History;
override val hasParam: Boolean get() = when(this) {
@@ -3559,18 +3542,6 @@ enum class GroupFeature: Feature {
else -> false
}
override val hasRole: Boolean
get() = when (this) {
TimedMessages -> false
DirectMessages -> true
FullDelete -> false
Reactions -> false
Voice -> true
Files -> true
SimplexLinks -> true
History -> false
}
override val text: String
get() = when(this) {
TimedMessages -> generalGetString(MR.strings.timed_messages)
@@ -3579,7 +3550,6 @@ enum class GroupFeature: Feature {
Reactions -> generalGetString(MR.strings.message_reactions)
Voice -> generalGetString(MR.strings.voice_messages)
Files -> generalGetString(MR.strings.files_and_media)
SimplexLinks -> generalGetString(MR.strings.simplex_links)
History -> generalGetString(MR.strings.recent_history)
}
@@ -3591,7 +3561,6 @@ enum class GroupFeature: Feature {
Reactions -> painterResource(MR.images.ic_add_reaction)
Voice -> painterResource(MR.images.ic_keyboard_voice)
Files -> painterResource(MR.images.ic_draft)
SimplexLinks -> painterResource(MR.images.ic_link)
History -> painterResource(MR.images.ic_schedule)
}
@@ -3603,7 +3572,6 @@ enum class GroupFeature: Feature {
Reactions -> painterResource(MR.images.ic_add_reaction_filled)
Voice -> painterResource(MR.images.ic_keyboard_voice_filled)
Files -> painterResource(MR.images.ic_draft_filled)
SimplexLinks -> painterResource(MR.images.ic_link)
History -> painterResource(MR.images.ic_schedule_filled)
}
@@ -3634,10 +3602,6 @@ enum class GroupFeature: Feature {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.allow_to_send_files)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_sending_files)
}
SimplexLinks -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.allow_to_send_simplex_links)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.prohibit_sending_simplex_links)
}
History -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.enable_sending_recent_history)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.disable_sending_recent_history)
@@ -3669,10 +3633,6 @@ enum class GroupFeature: Feature {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_files)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.files_are_prohibited_in_group)
}
SimplexLinks -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.group_members_can_send_simplex_links)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.simplex_links_are_prohibited_in_group)
}
History -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(MR.strings.recent_history_is_sent_to_new_members)
GroupFeatureEnabled.OFF -> generalGetString(MR.strings.recent_history_is_not_sent_to_new_members)
@@ -3786,12 +3746,11 @@ enum class FeatureAllowed {
@Serializable
data class FullGroupPreferences(
val timedMessages: TimedMessagesGroupPreference,
val directMessages: RoleGroupPreference,
val directMessages: GroupPreference,
val fullDelete: GroupPreference,
val reactions: GroupPreference,
val voice: RoleGroupPreference,
val files: RoleGroupPreference,
val simplexLinks: RoleGroupPreference,
val voice: GroupPreference,
val files: GroupPreference,
val history: GroupPreference,
) {
fun toGroupPreferences(): GroupPreferences =
@@ -3802,19 +3761,17 @@ data class FullGroupPreferences(
reactions = reactions,
voice = voice,
files = files,
simplexLinks = simplexLinks,
history = history
)
companion object {
val sampleData = FullGroupPreferences(
timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF),
directMessages = RoleGroupPreference(GroupFeatureEnabled.OFF, role = null),
directMessages = GroupPreference(GroupFeatureEnabled.OFF),
fullDelete = GroupPreference(GroupFeatureEnabled.OFF),
reactions = GroupPreference(GroupFeatureEnabled.ON),
voice = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
files = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
voice = GroupPreference(GroupFeatureEnabled.ON),
files = GroupPreference(GroupFeatureEnabled.ON),
history = GroupPreference(GroupFeatureEnabled.ON),
)
}
@@ -3823,23 +3780,21 @@ data class FullGroupPreferences(
@Serializable
data class GroupPreferences(
val timedMessages: TimedMessagesGroupPreference? = null,
val directMessages: RoleGroupPreference? = null,
val directMessages: GroupPreference? = null,
val fullDelete: GroupPreference? = null,
val reactions: GroupPreference? = null,
val voice: RoleGroupPreference? = null,
val files: RoleGroupPreference? = null,
val simplexLinks: RoleGroupPreference? = null,
val voice: GroupPreference? = null,
val files: GroupPreference? = null,
val history: GroupPreference? = null,
) {
companion object {
val sampleData = GroupPreferences(
timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF),
directMessages = RoleGroupPreference(GroupFeatureEnabled.OFF, role = null),
directMessages = GroupPreference(GroupFeatureEnabled.OFF),
fullDelete = GroupPreference(GroupFeatureEnabled.OFF),
reactions = GroupPreference(GroupFeatureEnabled.ON),
voice = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
files = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
simplexLinks = RoleGroupPreference(GroupFeatureEnabled.ON, role = null),
voice = GroupPreference(GroupFeatureEnabled.ON),
files = GroupPreference(GroupFeatureEnabled.ON),
history = GroupPreference(GroupFeatureEnabled.ON),
)
}
@@ -3850,26 +3805,6 @@ data class GroupPreference(
val enable: GroupFeatureEnabled
) {
val on: Boolean get() = enable == GroupFeatureEnabled.ON
fun enabled(role: GroupMemberRole?, m: GroupMember?): GroupFeatureEnabled =
when (enable) {
GroupFeatureEnabled.OFF -> GroupFeatureEnabled.OFF
GroupFeatureEnabled.ON ->
if (role != null && m != null) {
if (m.memberRole >= role) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF
} else {
GroupFeatureEnabled.ON
}
}
}
@Serializable
data class RoleGroupPreference(
val enable: GroupFeatureEnabled,
val role: GroupMemberRole? = null,
) {
fun on(m: GroupMember): Boolean =
enable == GroupFeatureEnabled.ON && m.memberRole >= (role ?: GroupMemberRole.Observer)
}
@Serializable
@@ -4823,7 +4758,6 @@ sealed class ChatErrorType {
is FallbackToSMPProhibited -> "fallbackToSMPProhibited"
is InlineFileProhibited -> "inlineFileProhibited"
is InvalidQuote -> "invalidQuote"
is InvalidForward -> "invalidForward"
is InvalidChatItemUpdate -> "invalidChatItemUpdate"
is InvalidChatItemDelete -> "invalidChatItemDelete"
is HasCurrentCall -> "hasCurrentCall"
@@ -4902,7 +4836,6 @@ sealed class ChatErrorType {
@Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType()
@Serializable @SerialName("inlineFileProhibited") class InlineFileProhibited(val fileId: Long): ChatErrorType()
@Serializable @SerialName("invalidQuote") object InvalidQuote: ChatErrorType()
@Serializable @SerialName("invalidForward") object InvalidForward: ChatErrorType()
@Serializable @SerialName("invalidChatItemUpdate") object InvalidChatItemUpdate: ChatErrorType()
@Serializable @SerialName("invalidChatItemDelete") object InvalidChatItemDelete: ChatErrorType()
@Serializable @SerialName("hasCurrentCall") object HasCurrentCall: ChatErrorType()
@@ -5592,26 +5525,3 @@ enum class AppSettingsLockScreenCalls {
}
}
}
@Serializable
data class UserNetworkInfo(
val networkType: UserNetworkType,
val online: Boolean,
)
enum class UserNetworkType {
@SerialName("none") NONE,
@SerialName("cellular") CELLULAR,
@SerialName("wifi") WIFI,
@SerialName("ethernet") ETHERNET,
@SerialName("other") OTHER;
val text: String
get() = when (this) {
NONE -> generalGetString(MR.strings.network_type_no_network_connection)
CELLULAR -> generalGetString(MR.strings.network_type_cellular)
WIFI -> generalGetString(MR.strings.network_type_network_wifi)
ETHERNET -> generalGetString(MR.strings.network_type_ethernet)
OTHER -> generalGetString(MR.strings.network_type_other)
}
}
@@ -9,6 +9,7 @@ import chat.simplex.common.views.helpers.DatabaseUtils.randomDatabasePassword
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import java.io.File
import java.nio.ByteBuffer
@@ -87,7 +88,6 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
Log.d(TAG, "Unable to migrate successfully: $res")
return
}
platform.androidRestartNetworkObserver()
controller.apiSetTempFolder(coreTmpDir.absolutePath)
controller.apiSetFilesFolder(appFilesDir.absolutePath)
if (appPlatform.isDesktop) {
@@ -23,7 +23,6 @@ interface PlatformInterface {
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
fun androidPictureInPictureAllowed(): Boolean = true
fun androidCallEnded() {}
fun androidRestartNetworkObserver() {}
@Composable fun androidLockPortraitOrientation() {}
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
@Composable fun desktopScrollBarComponents(): Triple<Animatable<Float, AnimationVector1D>, Modifier, MutableState<Job>> = remember { Triple(Animatable(0f), Modifier, mutableStateOf(Job())) }
@@ -39,13 +39,4 @@ interface SoundPlayerInterface {
fun stop()
}
interface CallSoundsPlayerInterface {
fun startConnectingCallSound(scope: CoroutineScope)
fun startInCallSound(scope: CoroutineScope)
fun stop()
fun vibrate(times: Int = 1)
}
expect object SoundPlayer: SoundPlayerInterface
expect object CallSoundsPlayer: CallSoundsPlayerInterface
@@ -85,7 +85,6 @@ fun TerminalLayout(
isDirectChat = false,
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
@@ -1,26 +1,6 @@
package chat.simplex.common.views.call
import androidx.compose.runtime.Composable
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.platform.*
import kotlinx.coroutines.*
@Composable
expect fun ActiveCallView()
fun activeCallWaitDeliveryReceipt(scope: CoroutineScope) = scope.launch(Dispatchers.Default) {
for (apiResp in controller.messagesChannel) {
val call = chatModel.activeCall.value
if (call == null || call.callState > CallState.InvitationSent) break
val msg = apiResp.resp
if (apiResp.remoteHostId == call.remoteHostId &&
msg is CR.ChatItemStatusUpdated &&
msg.chatItem.chatInfo.id == call.contact.id &&
msg.chatItem.chatItem.content is CIContent.SndCall &&
msg.chatItem.chatItem.meta.itemStatus is CIStatus.SndRcvd) {
CallSoundsPlayer.startInCallSound(scope)
break
}
}
}
@@ -187,11 +187,10 @@ data class ConnectionState(
)
// the servers are expected in this format:
// stuns:stun.simplex.im:443?transport=tcp
// turns:private2:Hxuq2QxUjnhj96Zq2r4HjqHRj@turn.simplex.im:443?transport=tcp
// stun:stun.simplex.im:443?transport=tcp
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp
fun parseRTCIceServer(str: String): RTCIceServer? {
var s = replaceScheme(str, "stun:")
s = replaceScheme(s, "stuns:")
s = replaceScheme(s, "turn:")
s = replaceScheme(s, "turns:")
val u = runCatching { URI(s) }.getOrNull()
@@ -199,7 +198,7 @@ fun parseRTCIceServer(str: String): RTCIceServer? {
val scheme = u.scheme
val host = u.host
val port = u.port
if (u.path == "" && (scheme == "stun" || scheme == "stuns" || scheme == "turn" || scheme == "turns")) {
if (u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns")) {
val userInfo = u.userInfo?.split(":")
val query = if (u.query == null || u.query == "") "" else "?${u.query}"
return RTCIceServer(
@@ -962,7 +962,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
@Composable
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) {
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null
if (chat.chatInfo is ChatInfo.Group) {
if (cItem.chatDir is CIDirection.GroupRcv) {
val member = cItem.chatDir.groupMember
@@ -1392,12 +1392,11 @@ private fun providerForGallery(
return null
}
// Pager has a bug with overflowing when total pages is around Int.MAX_VALUE. Using smaller value
var initialIndex = 10000 / 2
var initialIndex = Int.MAX_VALUE / 2
var initialChatId = cItemId
return object: ImageGalleryProvider {
override val initialIndex: Int = initialIndex
override val totalMediaSize = mutableStateOf(10000)
override val totalMediaSize = mutableStateOf(Int.MAX_VALUE)
override fun getMedia(index: Int): ProviderMedia? {
val internalIndex = initialIndex - index
val item = item(internalIndex, initialChatId)?.second ?: return null
@@ -1,7 +1,6 @@
@file:UseSerializers(UriSerializer::class)
package chat.simplex.common.views.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
@@ -12,8 +11,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.font.FontStyle
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
@@ -21,7 +18,8 @@ import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.filesToDelete
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.ui.theme.Indigo
import chat.simplex.common.ui.theme.isSystemInDarkTheme
import chat.simplex.common.views.chat.item.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
@@ -117,15 +115,6 @@ data class ComposeState(
}
}
val attachmentPreview: Boolean
get() = when (preview) {
ComposePreview.NoPreview -> false
is ComposePreview.CLinkPreview -> false
is ComposePreview.MediaPreview -> preview.content.isNotEmpty()
is ComposePreview.VoicePreview -> false
is ComposePreview.FilePreview -> true
}
val empty: Boolean
get() = message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem
@@ -254,23 +243,10 @@ fun ComposeView(
attachmentOption: MutableState<AttachmentOption?>,
showChooseAttachment: () -> Unit
) {
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
fun isSimplexLink(link: String): Boolean =
link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true)
fun parseMessage(msg: String): Pair<String?, Boolean> {
if (msg.isBlank()) return null to false
val parsedMsg = parseToMarkdown(msg) ?: return null to false
val link = parsedMsg.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) }
val simplexLink = parsedMsg.any { ft -> ft.format is Format.SimplexLink }
return link?.text to simplexLink
}
val linkUrl = rememberSaveable { mutableStateOf<String?>(null) }
// default value parsed because of draft
val hasSimplexLink = rememberSaveable { mutableStateOf(parseMessage(composeState.value.message).second) }
val prevLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val saveLastDraft = chatModel.controller.appPrefs.privacySaveLastDraft.get()
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
@@ -279,6 +255,15 @@ fun ComposeView(
AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile) { uris, text -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(uris, text) } }
fun isSimplexLink(link: String): Boolean =
link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true)
fun parseMessage(msg: String): String? {
val parsedMsg = parseToMarkdown(msg)
val link = parsedMsg?.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) }
return link?.text
}
fun loadLinkPreview(url: String, wait: Long? = null) {
if (pendingLinkUrl.value == url) {
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null))
@@ -298,9 +283,7 @@ fun ComposeView(
fun showLinkPreview(s: String) {
prevLinkUrl.value = linkUrl.value
val parsed = parseMessage(s)
linkUrl.value = parsed.first
hasSimplexLink.value = parsed.second
linkUrl.value = parseMessage(s)
val url = linkUrl.value
if (url != null) {
if (url != composeState.value.linkPreview?.uri && url != pendingLinkUrl.value) {
@@ -404,7 +387,7 @@ fun ComposeView(
fun checkLinkPreview(): MsgContent {
return when (val composePreview = cs.preview) {
is ComposePreview.CLinkPreview -> {
val url = parseMessage(msgText).first
val url = parseMessage(msgText)
val lp = composePreview.linkPreview
if (lp != null && url == lp.uri) {
MsgContent.MCLink(msgText, preview = lp)
@@ -580,16 +563,8 @@ fun ComposeView(
} else {
textStyle.value = smallFont
if (composeState.value.linkPreviewAllowed) {
if (s.isNotEmpty()) {
showLinkPreview(s)
} else {
resetLinkPreview()
hasSimplexLink.value = false
}
} else if (s.isNotEmpty() && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)) {
hasSimplexLink.value = parseMessage(s).second
} else {
hasSimplexLink.value = false
if (s.isNotEmpty()) showLinkPreview(s)
else resetLinkPreview()
}
}
}
@@ -725,16 +700,6 @@ fun ComposeView(
}
}
@Composable
fun MsgNotAllowedView(reason: String, icon: Painter) {
val color = CurrentColors.collectAsState().value.appColors.receivedMessage
Row(Modifier.padding(top = 5.dp).fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) {
Icon(icon, null, tint = MaterialTheme.colors.secondary)
Spacer(Modifier.width(DEFAULT_PADDING_HALF))
Text(reason, fontStyle = FontStyle.Italic)
}
}
@Composable
fun contextItemView() {
when (val contextItem = composeState.value.contextItem) {
@@ -778,17 +743,7 @@ fun ComposeView(
if (nextSendGrpInv.value) {
ComposeContextInvitingContactMemberView()
}
val simplexLinkProhibited = hasSimplexLink.value && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)
val fileProhibited = composeState.value.attachmentPreview && !chat.groupFeatureEnabled(GroupFeature.Files)
val voiceProhibited = composeState.value.preview is ComposePreview.VoicePreview && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) {
if (simplexLinkProhibited) {
MsgNotAllowedView(generalGetString(MR.strings.simplex_links_not_allowed), icon = painterResource(MR.images.ic_link))
} else if (fileProhibited) {
MsgNotAllowedView(generalGetString(MR.strings.files_and_media_not_allowed), icon = painterResource(MR.images.ic_draft))
} else if (voiceProhibited) {
MsgNotAllowedView(generalGetString(MR.strings.voice_messages_not_allowed), icon = painterResource(MR.images.ic_mic))
}
contextItemView()
when {
composeState.value.editing && composeState.value.preview is ComposePreview.VoicePreview -> {}
@@ -809,7 +764,7 @@ fun ComposeView(
modifier = Modifier.padding(end = 8.dp),
verticalAlignment = Alignment.Bottom,
) {
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on
val attachmentClicked = if (isGroupAndProhibitedFiles) {
{
AlertManager.shared.showAlertMsg(
@@ -916,7 +871,6 @@ fun ComposeView(
chat.chatInfo is ChatInfo.Direct,
liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown,
sendMsgEnabled = sendMsgEnabled.value,
sendButtonEnabled = sendMsgEnabled.value && !(simplexLinkProhibited || fileProhibited || voiceProhibited),
nextSendGrpInv = nextSendGrpInv.value,
needToAllowVoiceToContact,
allowedVoiceByPrefs,
@@ -39,7 +39,6 @@ fun SendMsgView(
isDirectChat: Boolean,
liveMessageAlertShown: SharedPreference<Boolean>,
sendMsgEnabled: Boolean,
sendButtonEnabled: Boolean,
nextSendGrpInv: Boolean,
needToAllowVoiceToContact: Boolean,
allowedVoiceByPrefs: Boolean,
@@ -76,8 +75,7 @@ fun SendMsgView(
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
val sendMsgButtonDisabled = !sendMsgEnabled || !cs.sendEnabled() ||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled ||
!sendButtonEnabled
cs.endLiveDisabled
PlatformTextField(composeState, sendMsgEnabled, sendMsgButtonDisabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage, onFilesPasted) {
if (!cs.inProgress) {
sendMessage(null)
@@ -432,7 +430,7 @@ private fun SendMsgButton(
.padding(4.dp)
.alpha(alpha.value)
.clip(CircleShape)
.background(if (enabled) sendButtonColor else MaterialTheme.colors.secondary.copy(alpha = 0.75f))
.background(if (enabled) sendButtonColor else MaterialTheme.colors.secondary)
.padding(3.dp)
)
}
@@ -554,7 +552,6 @@ fun PreviewSendMsgView() {
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
@@ -589,7 +586,6 @@ fun PreviewSendMsgViewEditing() {
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
@@ -624,7 +620,6 @@ fun PreviewSendMsgViewInProgress() {
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
sendButtonEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
@@ -309,7 +309,7 @@ fun GroupMemberInfoLayout(
SectionView {
if (contactId != null && knownDirectChat(contactId) != null) {
OpenChatButton(onClick = { openDirectChat(contactId) })
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
if (contactId != null) {
OpenChatButton(onClick = { openDirectChat(contactId) })
} else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) {
@@ -335,7 +335,7 @@ fun GroupMemberInfoLayout(
val clipboard = LocalClipboardManager.current
ShareAddressButton { clipboard.shareText(simplexChatLink(member.contactLink)) }
if (contactId != null) {
if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) {
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
}
} else {
@@ -6,6 +6,7 @@ import SectionDividerSpaced
import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
@@ -19,20 +20,13 @@ import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon
import chat.simplex.common.model.*
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
private val featureRoles: List<Pair<GroupMemberRole?, String>> = listOf(
null to generalGetString(MR.strings.feature_roles_all_members),
GroupMemberRole.Admin to generalGetString(MR.strings.feature_roles_admins),
GroupMemberRole.Owner to generalGetString(MR.strings.feature_roles_owners)
)
@Composable
fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit) {
fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> Unit,) {
val groupInfo = remember { derivedStateOf {
val ch = m.getChat(chatId)
val g = (ch?.chatInfo as? ChatInfo.Group)?.groupInfo
if (g == null || ch.remoteHostId != rhId) null else g
if (g == null || ch?.remoteHostId != rhId) null else g
}}
val gInfo = groupInfo.value ?: return
var preferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(gInfo.fullGroupPreferences) }
@@ -87,7 +81,7 @@ private fun GroupPreferencesLayout(
val onTTLUpdated = { ttl: Int? ->
applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl)))
}
FeatureSection(GroupFeature.TimedMessages, timedMessages, null, groupInfo, preferences, onTTLUpdated) { enable, _ ->
FeatureSection(GroupFeature.TimedMessages, timedMessages, groupInfo, preferences, onTTLUpdated) { enable ->
if (enable == GroupFeatureEnabled.ON) {
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = preferences.timedMessages.ttl ?: 86400)))
} else {
@@ -96,45 +90,33 @@ private fun GroupPreferencesLayout(
}
SectionDividerSpaced(true, maxBottomPadding = false)
val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) }
val directMessagesRole = remember(preferences) { mutableStateOf(preferences.directMessages.role) }
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, directMessagesRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
applyPrefs(preferences.copy(directMessages = RoleGroupPreference(enable = enable, role)))
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(directMessages = GroupPreference(enable = it)))
}
SectionDividerSpaced(true, maxBottomPadding = false)
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) }
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, null, groupInfo, preferences, onTTLUpdated) { enable, _ ->
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = enable)))
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it)))
}
SectionDividerSpaced(true, maxBottomPadding = false)
val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.enable) }
FeatureSection(GroupFeature.Reactions, allowReactions, null, groupInfo, preferences, onTTLUpdated) { enable, _ ->
applyPrefs(preferences.copy(reactions = GroupPreference(enable = enable)))
FeatureSection(GroupFeature.Reactions, allowReactions, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(reactions = GroupPreference(enable = it)))
}
SectionDividerSpaced(true, maxBottomPadding = false)
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) }
val voiceRole = remember(preferences) { mutableStateOf(preferences.voice.role) }
FeatureSection(GroupFeature.Voice, allowVoice, voiceRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
applyPrefs(preferences.copy(voice = RoleGroupPreference(enable = enable, role)))
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(voice = GroupPreference(enable = it)))
}
SectionDividerSpaced(true, maxBottomPadding = false)
val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) }
val filesRole = remember(preferences) { mutableStateOf(preferences.files.role) }
FeatureSection(GroupFeature.Files, allowFiles, filesRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
applyPrefs(preferences.copy(files = RoleGroupPreference(enable = enable, role)))
FeatureSection(GroupFeature.Files, allowFiles, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(files = GroupPreference(enable = it)))
}
// TODO enable simplexLinks preference in 5.8
// SectionDividerSpaced(true, maxBottomPadding = false)
// val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) }
// val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) }
// FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role ->
// applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role)))
// }
SectionDividerSpaced(true, maxBottomPadding = false)
val enableHistory = remember(preferences) { mutableStateOf(preferences.history.enable) }
FeatureSection(GroupFeature.History, enableHistory, null, groupInfo, preferences, onTTLUpdated) { enable, _ ->
applyPrefs(preferences.copy(history = GroupPreference(enable = enable)))
FeatureSection(GroupFeature.History, enableHistory, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(history = GroupPreference(enable = it)))
}
if (groupInfo.canEdit) {
SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false)
@@ -152,11 +134,10 @@ private fun GroupPreferencesLayout(
private fun FeatureSection(
feature: GroupFeature,
enableFeature: State<GroupFeatureEnabled>,
enableForRole: State<GroupMemberRole?>? = null,
groupInfo: GroupInfo,
preferences: FullGroupPreferences,
onTTLUpdated: (Int?) -> Unit,
onSelected: (GroupFeatureEnabled, GroupMemberRole?) -> Unit
onSelected: (GroupFeatureEnabled) -> Unit
) {
SectionView {
val on = enableFeature.value == GroupFeatureEnabled.ON
@@ -170,7 +151,7 @@ private fun FeatureSection(
iconTint,
enableFeature.value == GroupFeatureEnabled.ON,
) { checked ->
onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF, enableForRole?.value)
onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF)
}
if (timedOn) {
val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) }
@@ -184,18 +165,6 @@ private fun FeatureSection(
onSelected = onTTLUpdated
)
}
if (enableFeature.value == GroupFeatureEnabled.ON && enableForRole != null) {
ExposedDropDownSettingRow(
generalGetString(MR.strings.feature_enabled_for),
featureRoles,
enableForRole,
// remove in v5.8
enabled = remember { mutableStateOf(false) },
onSelected = { value ->
onSelected(enableFeature.value, value)
}
)
}
} else {
InfoRow(
feature.text,
@@ -206,14 +175,6 @@ private fun FeatureSection(
if (timedOn) {
InfoRow(generalGetString(MR.strings.delete_after), timeText(preferences.timedMessages.ttl))
}
if (enableFeature.value == GroupFeatureEnabled.ON && enableForRole != null) {
InfoRow(generalGetString(MR.strings.feature_enabled_for), featureRoles.firstOrNull { it.first == enableForRole.value }?.second ?: generalGetString(MR.strings.feature_roles_all_members), textColor = MaterialTheme.colors.secondary)
}
}
}
KeyChangeEffect(enableFeature.value) {
if (enableFeature.value == GroupFeatureEnabled.OFF) {
onSelected(enableFeature.value, null)
}
}
SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.canEdit))
@@ -16,7 +16,6 @@ import chat.simplex.common.platform.onRightClick
@Composable
fun CIChatFeatureView(
chatInfo: ChatInfo,
chatItem: ChatItem,
feature: Feature,
iconColor: Color,
@@ -24,7 +23,7 @@ fun CIChatFeatureView(
revealed: MutableState<Boolean>,
showMenu: MutableState<Boolean>,
) {
val merged = if (!revealed.value) mergedFeatures(chatItem, chatInfo) else emptyList()
val merged = if (!revealed.value) mergedFeatures(chatItem) else emptyList()
Box(
Modifier
.combinedClickable(
@@ -71,7 +70,7 @@ private fun Feature.toFeatureInfo(color: Color, param: Int?, type: String): Feat
)
@Composable
private fun mergedFeatures(chatItem: ChatItem, chatInfo: ChatInfo): List<FeatureInfo>? {
private fun mergedFeatures(chatItem: ChatItem): List<FeatureInfo>? {
val m = ChatModel
val fs: ArrayList<FeatureInfo> = arrayListOf()
val icons: MutableSet<PainterBox> = mutableSetOf()
@@ -79,7 +78,7 @@ private fun mergedFeatures(chatItem: ChatItem, chatInfo: ChatInfo): List<Feature
if (i != null) {
val reversedChatItems = m.chatItems.asReversed()
while (i < reversedChatItems.size) {
val f = featureInfo(reversedChatItems[i], chatInfo) ?: break
val f = featureInfo(reversedChatItems[i]) ?: break
if (!icons.contains(f.icon)) {
fs.add(0, f)
icons.add(f.icon)
@@ -91,12 +90,12 @@ private fun mergedFeatures(chatItem: ChatItem, chatInfo: ChatInfo): List<Feature
}
@Composable
private fun featureInfo(ci: ChatItem, chatInfo: ChatInfo): FeatureInfo? =
private fun featureInfo(ci: ChatItem): FeatureInfo? =
when (ci.content) {
is CIContent.RcvChatFeature -> ci.content.feature.toFeatureInfo(ci.content.enabled.iconColor, ci.content.param, ci.content.feature.name)
is CIContent.SndChatFeature -> ci.content.feature.toFeatureInfo(ci.content.enabled.iconColor, ci.content.param, ci.content.feature.name)
is CIContent.RcvGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enabled(ci.content.memberRole_, (chatInfo as ChatInfo.Group).groupInfo.membership).iconColor, ci.content.param, ci.content.groupFeature.name)
is CIContent.SndGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enabled(ci.content.memberRole_, (chatInfo as ChatInfo.Group).groupInfo.membership).iconColor, ci.content.param, ci.content.groupFeature.name)
is CIContent.RcvGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enable.iconColor, ci.content.param, ci.content.groupFeature.name)
is CIContent.SndGroupFeature -> ci.content.groupFeature.toFeatureInfo(ci.content.preference.enable.iconColor, ci.content.param, ci.content.groupFeature.name)
else -> null
}
@@ -304,7 +304,7 @@ fun ChatItemView(
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed)
MarkedDeletedItemDropdownMenu()
} else {
if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem, cInfo.timedMessagesTTL)
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
@@ -442,11 +442,11 @@ fun ChatItemView(
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatFeature -> {
CIChatFeatureView(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndChatFeature -> {
CIChatFeatureView(cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatPreference -> {
@@ -454,23 +454,23 @@ fun ChatItemView(
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
}
is CIContent.SndChatPreference -> {
CIChatFeatureView(cInfo, cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu)
CIChatFeatureView(cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupFeature -> {
CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as ChatInfo.Group).groupInfo.membership).iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndGroupFeature -> {
CIChatFeatureView(cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as ChatInfo.Group).groupInfo.membership).iconColor, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatFeatureRejected -> {
CIChatFeatureView(cInfo, cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupFeatureRejected -> {
CIChatFeatureView(cInfo, cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu)
CIChatFeatureView(cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndModerated -> DeletedItem()
@@ -724,7 +724,7 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes
deleteMessage(chatItem.id, CIDeleteMode.cidmInternal)
AlertManager.shared.hideAlert()
}) { Text(stringResource(MR.strings.for_me_only), color = MaterialTheme.colors.error) }
if (chatItem.meta.deletable && !chatItem.localNote) {
if (chatItem.meta.editable && !chatItem.localNote) {
Spacer(Modifier.padding(horizontal = 4.dp))
TextButton(onClick = {
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
@@ -96,7 +96,6 @@ fun FramedItemView(
.fillMaxWidth()
.padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (ci.quotedItem == null) 6.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (icon != null) {
Icon(
@@ -185,7 +184,7 @@ fun FramedItemView(
}
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) &&
!ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null && ci.meta.itemForwarded == null
!ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
@@ -220,11 +219,7 @@ fun FramedItemView(
} else if (ci.meta.isLive) {
FramedItemHeader(stringResource(MR.strings.live), false)
}
if (ci.quotedItem != null) {
ciQuoteView(ci.quotedItem)
} else if (ci.meta.itemForwarded != null) {
FramedItemHeader(stringResource(MR.strings.forwarded_description), true, painterResource(MR.images.ic_forward))
}
ci.quotedItem?.let { ciQuoteView(it) }
if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
Column(
@@ -190,7 +190,6 @@ class AlertManager {
fun showAlertMsg(
title: String, text: String? = null,
confirmText: String = generalGetString(MR.strings.ok),
onConfirm: (() -> Unit)? = null,
hostDevice: Pair<Long?, String>? = null,
shareText: Boolean? = null
) {
@@ -221,7 +220,6 @@ class AlertManager {
}
TextButton(
onClick = {
onConfirm?.invoke()
hideAlert()
},
Modifier.focusRequester(focusRequester)
@@ -259,9 +257,8 @@ class AlertManager {
title: StringResource,
text: StringResource? = null,
confirmText: StringResource = MR.strings.ok,
onConfirm: (() -> Unit)? = null,
hostDevice: Pair<Long?, String>? = null,
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), onConfirm, hostDevice)
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), hostDevice)
@Composable
fun showInView() {
@@ -254,12 +254,12 @@ fun TextIconSpaced(extraPadding: Boolean = false) {
}
@Composable
fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground) {
fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null) {
SectionItemViewSpaceBetween {
Row {
val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
if (icon != null) Icon(icon, title, Modifier.padding(end = 8.dp).size(iconSize), tint = iconTint ?: MaterialTheme.colors.secondary)
Text(title, color = textColor)
Text(title)
}
Text(value, color = MaterialTheme.colors.secondary)
}
@@ -15,6 +15,7 @@ import chat.simplex.res.MR
import com.charleskorn.kaml.decodeFromStream
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import java.io.*
import java.net.URI
@@ -38,7 +38,9 @@ import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.runBlocking
@Composable
fun ConnectMobileView() {
@@ -267,20 +269,12 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState<Boolean>, c
var cachedR by remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) }
val customPort = rememberSaveable { mutableStateOf<Int?>(null) }
var userChangedAddress by rememberSaveable { mutableStateOf(false) }
var userChangedPort by rememberSaveable { mutableStateOf(false) }
val startRemoteHost = suspend {
if (customAddress.value != cachedR.address && cachedR != null) {
userChangedAddress = true
}
if (customPort.value != cachedR.port && cachedR != null) {
userChangedPort = true
}
val r = chatModel.controller.startRemoteHost(
rhId = null,
multicast = controller.appPrefs.offerRemoteMulticast.get(),
address = if (customAddress.value != null && userChangedAddress) customAddress.value else cachedR.rh?.bindAddress_,
port = if (customPort.value != null && userChangedPort) customPort.value else cachedR.rh?.bindPort_
address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_,
port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_
)
if (r != null) {
cachedR = r
@@ -349,20 +343,12 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState
var cachedR by remember { mutableStateOf<CR.RemoteHostStarted?>(null) }
val customAddress = rememberSaveable { mutableStateOf<RemoteCtrlAddress?>(null) }
val customPort = rememberSaveable { mutableStateOf<Int?>(null) }
var userChangedAddress by rememberSaveable { mutableStateOf(false) }
var userChangedPort by rememberSaveable { mutableStateOf(false) }
val startRemoteHost = suspend {
if (customAddress.value != cachedR.address && cachedR != null) {
userChangedAddress = true
}
if (customPort.value != cachedR.port && cachedR != null) {
userChangedPort = true
}
val r = chatModel.controller.startRemoteHost(
rhId = rh.remoteHostId,
multicast = controller.appPrefs.offerRemoteMulticast.get(),
address = if (customAddress.value != null && userChangedAddress) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_,
port = if (customPort.value != null && userChangedPort) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_
address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_,
port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_
)
if (r != null) {
cachedR = r
@@ -167,7 +167,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
// can't be higher than 130ms to avoid overflow on 32bit systems
TimeoutSettingRow(
stringResource(MR.strings.network_option_protocol_timeout_per_kb), networkTCPTimeoutPerKb,
listOf(2_500, 5_000, 10_000, 15_000, 20_000, 30_000), secondsLabel
listOf(15_000, 30_000, 45_000, 60_000, 90_000, 120_000), secondsLabel
)
}
SectionItemView {
@@ -2,7 +2,6 @@ package chat.simplex.common.views.usersettings
import SectionBottomSpacer
import SectionCustomFooter
import SectionDividerSpaced
import SectionItemView
import SectionItemWithValue
import SectionView
@@ -22,12 +21,12 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.input.*
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ClickableText
import chat.simplex.common.views.helpers.*
@@ -191,16 +190,6 @@ fun NetworkAndServersView() {
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
}
if (appPlatform.isAndroid) {
SectionDividerSpaced()
SectionView(generalGetString(MR.strings.settings_section_title_network_connection).uppercase()) {
val info = remember { chatModel.networkInfo }.value
SettingsActionItemWithContent(icon = null, info.networkType.text) {
Icon(painterResource(MR.images.ic_circle_filled), stringResource(MR.strings.icon_descr_server_status_connected), tint = if (info.online) Color.Green else MaterialTheme.colors.error)
}
}
}
SectionBottomSpacer()
}
}
@@ -46,7 +46,6 @@
<string name="invalid_message_format">invalid message format</string>
<string name="live">LIVE</string>
<string name="moderated_description">moderated</string>
<string name="forwarded_description">forwarded</string>
<string name="invalid_chat">invalid chat</string>
<string name="invalid_data">invalid data</string>
<string name="error_showing_message">error showing message</string>
@@ -348,9 +347,6 @@
<string name="files_and_media_prohibited">Files and media prohibited!</string>
<string name="only_owners_can_enable_files_and_media">Only group owners can enable files and media.</string>
<string name="compose_send_direct_message_to_connect">Send direct message to connect</string>
<string name="simplex_links_not_allowed">SimpleX links not allowed</string>
<string name="files_and_media_not_allowed">Files and media not allowed</string>
<string name="voice_messages_not_allowed">Voice messages not allowed</string>
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Image</string>
@@ -818,14 +814,6 @@
<!-- CallView -->
<string name="unable_to_open_browser_title">Error opening browser</string>
<string name="unable_to_open_browser_desc">The default web browser is required for calls. Please configure the default browser in the system, and share more information with the developers.</string>
<string name="permissions_required">Grant permissions</string>
<string name="permissions_record_audio">Microphone</string>
<string name="permissions_camera">Camera</string>
<string name="permissions_camera_and_record_audio">Camera and microphone</string>
<string name="permissions_grant">Grant permission(s) to make calls</string>
<string name="permissions_grant_in_settings">Grant in settings</string>
<string name="permissions_find_in_settings_and_grant">Find this permission in Android settings and grant it manually.</string>
<string name="permissions_open_settings">Open settings</string>
<!-- SimpleXInfo -->
<string name="next_generation_of_private_messaging">The next generation of private messaging</string>
@@ -1021,7 +1009,6 @@
<string name="settings_section_title_themes">THEMES</string>
<string name="settings_section_title_messages">MESSAGES AND FILES</string>
<string name="settings_section_title_calls">CALLS</string>
<string name="settings_section_title_network_connection">Network connection</string>
<string name="settings_section_title_incognito">Incognito mode</string>
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>
<string name="settings_section_title_use_from_desktop">Use from desktop</string>
@@ -1535,7 +1522,6 @@
<string name="message_reactions">Message reactions</string>
<string name="voice_messages">Voice messages</string>
<string name="files_and_media">Files and media</string>
<string name="simplex_links">SimpleX links</string>
<string name="recent_history">Visible history</string>
<string name="audio_video_calls">Audio/video calls</string>
<string name="available_in_v51">\nAvailable in v5.1</string>
@@ -1593,8 +1579,6 @@
<string name="prohibit_message_reactions_group">Prohibit messages reactions.</string>
<string name="allow_to_send_files">Allow to send files and media.</string>
<string name="prohibit_sending_files">Prohibit sending files and media.</string>
<string name="allow_to_send_simplex_links">Allow to send SimpleX links.</string>
<string name="prohibit_sending_simplex_links">Prohibit sending SimpleX links</string>
<string name="enable_sending_recent_history">Send up to 100 last messages to new members.</string>
<string name="disable_sending_recent_history">Do not send history to new members.</string>
<string name="group_members_can_send_disappearing">Group members can send disappearing messages.</string>
@@ -1609,8 +1593,6 @@
<string name="message_reactions_are_prohibited">Message reactions are prohibited in this group.</string>
<string name="group_members_can_send_files">Group members can send files and media.</string>
<string name="files_are_prohibited_in_group">Files and media are prohibited in this group.</string>
<string name="group_members_can_send_simplex_links">Group members can send SimpleX links.</string>
<string name="simplex_links_are_prohibited_in_group">SimpleX links are prohibited in this group.</string>
<string name="recent_history_is_sent_to_new_members">Up to 100 last messages are sent to new members.</string>
<string name="recent_history_is_not_sent_to_new_members">History is not sent to new members.</string>
<string name="delete_after">Delete after</string>
@@ -1633,10 +1615,6 @@
<string name="feature_offered_item">offered %s</string>
<string name="feature_offered_item_with_param">offered %s: %2s</string>
<string name="feature_cancelled_item">cancelled %s</string>
<string name="feature_roles_all_members">all members</string>
<string name="feature_roles_admins">admins</string>
<string name="feature_roles_owners">owners</string>
<string name="feature_enabled_for">Enabled for</string>
<!-- WhatsNewView.kt -->
<string name="whats_new">What\'s new</string>
@@ -1939,11 +1917,4 @@
<string name="migrate_from_device_check_connection_and_try_again">Check your internet connection and try again</string>
<string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Warning</b>: the archive will be deleted.]]></string>
<string name="migrate_from_device_error_verifying_passphrase">Error verifying passphrase:</string>
<!-- NetworkObserver.kt -->
<string name="network_type_no_network_connection">No network connection</string>
<string name="network_type_cellular">Cellular</string>
<string name="network_type_network_wifi">WiFi</string>
<string name="network_type_ethernet">Wired ethernet</string>
<string name="network_type_other">Other</string>
</resources>
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path transform="scale(-1,1) translate(-960,0)" d="m236.5-495.5 142.5 143q8.5 8.5 8.25 20.25T378.5-312q-9 8.5-21 8.5t-20.5-9L145.5-504q-9-8.5-9-20t9-20.5L338-737q9-9 20.75-8.75T379.5-737q8.5 8.5 8.5 20.5t-8.5 20.5l-143 143h403q84 0 139.75 56T835-357.5V-234q0 12.5-8.25 20.75T806.5-205q-12.5 0-20.75-8.25T777.5-234v-123.5q0-60-39-99t-99-39h-403Z"/></svg>

Before

Width:  |  Height:  |  Size: 441 B

@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="120"
height="120"
viewBox="121 0 40 40"
fill="none"
version="1.1"
id="svg3"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 126.52238,11.425398 5.80302,5.716401 5.88962,-5.889626 2.8582,2.858201 L 135.1836,20 l 5.7164,5.716402 -2.94482,2.8582 -5.7164,-5.629789 -5.88962,5.803014 -2.8582,-2.858201 5.88962,-5.803014 -5.803,-5.716402 z"
fill="#030749"
id="path1"
style="stroke-width:0.866122" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 137.86858,28.661214 2.94481,-2.944812 v 0 l 5.88963,-5.803014 -5.80302,-5.62979 v 0 l -2.8582,-2.8582 -5.7164,-5.7164023 2.94481,-2.9448129 5.7164,5.7164017 5.88963,-5.8030138 2.8582,2.8582008 -5.88962,5.8030145 5.7164,5.716401 5.88962,-5.803014 2.8582,2.858201 -5.88962,5.803014 5.803,5.716402 -2.9448,2.858201 -5.7164,-5.716402 -5.88963,5.803013 5.7164,5.716402 -2.8582,2.944813 -5.80301,-5.716402 -5.80302,5.803015 -2.8582,-2.858201 z"
fill="url(#paint0_linear_40_164)"
id="path2"
style="fill:url(#paint0_linear_40_164);stroke-width:0.866122" />
<defs
id="defs3">
<linearGradient
x1="135.948"
y1="-0.81632602"
x2="132.09599"
y2="36.985699"
gradientUnits="userSpaceOnUse"
id="paint0_linear_40_164"
gradientTransform="matrix(0.86612147,0,0,0.86612147,18.863485,2.6775707)">
<stop
stop-color="#01f1ff"
id="stop2" />
<stop
offset="1"
stop-color="#0197ff"
id="stop3" />
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -36,10 +36,9 @@ var localizedState = "";
var localizedDescription = "";
const processCommand = (function () {
const defaultIceServers = [
{ urls: ["stuns:stun.simplex.im:443"] },
{ urls: ["stun:stun.simplex.im:443"] },
//{ urls: ["turns:turn.simplex.im:443?transport=udp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj" },
{ urls: ["turns:turn.simplex.im:443?transport=tcp"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj" },
{ urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
{ urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
];
function getCallConfig(encodedInsertableStreams, iceServers, relay) {
return {
@@ -21,7 +21,6 @@ import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.TerminalView
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import java.awt.event.WindowEvent
@@ -104,7 +103,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
simplexWindowState.windowState = windowState
// Reload all strings in all @Composable's after language change at runtime
if (remember { ChatController.appPrefs.appLanguage.state }.value != "") {
Window(state = windowState, icon = painterResource(MR.images.ic_simplex), onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = {
Window(state = windowState, onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = {
if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) {
simplexWindowState.backstack.lastOrNull()?.invoke() != null
} else {
@@ -213,7 +213,7 @@ actual object SoundPlayer: SoundPlayerInterface {
override fun start(scope: CoroutineScope, sound: Boolean) {
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
tmpFile.deleteOnExit()
SoundPlayer::class.java.getResource("/media/ring_once.mp3")!!.openStream()!!.use { it.copyTo(tmpFile.outputStream()) }
SoundPlayer::class.java.getResource("/media/ring_once.mp3").openStream()!!.use { it.copyTo(tmpFile.outputStream()) }
playing = true
scope.launch {
while (playing && sound) {
@@ -228,37 +228,3 @@ actual object SoundPlayer: SoundPlayerInterface {
AudioPlayer.stop()
}
}
actual object CallSoundsPlayer: CallSoundsPlayerInterface {
private var playingJob: Job? = null
private fun start(soundPath: String, delay: Long, scope: CoroutineScope) {
playingJob?.cancel()
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
tmpFile.deleteOnExit()
SoundPlayer::class.java.getResource(soundPath)!!.openStream()!!.use { it.copyTo(tmpFile.outputStream()) }
playingJob = scope.launch {
while (isActive) {
AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), true)
delay(delay)
}
}
}
override fun startConnectingCallSound(scope: CoroutineScope) {
// Taken from https://github.com/TelegramOrg/Telegram-Android
// https://github.com/TelegramOrg/Telegram-Android/blob/master/LICENSE
start("/media/connecting_call.mp3", 3000, scope)
}
override fun startInCallSound(scope: CoroutineScope) {
start("/media/in_call.mp3", 5000, scope)
}
override fun vibrate(times: Int) {}
override fun stop() {
playingJob?.cancel()
AudioPlayer.stop()
}
}
@@ -1,15 +1,30 @@
package chat.simplex.common.views.call
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import org.nanohttpd.protocols.http.IHTTPSession
import org.nanohttpd.protocols.http.response.Response
@@ -30,7 +45,6 @@ actual fun ActiveCallView() {
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
}
BackHandler(onBack = endCall)
val scope = rememberCoroutineScope()
WebRTCController(chatModel.callCommand) { apiMsg ->
Log.d(TAG, "received from WebRTCController: $apiMsg")
val call = chatModel.activeCall.value
@@ -42,8 +56,6 @@ actual fun ActiveCallView() {
val callType = CallType(call.localMedia, r.capabilities)
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
CallSoundsPlayer.startConnectingCallSound(scope)
activeCallWaitDeliveryReceipt(scope)
}
is WCallResponse.Offer -> withBGApi {
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
@@ -52,7 +64,6 @@ actual fun ActiveCallView() {
is WCallResponse.Answer -> withBGApi {
chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates)
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
CallSoundsPlayer.stop()
}
is WCallResponse.Ice -> withBGApi {
chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates)
@@ -110,7 +121,6 @@ actual fun ActiveCallView() {
// After the first call, End command gets added to the list which prevents making another calls
chatModel.callCommand.removeAll { it is WCallCommand.End }
onDispose {
CallSoundsPlayer.stop()
chatModel.activeCallViewIsVisible.value = false
chatModel.callCommand.clear()
}
@@ -7,8 +7,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import chat.simplex.common.platform.*
import chat.simplex.common.simplexWindowState
import java.awt.Window
@Composable
actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {
@@ -25,15 +23,14 @@ actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLon
}
}
/*
* This function doesn't take into account multi-window environment. In case more windows will be used, modify the code
* */
@Composable
actual fun LocalWindowWidth(): Dp = with(LocalDensity.current) {
val windows = java.awt.Window.getWindows()
if (windows.size == 1) {
(windows.getOrNull(0)?.width ?: 0).toDp()
} else {
simplexWindowState.windowState.size.width
actual fun LocalWindowWidth(): Dp {
return with(LocalDensity.current) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }
/*val density = LocalDensity.current
var width by remember { mutableStateOf(with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }) }
SideEffect {
if (width != with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() })
width = with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }
}
return width.also { println("LALAL $it") }*/
}
+5 -2
View File
@@ -12,7 +12,10 @@ version = extra["desktop.version_name"] as String
kotlin {
jvm()
jvm {
jvmToolchain(11)
withJava()
}
sourceSets {
val jvmMain by getting {
dependencies {
@@ -148,7 +151,7 @@ cmake {
tasks.named("clean") {
dependsOn("cmakeClean")
}
tasks.named("compileKotlinJvm") {
tasks.named("compileJava") {
dependsOn("cmakeBuildAndCopy")
}
afterEvaluate {
+4 -5
View File
@@ -24,13 +24,12 @@ android.nonTransitiveRClass=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=5.7-beta.0
android.version_code=196
android.version_name=5.6
android.version_code=191
desktop.version_name=5.7-beta.0
desktop.version_code=37
desktop.version_name=5.6
desktop.version_code=35
kotlin.version=1.9.23
gradle.plugin.version=8.2.0
+57 -4
View File
@@ -1,8 +1,61 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
module Main where
import Server (simplexChatServer)
import Simplex.Chat.Terminal (terminalChatConfig)
import Simplex.Chat.Terminal.Main (simplexChatCLI)
import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM
import Control.Monad
import Data.Time.Clock (getCurrentTime)
import Data.Time.LocalTime (getCurrentTimeZone)
import Server
import Simplex.Chat.Controller (ChatController (..), ChatResponse (..), currentRemoteHost, versionNumber, versionString)
import Simplex.Chat.Core
import Simplex.Chat.Options
import Simplex.Chat.Terminal
import Simplex.Chat.View (serializeChatResponse)
import Simplex.Messaging.Client (NetworkConfig (..))
import System.Directory (getAppUserDataDirectory)
import System.Terminal (withTerminal)
main :: IO ()
main = simplexChatCLI terminalChatConfig (Just simplexChatServer)
main = do
appDir <- getAppUserDataDirectory "simplex"
opts@ChatOpts {chatCmd, chatServerPort} <- getChatOpts appDir "simplex_v1"
if null chatCmd
then case chatServerPort of
Just chatPort -> simplexChatServer defaultChatServerConfig {chatPort} terminalChatConfig opts
_ -> runCLI opts
else simplexChatCore terminalChatConfig opts $ runCommand opts
where
runCLI opts = do
welcome opts
t <- withTerminal pure
simplexChatTerminal terminalChatConfig opts t
runCommand ChatOpts {chatCmd, chatCmdLog, chatCmdDelay} user cc = do
when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do
(_, _, r') <- atomically . readTBQueue $ outputQ cc
case r' of
CRNewChatItem {} -> printResponse r'
_ -> when (chatCmdLog == CCLAll) $ printResponse r'
sendChatCmdStr cc chatCmd >>= printResponse
threadDelay $ chatCmdDelay * 1000000
where
printResponse r = do
ts <- getCurrentTime
tz <- getCurrentTimeZone
rh <- readTVarIO $ currentRemoteHost cc
putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r
welcome :: ChatOpts -> IO ()
welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} =
mapM_
putStrLn
[ versionString versionNumber,
"db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db",
maybe
"direct network connection - use `/network` command or `-x` CLI option to connect via SOCKS5 at :9050"
(("using SOCKS5 proxy " <>) . show)
(socksProxy networkConfig),
"type \"/help\" or \"/h\" for usage info"
]
+3 -3
View File
@@ -28,9 +28,9 @@ import Simplex.Messaging.Util (raceAny_)
import UnliftIO.Exception
import UnliftIO.STM
simplexChatServer :: ServiceName -> ChatConfig -> ChatOpts -> IO ()
simplexChatServer chatPort cfg opts =
simplexChatCore cfg opts . const $ runChatServer defaultChatServerConfig {chatPort}
simplexChatServer :: ChatServerConfig -> ChatConfig -> ChatOpts -> IO ()
simplexChatServer srvCfg cfg opts =
simplexChatCore cfg opts . const $ runChatServer srvCfg
data ChatServerConfig = ChatServerConfig
{ chatPort :: ServiceName,
@@ -31,7 +31,6 @@ import Simplex.Chat.Messages
import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Protocol (MsgContent (..))
import Simplex.Chat.Types
import Simplex.Chat.Types.Shared
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util ((<$?>))
import Data.Char (isSpace)
@@ -71,7 +70,7 @@ crDirectoryEvent = \case
CRChatItemDeleted {deletedChatItem = AChatItem _ SMDRcv (DirectChat ct) _, byUser = False} -> Just $ DEItemDeleteIgnored ct
CRNewChatItem {chatItem = AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}} ->
Just $ case (mc, itemLive) of
(MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.dropWhileEnd isSpace t
(MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly directoryCmdP $ T.dropWhileEnd isSpace t
_ -> DEUnsupportedMessage ct ciId
where
ciId = chatItemId' ci
@@ -36,7 +36,6 @@ import Simplex.Chat.Messages
import Simplex.Chat.Options
import Simplex.Chat.Protocol (MsgContent (..))
import Simplex.Chat.Types
import Simplex.Chat.Types.Shared
import Simplex.Chat.View (serializeChatResponse, simplexChatContact)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.TMap (TMap)
@@ -1,38 +0,0 @@
---
layout: layouts/article.html
title: "Why I joined SimpleX Chat - by Esra'a al Shafei"
date: 2024-04-04
previewBody: blog_previews/20240404.html
image: images/20240404-esraa.png
permalink: "/blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.html"
---
# Why I joined SimpleX Chat
**Published:** Apr 4, 2024
_By [Esra'a al Shafei](https://mastodon.social/@alshafei)_
Transitioning from a lifelong career dedicated to nonprofits, including Board roles at organizations like the Wikimedia Foundation, Access Now and Tor, my decision to join SimpleX Chat may come as a surprise to some. But, as I step into this new chapter, I want to share the insights and convictions that have guided me here, shedding light on what I think sets SimpleX Chat apart and why this move feels like an essential learning opportunity.
The nonprofit world has been my primary focus for decades. My team and I ran the platforms at Majal.org with an extremely limited budget. We had to navigate many complexities and challenges that shadow the nonprofit model. And because we worked primarily in creating applications and tools, a recurring theme has been financial sustainability. Being a Bahrain-based entity for most of these years meant that the many communities we served were not in a position to provide contributions and we were not eligible for most foundation grants. This drastically limited our growth and the reliability of our apps. When we failed to raise sufficient funds or meet our target budgets, we often had to shutter certain applications, sometimes after spending more than 10 years building them.
With secure and private messaging, the stakes are even graver. Any failure to commit and resource/fund ongoing development, security patches, etc means lives can be at risk. I still believe in nonprofit models, and its why I continue to serve them through various volunteer roles. I do also believe that there is room for a mixture of models that, in the case of something as unique as SimpleX Chat, can serve as a fully open and transparent public interest technology while also having a profitable values-aligned company that can keep the lights on to continue developing, expanding, and improving the protocol, network and their reach.
Im no stranger to writing about some VC models being [corrupt](https://mastodon.social/@alshafei/112125959080515656). Frankly, I also hold the view that some tech VCs are amongst the [most complicit](https://responsiblestatecraft.org/defense-tech/) in egregious war crimes worldwide, or enabling the [intrusive surveillance](https://mastodon.social/@alshafei/112140566088322925) were fighting against. So being part of a VC-funded venture is not a decision I take lightly. However, I have been following SimpleX Chats growth since early 2022 when I first met Evgeny at the Mozilla Festival. I appreciated the drive and Evgenys firm refusal to settle for the current models of private messaging. We share the belief that messaging is something we need to keep improving and that we must continue pushing its boundaries to make it even more private, secure, usable for groups, and, most importantly - fully decentralized. This is a major undertaking, and it requires funding to achieve. Candidly, I did worry about funding and sustainability because, at the time, SimpleX was still primarily funded by user contributions.
But even knowing this, I scrutinized SimpleX Chat for taking VC funding ($350K) from Village Global and questioned the individuals featured on its frontpage. I had to speak with Evgeny directly to learn who exactly from this fund was involved, how much power they wielded, if any, and if this changes the ethos of the company - all of which he is already making public. It was only after these discussions that I was comfortable to take a leap of faith and continue to use the app and vouch for its current and future offerings. It required me to question my own views on whether a VC-funded company can actually have major positive contributions to privacy as well as the open ecosystem.
<img src="./images/20240404-messsaging-apps.png" class="float-to-right" width="50%">
The web has a long history of [trading privacy](https://www.engadget.com/from-its-start-gmail-conditioned-us-to-trade-privacy-for-free-services-120009741.html) for “free” services. Traditionally, these services have also been centralized, closed-source, non-transparent, and profit-oriented. The companies behind these apps and services became prolific because of their disregard of privacy rights, which normalized lucrative surveillance capitalism. There is such an extensive global monopoly that in Africa, only 1 of the 5 biggest messaging apps in Africa isn't owned by Meta, notoriously known for spying not just through its own apps but even through [its competitors](https://qz.com/project-ghostbusters-facebook-meta-wiretap-snapchat-1851366814), relentless, massive data harvesting that stretches far beyond its own walled gardens:
Some of the worlds top engineers often go to these companies because of the benefits and financial opportunities. We can question their ethics all day long, but we also need to question if the web would look significantly different if there were as many opportunities at privacy-first companies with purpose and strong, proven moral boundaries, set up in a way that can guarantee operational independence from any shareholders and VCs.
SimpleX could have taken the route of other companies in the privacy space, whether its Skiff which rushed to take a large amount of [VC money](https://techcrunch.com/2022/03/30/skiff-series-a-encrypted-workspaces/) only to [shutter its doors](https://www.techradar.com/computing/cyber-security/skiff-gets-bought-by-notion-raising-privacy-concerns) after an acquisition, leaving its users hanging with many unanswered questions, or giving up control of the company, which would puts its future solely in the hands of VCs with majority ownership. SimpleX aims to prevent this, and in fact has left money on the table to ensure that it does not occur. Had it not been for this information, I would not have joined, and I would have remained a user of the product, albeit a very cautious one, constantly wondering whether it will be sold or corrupted.
Its worth noting that some private foundations operate on the VC model in supporting nonprofits, either by requiring Board seats or requesting that their funding be used towards very specific objectives not always in alignment with the organizations values and mission. Its also worth noting that [some nonprofits](https://www.engadget.com/2019-05-31-sex-lies-and-surveillance-fosta-privacy.html) actually operate on the models of surveillance and censorship. Therefore, whether an organization or company is VC-backed or a nonprofit should not be the sole factor in deciding whether or not it is trustworthy. Actions are important, with full transparency being one of the most critical factors, and being fully open source being another to attract valid criticisms and audits to ensure any product or protocol lives up to its privacy and security promise. SimpleX Chat prides itself on being both transparent and open, on top of also being fully decentralized. If youre new to it and eager to know more, you can start with [this overview](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md).
Another important consideration is that the SimpleX network does have a plan that would rely on users' payments for specific or tailored services, and not on some other sources of revenue or funds (ads, etc.). Building anything that users would be willing to pay for requires substantially more time and resources, hence the VC route to establish a business model that doesnt translate to the user being the product. But any business services need to be separate from SimpleX as a public interest technology. As outlined in this [recent post](./20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md), Ill be using my background in nonprofit governance structures to ensure that the SimpleX network protocols evolve under the stewardship of nonprofit entities in various jurisdictions, so that its continued evolution aligns more closely with the vision of community-driven, independent and decentralized governance. This would help create a necessary balance between different structures, in the same way many tech nonprofits also have for-profit subsidiaries to attract fee-for-service agreements to sustain their operations.
In summary: My decision to join Simplex Chat, despite my deep-rooted beliefs and skepticism towards VC funding, reflects a broader realization: that the fight for privacy, security, and decentralization in todays web is multifaceted and sometimes requires us to depart from our comfort zones to explore sustainable paths for continuous growth and impact so that open source privacy tools and protocols are no longer “niche”, but universally accessible standards. As long as nothing in this journey compromises our moral principles and integrity, this will remain a very worthwhile goal to pursue.
-8
View File
@@ -1,13 +1,5 @@
# Blog
Apr 4. 2024 [Why I joined SimpleX Chat](./20240404-why-i-joined-simplex-chat-esraa-al-shafei.md)
_By [Esra'a al Shafei](https://mastodon.social/@alshafei)_
Transitioning from a lifelong career dedicated to nonprofits, including Board roles at organizations like the Wikimedia Foundation, Access Now and Tor, my decision to join SimpleX Chat may come as a surprise to some. But, as I step into this new chapter, I want to share the insights and convictions that have guided me here, shedding light on what I think sets SimpleX Chat apart and why this move feels like an essential learning opportunity.
---
Mar 23, 2024 [SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration](./20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md)
SimpleX network: deliver real privacy via a profitable business and non-profit protocol governance:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

+7 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 5e783396e05ced83b22914c172d2f2e8e14e5be2
tag: ee90ea6a69fe8283d37d9821cd83798fd0a76260
source-repository-package
type: git
@@ -34,6 +34,12 @@ source-repository-package
location: https://github.com/simplex-chat/aeson.git
tag: aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b
-- old bs/text compat for 8.10
source-repository-package
type: git
location: https://github.com/simplex-chat/base64.git
tag: 2d77b6dbcaffc00570a70be8694049f3710e7c94
source-repository-package
type: git
location: https://github.com/simplex-chat/haskell-terminal.git
+1 -1
View File
@@ -3,7 +3,7 @@ title: Accessing files in Android app
revision: 07.02.2023
---
| 07.02.2023 | EN, [CZ](/docs/lang/cs/ANDROID.md), [FR](/docs/lang/fr/ANDROID.md), [PL](/docs/lang/pl/ANDROID.md) |
| 07.02.2023 | EN, [CZ](/docs/lang/cs/ANDROID.md), [FR](/docs/lang/fr/ANDROID.md) |
# Accessing files in Android app
+2 -2
View File
@@ -3,7 +3,7 @@ title: Terminal CLI
revision: 31.01.2023
---
| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CLI.md), [CZ](/docs/lang/cs/CLI.md), [PL](/docs/lang/pl/CLI.md) |
| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CLI.md), [CZ](/docs/lang/cs/CLI.md) |
# SimpleX Chat terminal (console) app for Linux/MacOS/Windows
@@ -102,7 +102,7 @@ DOCKER_BUILDKIT=1 docker build --output ~/.local/bin .
#### In any OS
1. Install [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 9.6.4 and cabal 3.10.1.0:
1. Install [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 9.6.3 and cabal 3.10.1.0:
```shell
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
+11 -11
View File
@@ -3,7 +3,7 @@ title: Contributing guide
revision: 31.01.2023
---
| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md), [PL](/docs/lang/pl/CONTRIBUTING.md) |
| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/CONTRIBUTING.md), [CZ](/docs/lang/cs/CONTRIBUTING.md) |
# Contributing guide
@@ -30,13 +30,13 @@ You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order t
**In simplex-chat repo**
- `stable` - stable release of the apps, can be used for updates to the previous stable release (GHC 9.6.4).
- `stable` - stable release of the apps, can be used for updates to the previous stable release (GHC 9.6.3).
- `stable-android` - used to build stable Android core library with Nix (GHC 8.10.7) - only for Android armv7a.
- `stable-ios` - used to build stable iOS core library with Nix (GHC 8.10.7) this branch should be the same as `stable-android` except Nix configuration files. Deprecated.
- `master` - branch for beta version releases (GHC 9.6.4).
- `master` - branch for beta version releases (GHC 9.6.3).
- `master-ghc8107` - branch for beta version releases (GHC 8.10.7). Deprecated.
@@ -50,7 +50,7 @@ You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order t
**In simplexmq repo**
- `master` - uses GHC 9.6.4 its commit should be used in `master` branch of simplex-chat repo.
- `master` - uses GHC 9.6.3 its commit should be used in `master` branch of simplex-chat repo.
- `master-ghc8107` - its commit should be used in `master-android` (and `master-ios`) branch of simplex-chat repo. Deprecated.
@@ -77,28 +77,28 @@ You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order t
7. 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.4
## Differences between GHC 8.10.7 and GHC 9.6.3
1. The main difference is related to `DuplicateRecordFields` extension.
It is no longer possible in GHC 9.6.4 to specify type when using selectors, instead OverloadedRecordDot extension and syntax are used that need to be removed in GHC 8.10.7:
It is no longer possible in GHC 9.6.3 to specify type when using selectors, instead OverloadedRecordDot extension and syntax are used that need to be removed in GHC 8.10.7:
```haskell
{-# LANGUAGE DuplicateRecordFields #-}
-- use this in GHC 9.6.4 when needed
-- use this in GHC 9.6.3 when needed
{-# LANGUAGE OverloadedRecordDot #-}
-- GHC 9.6.4 syntax
-- GHC 9.6.3 syntax
let x = record.field
-- GHC 8.10.7 syntax absent in GHC 9.6.4
-- GHC 8.10.7 syntax removed in GHC 9.6.3
let x = field (record :: Record)
```
It is still possible to specify type when using record update syntax, use this pragma to suppress compiler warning:
```haskell
-- use this in GHC 9.6.4 when needed
-- use this in GHC 9.6.3 when needed
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
let r' = (record :: Record) {field = value}
@@ -107,7 +107,7 @@ let r' = (record :: Record) {field = value}
2. Most monad functions now have to be imported from `Control.Monad`, and not from specific monad modules (e.g. `Control.Monad.Except`).
```haskell
-- use this in GHC 9.6.4 when needed
-- use this in GHC 9.6.3 when needed
import Control.Monad
```
+1 -1
View File
@@ -19,7 +19,7 @@ You can get the latest beta releases from [GitHub](https://github.com/simplex-ch
<img src="/docs/images/simplex-desktop-light.png" alt="desktop app" width=500>
You can link your mobile device with desktop to use the same profile remotely, but this is only possible when both devices are connected to the same local network.
Using the same profile as on mobile device is not yet supported you need to create a separate profile to use desktop apps.
**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-22_04-x86_64.deb).
+1 -1
View File
@@ -3,7 +3,7 @@ title: Hosting your own SMP Server
revision: 31.07.2023
---
| Updated 05.06.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md), [PL](/docs/lang/pl/SERVER.md) |
| Updated 05.06.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) |
# Hosting your own SMP Server
+1 -1
View File
@@ -3,7 +3,7 @@ title: SimpleX platform
revision: 07.02.2023
---
| Updated 07.02.2023 | Languages: EN, [FR](/docs/lang/fr/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md), [PL](/docs/lang/pl/SIMPLEX.md) |
| Updated 07.02.2023 | Languages: EN, [FR](/docs/lang/fr/SIMPLEX.md), [CZ](/docs/lang/cs/SIMPLEX.md) |
# SimpleX platform - motivation and comparison
## Problems
+1 -1
View File
@@ -3,7 +3,7 @@ title: Contributing translations to SimpleX Chat
revision: 19.03.2023
---
| 19.03.2023 | EN, [CZ](/docs/lang/cs/TRANSLATIONS.md), [FR](/docs/lang/fr/TRANSLATIONS.md), [PL](/docs/lang/pl/TRANSLATIONS.md) |
| 19.03.2023 | EN, [CZ](/docs/lang/cs/TRANSLATIONS.md), [FR](/docs/lang/fr/TRANSLATIONS.md) |
# Contributing translations to SimpleX Chat
-29
View File
@@ -1,29 +0,0 @@
---
title: Transparency Reports
permalink: /transparency/index.html
revision: 09.04.2024
---
# Transparency Reports
**Updated**: Apr 9, 2024
SimpleX Chat Ltd. is a company registered in the UK it develops communication software enabling users to operate and communicate via SimpleX network, without user profile identifiers of any kind, and without having their data hosted by any network infrastructure operators.
This page will include any and all reports on requests for user data.
*To date, we received none*.
Our objective is to consistently ensure that no user data and absolute minimum of the metadata required for the network to function is available for disclosure by any infrastructure operators, under any circumstances.
**Helpful resources**:
- [Privacy policy](https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md)
- [Privacy and security: technical details and limitations](https://github.com/simplex-chat/simplex-chat?tab=readme-ov-file#privacy-and-security-technical-details-and-limitations)
- Whitepaper:
- [Trust in servers](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#trust-in-servers)
- [Encryption Primitives Used](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#encryption-primitives-used)
- [Threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model)
Have a more specific question? Reach out to us via [SimpleX Chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion) or via email [chat@simplex.chat](mailto:chat@simplex.chat).
For any sensitive questions please use SimpleX Chat or encrypted email messages using the key for this address from [keys.openpgp.org](https://keys.openpgp.org/search?q=chat%40simplex.chat) (its fingerprint is `FB44 AF81 A45B DE32 7319 797C 8510 7E35 7D4A 17FC`) and make your key available for a secure reply.
+1 -1
View File
@@ -3,7 +3,7 @@ title: Using custom WebRTC ICE servers in SimpleX Chat
revision: 31.01.2023
---
| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md), [PL](/docs/lang/pl/WEBRTC.md) |
| Updated 31.01.2023 | Languages: EN, [FR](/docs/lang/fr/WEBRTC.md), [CZ](/docs/lang/cs/WEBRTC.md) |
# Using custom WebRTC ICE servers in SimpleX Chat

Some files were not shown because too many files have changed in this diff Show More