Compare commits

..

1 Commits

Author SHA1 Message Date
Avently
fa9f5b3250 android, desktop: move function to different thread 2024-01-22 20:26:14 +07:00
177 changed files with 1400 additions and 4273 deletions

View File

@@ -1,41 +1,32 @@
ARG TAG=22.04 FROM ubuntu:focal AS build
FROM ubuntu:${TAG} AS build # Install curl and simplex-chat-related dependencies
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev libssl-dev
### Build stage
# Install curl and git and simplex-chat dependencies
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.3
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.1.0
# Install ghcup # Install ghcup
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/bin/ghcup && \
chmod +x /usr/bin/ghcup
# Adjust PATH
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
# Install ghc
RUN ghcup install ghc 9.6.3
# Install cabal
RUN ghcup install cabal 3.10.1.0
# Set both as default # Set both as default
RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \ RUN ghcup set ghc 9.6.3 && \
ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}" ghcup set cabal 3.10.1.0
COPY . /project COPY . /project
WORKDIR /project WORKDIR /project
# Adjust PATH
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
# Adjust build # Adjust build
RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local
# Compile simplex-chat # Compile simplex-chat
RUN cabal update RUN cabal update
RUN cabal build exe:simplex-chat RUN cabal install
# Strip the binary from debug symbols to reduce size
RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \
mv "$bin" ./ && \
strip ./simplex-chat
# Copy compiled app from build stage
FROM scratch AS export-stage FROM scratch AS export-stage
COPY --from=build /project/simplex-chat / COPY --from=build /root/.cabal/bin/simplex-chat /

View File

@@ -234,8 +234,6 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent and important updates: Recent and important updates:
[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md)
[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md). [Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md).
[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md). [Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md).
@@ -301,7 +299,7 @@ What is already implemented:
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections. 11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
12. Manual messaging queue rotations to move conversation to another SMP relay. 12. Manual messaging queue rotations to move conversation to another SMP relay.
13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html). 13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html).
14. Local files encryption. 14. Local files encryption, except videos (to be added later).
We plan to add: We plan to add:
@@ -373,13 +371,12 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A
- ✅ Desktop client. - ✅ Desktop client.
- ✅ Encryption of local files stored in the app. - ✅ Encryption of local files stored in the app.
- ✅ Using mobile profiles from the desktop app. - ✅ Using mobile profiles from the desktop app.
- ✅ Private notes.
- ✅ Improve sending videos (including encryption of locally stored videos).
- 🏗 Improve experience for the new users. - 🏗 Improve experience for the new users.
- 🏗 Post-quantum resistant key exchange in double ratchet protocol. - 🏗 Post-quantum resistant key exchange in double ratchet protocol.
- 🏗 Large groups, communities and public channels. - 🏗 Large groups, communities and public channels.
- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic). - Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- Privacy & security slider - a simple way to set all settings at once. - Privacy & security slider - a simple way to set all settings at once.
- Improve sending videos (including encryption of locally stored videos).
- SMP queue redundancy and rotation (manual is supported). - SMP queue redundancy and rotation (manual is supported).
- Include optional message into connection request sent via contact address. - Include optional message into connection request sent via contact address.
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.). - Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).

View File

@@ -34,8 +34,6 @@ struct ContentView: View {
@State private var waitingForOrPassedAuth = true @State private var waitingForOrPassedAuth = true
@State private var chatListActionSheet: ChatListActionSheet? = nil @State private var chatListActionSheet: ChatListActionSheet? = nil
private let callTopPadding: CGFloat = 50
private enum ChatListActionSheet: Identifiable { private enum ChatListActionSheet: Identifiable {
case planAndConnectSheet(sheet: PlanAndConnectActionSheet) case planAndConnectSheet(sheet: PlanAndConnectActionSheet)
@@ -52,28 +50,16 @@ struct ContentView: View {
var body: some View { var body: some View {
ZStack { ZStack {
let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted
// contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings. // contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings.
// i.e. with separate branches like this settings are closed: `if prefPerformLA { ... contentView() ... } else { contentView() } // i.e. with separate branches like this settings are closed: `if prefPerformLA { ... contentView() ... } else { contentView() }
if !prefPerformLA || accessAuthenticated { if !prefPerformLA || accessAuthenticated {
contentView() contentView()
.padding(.top, showCallArea ? callTopPadding : 0)
} else { } else {
lockButton() lockButton()
.padding(.top, showCallArea ? callTopPadding : 0)
} }
if showCallArea, let call = chatModel.activeCall {
VStack {
activeCallInteractiveArea(call)
Spacer()
}
}
if chatModel.showCallView, let call = chatModel.activeCall { if chatModel.showCallView, let call = chatModel.activeCall {
callView(call) callView(call)
} }
if !showSettings, let la = chatModel.laRequest { if !showSettings, let la = chatModel.laRequest {
LocalAuthView(authRequest: la) LocalAuthView(authRequest: la)
.onDisappear { .onDisappear {
@@ -149,11 +135,11 @@ struct ContentView: View {
if case .onboardingComplete = step, if case .onboardingComplete = step,
chatModel.currentUser != nil { chatModel.currentUser != nil {
mainView() mainView()
.actionSheet(item: $chatListActionSheet) { sheet in .actionSheet(item: $chatListActionSheet) { sheet in
switch sheet { switch sheet {
case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false) case let .planAndConnectSheet(sheet): return planAndConnectActionSheet(sheet, dismiss: false)
}
} }
}
} else { } else {
OnboardingView(onboarding: step) OnboardingView(onboarding: step)
} }
@@ -177,40 +163,6 @@ struct ContentView: View {
} }
} }
@ViewBuilder private func activeCallInteractiveArea(_ call: Call) -> some View {
HStack {
Text(call.contact.displayName).font(.body).foregroundColor(.white)
Spacer()
CallDuration(call: call)
}
.padding(.horizontal)
.frame(height: callTopPadding - 10)
.background(Color(uiColor: UIColor(red: 47/255, green: 208/255, blue: 88/255, alpha: 1)))
.onTapGesture {
chatModel.activeCallViewIsCollapsed = false
}
}
struct CallDuration: View {
let call: Call
@State var text: String = ""
@State var timer: Timer? = nil
var body: some View {
Text(text).frame(minWidth: text.count <= 5 ? 52 : 77, alignment: .leading).offset(x: 4).font(.body).foregroundColor(.white)
.onAppear {
timer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { timer in
if let connectedAt = call.connectedAt {
text = durationText(Int(Date.now.timeIntervalSince1970 - connectedAt.timeIntervalSince1970))
}
}
}
.onDisappear {
_ = timer?.invalidate()
}
}
}
private func lockButton() -> some View { private func lockButton() -> some View {
Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") } Button(action: authenticateContentViewAccess) { Label("Unlock", systemImage: "lock") }
} }

View File

@@ -80,7 +80,6 @@ final class ChatModel: ObservableObject {
@Published var tokenRegistered = false @Published var tokenRegistered = false
@Published var tokenStatus: NtfTknStatus? @Published var tokenStatus: NtfTknStatus?
@Published var notificationMode = NotificationsMode.off @Published var notificationMode = NotificationsMode.off
@Published var notificationServer: String?
@Published var notificationPreview: NotificationPreviewMode = ntfPreviewModeGroupDefault.get() @Published var notificationPreview: NotificationPreviewMode = ntfPreviewModeGroupDefault.get()
// pending notification actions // pending notification actions
@Published var ntfContactRequest: NTFContactRequest? @Published var ntfContactRequest: NTFContactRequest?
@@ -90,7 +89,6 @@ final class ChatModel: ObservableObject {
@Published var activeCall: Call? @Published var activeCall: Call?
let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor() let callCommand: WebRTCCommandProcessor = WebRTCCommandProcessor()
@Published var showCallView = false @Published var showCallView = false
@Published var activeCallViewIsCollapsed = false
// remote desktop // remote desktop
@Published var remoteCtrlSession: RemoteCtrlSession? @Published var remoteCtrlSession: RemoteCtrlSession?
// currently showing invitation // currently showing invitation

View File

@@ -412,14 +412,14 @@ func apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64
throw r throw r
} }
func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String?) { func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
let r = chatSendCmdSync(.apiGetNtfToken) let r = chatSendCmdSync(.apiGetNtfToken)
switch r { switch r {
case let .ntfToken(token, status, ntfMode, ntfServer): return (token, status, ntfMode, ntfServer) case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off, nil) case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
default: default:
logger.debug("apiGetNtfToken response: \(String(describing: r))") logger.debug("apiGetNtfToken response: \(String(describing: r))")
return (nil, nil, .off, nil) return (nil, nil, .off)
} }
} }
@@ -1172,7 +1172,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] {
let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil } let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil }
return ChatModel.shared.chats return ChatModel.shared.chats
.compactMap{ $0.chatInfo.contact } .compactMap{ $0.chatInfo.contact }
.filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) } .filter{ !memberContactIds.contains($0.apiId) }
.sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() }
} }
@@ -1309,7 +1309,7 @@ func startChat(refreshInvitations: Bool = true) throws {
if (refreshInvitations) { if (refreshInvitations) {
try refreshCallInvitations() try refreshCallInvitations()
} }
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken() (m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
// deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called, // deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called,
// when it is called before startChat // when it is called before startChat
if let token = m.deviceToken { if let token = m.deviceToken {
@@ -1861,9 +1861,7 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async {
let cItem = aChatItem.chatItem let cItem = aChatItem.chatItem
if active(user) { if active(user) {
if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) { if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) {
if cItem.showNotification { NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
} }
} }
} }

View File

@@ -12,67 +12,49 @@ import SimpleXChat
struct ActiveCallView: View { struct ActiveCallView: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@ObservedObject var call: Call @ObservedObject var call: Call
@Environment(\.scenePhase) var scenePhase @Environment(\.scenePhase) var scenePhase
@State private var client: WebRTCClient? = nil @State private var client: WebRTCClient? = nil
@State private var activeCall: WebRTCClient.Call? = nil @State private var activeCall: WebRTCClient.Call? = nil
@State private var localRendererAspectRatio: CGFloat? = nil @State private var localRendererAspectRatio: CGFloat? = nil
@Binding var canConnectCall: Bool @Binding var canConnectCall: Bool
@State var prevColorScheme: ColorScheme = .dark
@State var pipShown = false
var body: some View { var body: some View {
ZStack(alignment: .topLeading) { ZStack(alignment: .bottom) {
ZStack(alignment: .bottom) { if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil {
if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil { GeometryReader { g in
GeometryReader { g in let width = g.size.width * 0.3
let width = g.size.width * 0.3 ZStack(alignment: .topTrailing) {
ZStack(alignment: .topTrailing) { CallViewRemote(client: client, activeCall: $activeCall)
CallViewRemote(client: client, activeCall: $activeCall, activeCallViewIsCollapsed: $m.activeCallViewIsCollapsed, pipShown: $pipShown) CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio)
CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio, pipShown: $pipShown) .cornerRadius(10)
.cornerRadius(10) .frame(width: width, height: width / (localRendererAspectRatio ?? 1))
.frame(width: width, height: width / (localRendererAspectRatio ?? 1)) .padding([.top, .trailing], 17)
.padding([.top, .trailing], 17)
ZStack(alignment: .center) {
// For some reason, when the view in GeometryReader and ZStack is visible, it steals clicks on a back button, so showing something on top like this with background color helps (.clear color doesn't work)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.primary.opacity(0.000001))
}
} }
} }
if let call = m.activeCall, let client = client, (!pipShown || !call.supportsVideo) { }
ActiveCallOverlay(call: call, client: client) if let call = m.activeCall, let client = client {
} ActiveCallOverlay(call: call, client: client)
} }
} }
.allowsHitTesting(!m.activeCallViewIsCollapsed)
.opacity(m.activeCallViewIsCollapsed ? 0 : 1)
.onAppear { .onAppear {
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)") logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase)), canConnectCall \(canConnectCall)")
AppDelegate.keepScreenOn(true) AppDelegate.keepScreenOn(true)
createWebRTCClient() createWebRTCClient()
dismissAllSheets() dismissAllSheets()
hideKeyboard()
prevColorScheme = colorScheme
} }
.onChange(of: canConnectCall) { _ in .onChange(of: canConnectCall) { _ in
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)") logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall)")
createWebRTCClient() createWebRTCClient()
} }
.onChange(of: m.activeCallViewIsCollapsed) { _ in
hideKeyboard()
}
.onDisappear { .onDisappear {
logger.debug("ActiveCallView: disappear") logger.debug("ActiveCallView: disappear")
Task { await m.callCommand.setClient(nil) } Task { await m.callCommand.setClient(nil) }
AppDelegate.keepScreenOn(false) AppDelegate.keepScreenOn(false)
client?.endCall() client?.endCall()
} }
.background(m.activeCallViewIsCollapsed ? .clear : .black) .background(.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 .preferredColorScheme(.dark)
.preferredColorScheme(m.activeCallViewIsCollapsed || CallController.useCallKit() ? prevColorScheme : .dark)
} }
private func createWebRTCClient() { private func createWebRTCClient() {
@@ -87,8 +69,8 @@ struct ActiveCallView: View {
@MainActor @MainActor
private func processRtcMessage(msg: WVAPIMessage) { private func processRtcMessage(msg: WVAPIMessage) {
if call == m.activeCall, if call == m.activeCall,
let call = m.activeCall, let call = m.activeCall,
let client = client { let client = client {
logger.debug("ActiveCallView: response \(msg.resp.respType)") logger.debug("ActiveCallView: response \(msg.resp.respType)")
switch msg.resp { switch msg.resp {
case let .capabilities(capabilities): case let .capabilities(capabilities):
@@ -108,7 +90,7 @@ struct ActiveCallView: View {
Task { Task {
do { do {
try await apiSendCallOffer(call.contact, offer, iceCandidates, try await apiSendCallOffer(call.contact, offer, iceCandidates,
media: call.localMedia, capabilities: capabilities) media: call.localMedia, capabilities: capabilities)
} catch { } catch {
logger.error("apiSendCallOffer \(responseError(error))") logger.error("apiSendCallOffer \(responseError(error))")
} }
@@ -140,15 +122,13 @@ struct ActiveCallView: View {
if let callStatus = WebRTCCallStatus.init(rawValue: state.connectionState), if let callStatus = WebRTCCallStatus.init(rawValue: state.connectionState),
case .connected = callStatus { case .connected = callStatus {
call.direction == .outgoing call.direction == .outgoing
? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil) ? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil) : CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
call.callState = .connected call.callState = .connected
call.connectedAt = .now
} }
if state.connectionState == "closed" { if state.connectionState == "closed" {
closeCallView(client) closeCallView(client)
m.activeCall = nil m.activeCall = nil
m.activeCallViewIsCollapsed = false
} }
Task { Task {
do { do {
@@ -160,7 +140,6 @@ struct ActiveCallView: View {
case let .connected(connectionInfo): case let .connected(connectionInfo):
call.callState = .connected call.callState = .connected
call.connectionInfo = connectionInfo call.connectionInfo = connectionInfo
call.connectedAt = .now
case .ended: case .ended:
closeCallView(client) closeCallView(client)
call.callState = .ended call.callState = .ended
@@ -174,7 +153,6 @@ struct ActiveCallView: View {
case .end: case .end:
closeCallView(client) closeCallView(client)
m.activeCall = nil m.activeCall = nil
m.activeCallViewIsCollapsed = false
default: () default: ()
} }
case let .error(message): case let .error(message):
@@ -203,7 +181,7 @@ struct ActiveCallOverlay: View {
VStack { VStack {
switch call.localMedia { switch call.localMedia {
case .video: case .video:
videoCallInfoView(call) callInfoView(call, .leading)
.foregroundColor(.white) .foregroundColor(.white)
.opacity(0.8) .opacity(0.8)
.padding() .padding()
@@ -230,25 +208,16 @@ struct ActiveCallOverlay: View {
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
case .audio: case .audio:
ZStack(alignment: .topLeading) { VStack {
Button { ProfileImage(imageStr: call.contact.profile.image)
chatModel.activeCallViewIsCollapsed = true .scaledToFit()
} label: { .frame(width: 192, height: 192)
Label("Back", systemImage: "chevron.left") callInfoView(call, .center)
.padding()
.foregroundColor(.white.opacity(0.8))
}
VStack {
ProfileImage(imageStr: call.contact.profile.image)
.scaledToFit()
.frame(width: 192, height: 192)
audioCallInfoView(call)
}
.foregroundColor(.white)
.opacity(0.8)
.padding()
.frame(maxHeight: .infinity)
} }
.foregroundColor(.white)
.opacity(0.8)
.padding()
.frame(maxHeight: .infinity)
Spacer() Spacer()
@@ -266,12 +235,12 @@ struct ActiveCallOverlay: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
private func audioCallInfoView(_ call: Call) -> some View { private func callInfoView(_ call: Call, _ alignment: Alignment) -> some View {
VStack { VStack {
Text(call.contact.chatViewName) Text(call.contact.chatViewName)
.lineLimit(1) .lineLimit(1)
.font(.title) .font(.title)
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: alignment)
Group { Group {
Text(call.callState.text) Text(call.callState.text)
HStack { HStack {
@@ -282,36 +251,7 @@ struct ActiveCallOverlay: View {
} }
} }
.font(.subheadline) .font(.subheadline)
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: alignment)
}
}
private func videoCallInfoView(_ call: Call) -> some View {
VStack {
Button {
chatModel.activeCallViewIsCollapsed = true
} label: {
HStack(alignment: .center, spacing: 16) {
Image(systemName: "chevron.left")
.resizable()
.frame(width: 10, height: 18)
Text(call.contact.chatViewName)
.lineLimit(1)
.font(.title)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
Group {
Text(call.callState.text)
HStack {
Text(call.encryptionStatus)
if let connInfo = call.connectionInfo {
Text("(") + Text(connInfo.text) + Text(")")
}
}
}
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
} }
} }

View File

@@ -92,7 +92,6 @@ class CallManager {
if case .ended = call.callState { if case .ended = call.callState {
logger.debug("CallManager.endCall: call ended") logger.debug("CallManager.endCall: call ended")
m.activeCall = nil m.activeCall = nil
m.activeCallViewIsCollapsed = false
m.showCallView = false m.showCallView = false
completed() completed()
} else { } else {
@@ -101,7 +100,6 @@ class CallManager {
await m.callCommand.processCommand(.end) await m.callCommand.processCommand(.end)
await MainActor.run { await MainActor.run {
m.activeCall = nil m.activeCall = nil
m.activeCallViewIsCollapsed = false
m.showCallView = false m.showCallView = false
completed() completed()
} }

View File

@@ -6,20 +6,14 @@
import SwiftUI import SwiftUI
import WebRTC import WebRTC
import SimpleXChat import SimpleXChat
import AVKit
struct CallViewRemote: UIViewRepresentable { struct CallViewRemote: UIViewRepresentable {
var client: WebRTCClient var client: WebRTCClient
var activeCall: Binding<WebRTCClient.Call?> var activeCall: Binding<WebRTCClient.Call?>
@State var enablePip: (Bool) -> Void = {_ in }
@Binding var activeCallViewIsCollapsed: Bool
@Binding var pipShown: Bool
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, activeCallViewIsCollapsed: Binding<Bool>, pipShown: Binding<Bool>) { init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>) {
self.client = client self.client = client
self.activeCall = activeCall self.activeCall = activeCall
self._activeCallViewIsCollapsed = activeCallViewIsCollapsed
self._pipShown = pipShown
} }
func makeUIView(context: Context) -> UIView { func makeUIView(context: Context) -> UIView {
@@ -29,120 +23,12 @@ struct CallViewRemote: UIViewRepresentable {
remoteRenderer.videoContentMode = .scaleAspectFill remoteRenderer.videoContentMode = .scaleAspectFill
client.addRemoteRenderer(call, remoteRenderer) client.addRemoteRenderer(call, remoteRenderer)
addSubviewAndResize(remoteRenderer, into: view) addSubviewAndResize(remoteRenderer, into: view)
if AVPictureInPictureController.isPictureInPictureSupported() {
makeViewWithRTCRenderer(call, remoteRenderer, view, context)
}
} }
return view return view
} }
func makeViewWithRTCRenderer(_ call: WebRTCClient.Call, _ remoteRenderer: RTCMTLVideoView, _ view: UIView, _ context: Context) {
let pipRemoteRenderer = RTCMTLVideoView(frame: view.frame)
pipRemoteRenderer.videoContentMode = .scaleAspectFill
let pipVideoCallViewController = AVPictureInPictureVideoCallViewController()
pipVideoCallViewController.preferredContentSize = CGSize(width: 1080, height: 1920)
addSubviewAndResize(pipRemoteRenderer, into: pipVideoCallViewController.view)
let pipContentSource = AVPictureInPictureController.ContentSource(
activeVideoCallSourceView: view,
contentViewController: pipVideoCallViewController
)
let pipController = AVPictureInPictureController(contentSource: pipContentSource)
pipController.canStartPictureInPictureAutomaticallyFromInline = true
pipController.delegate = context.coordinator
context.coordinator.pipController = pipController
context.coordinator.willShowHide = { show in
if show {
client.addRemoteRenderer(call, pipRemoteRenderer)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
activeCallViewIsCollapsed = true
}
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
activeCallViewIsCollapsed = false
}
}
}
context.coordinator.didShowHide = { show in
if show {
remoteRenderer.isHidden = true
} else {
client.removeRemoteRenderer(call, pipRemoteRenderer)
remoteRenderer.isHidden = false
}
pipShown = show
}
DispatchQueue.main.async {
enablePip = { enable in
if enable != pipShown /* pipController.isPictureInPictureActive */ {
if enable {
pipController.startPictureInPicture()
} else {
pipController.stopPictureInPicture()
}
}
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
func updateUIView(_ view: UIView, context: Context) { func updateUIView(_ view: UIView, context: Context) {
logger.debug("CallView.updateUIView remote") logger.debug("CallView.updateUIView remote")
DispatchQueue.main.async {
if activeCallViewIsCollapsed != pipShown {
enablePip(activeCallViewIsCollapsed)
}
}
}
// MARK: - Coordinator
class Coordinator: NSObject, AVPictureInPictureControllerDelegate {
var pipController: AVPictureInPictureController? = nil
var willShowHide: (Bool) -> Void = { _ in }
var didShowHide: (Bool) -> Void = { _ in }
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
willShowHide(true)
}
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
didShowHide(true)
}
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
logger.error("PiP failed to start: \(error.localizedDescription)")
}
func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
willShowHide(false)
}
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
didShowHide(false)
}
deinit {
pipController?.stopPictureInPicture()
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
pipController?.contentSource = nil
pipController?.delegate = nil
pipController = nil
}
}
class SampleBufferVideoCallView: UIView {
override class var layerClass: AnyClass {
get { return AVSampleBufferDisplayLayer.self }
}
var sampleBufferDisplayLayer: AVSampleBufferDisplayLayer {
return layer as! AVSampleBufferDisplayLayer
}
} }
} }
@@ -150,14 +36,11 @@ struct CallViewLocal: UIViewRepresentable {
var client: WebRTCClient var client: WebRTCClient
var activeCall: Binding<WebRTCClient.Call?> var activeCall: Binding<WebRTCClient.Call?>
var localRendererAspectRatio: Binding<CGFloat?> var localRendererAspectRatio: Binding<CGFloat?>
@State var pipStateChanged: (Bool) -> Void = {_ in }
@Binding var pipShown: Bool
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, localRendererAspectRatio: Binding<CGFloat?>, pipShown: Binding<Bool>) { init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, localRendererAspectRatio: Binding<CGFloat?>) {
self.client = client self.client = client
self.activeCall = activeCall self.activeCall = activeCall
self.localRendererAspectRatio = localRendererAspectRatio self.localRendererAspectRatio = localRendererAspectRatio
self._pipShown = pipShown
} }
func makeUIView(context: Context) -> UIView { func makeUIView(context: Context) -> UIView {
@@ -167,18 +50,12 @@ struct CallViewLocal: UIViewRepresentable {
client.addLocalRenderer(call, localRenderer) client.addLocalRenderer(call, localRenderer)
client.startCaptureLocalVideo(call) client.startCaptureLocalVideo(call)
addSubviewAndResize(localRenderer, into: view) addSubviewAndResize(localRenderer, into: view)
DispatchQueue.main.async {
pipStateChanged = { shown in
localRenderer.isHidden = shown
}
}
} }
return view return view
} }
func updateUIView(_ view: UIView, context: Context) { func updateUIView(_ view: UIView, context: Context) {
logger.debug("CallView.updateUIView local") logger.debug("CallView.updateUIView local")
pipStateChanged(pipShown)
} }
} }

View File

@@ -28,7 +28,6 @@ class Call: ObservableObject, Equatable {
@Published var speakerEnabled = false @Published var speakerEnabled = false
@Published var videoEnabled: Bool @Published var videoEnabled: Bool
@Published var connectionInfo: ConnectionInfo? @Published var connectionInfo: ConnectionInfo?
@Published var connectedAt: Date? = nil
init( init(
direction: CallDirection, direction: CallDirection,
@@ -60,7 +59,6 @@ class Call: ObservableObject, Equatable {
} }
} }
var hasMedia: Bool { get { callState == .offerSent || callState == .negotiated || callState == .connected } } var hasMedia: Bool { get { callState == .offerSent || callState == .negotiated || callState == .connected } }
var supportsVideo: Bool { get { peerMedia == .video || localMedia == .video } }
} }
enum CallDirection { enum CallDirection {

View File

@@ -331,10 +331,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
activeCall.remoteStream?.add(renderer) activeCall.remoteStream?.add(renderer)
} }
func removeRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) {
activeCall.remoteStream?.remove(renderer)
}
func startCaptureLocalVideo(_ activeCall: Call) { func startCaptureLocalVideo(_ activeCall: Call) {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
guard guard
@@ -414,7 +410,6 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg
guard let call = activeCall.wrappedValue else { return } guard let call = activeCall.wrappedValue else { return }
logger.debug("WebRTCClient: ending the call") logger.debug("WebRTCClient: ending the call")
activeCall.wrappedValue = nil activeCall.wrappedValue = nil
(call.localCamera as? RTCCameraVideoCapturer)?.stopCapture()
call.connection.close() call.connection.close()
call.connection.delegate = nil call.connection.delegate = nil
call.frameEncryptor?.delegate = nil call.frameEncryptor?.delegate = nil

View File

@@ -29,9 +29,6 @@ struct CIImageView: View {
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy) FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy)
} }
.onTapGesture { showFullScreenImage = true } .onTapGesture { showFullScreenImage = true }
.onChange(of: m.activeCallViewIsCollapsed) { _ in
showFullScreenImage = false
}
} else if let data = Data(base64Encoded: dropImagePrefix(image)), } else if let data = Data(base64Encoded: dropImagePrefix(image)),
let uiImage = UIImage(data: data) { let uiImage = UIImage(data: data) {
imageView(uiImage) imageView(uiImage)

View File

@@ -120,9 +120,6 @@ struct CIVideoView: View {
showFullScreenPlayer = urlDecrypted != nil showFullScreenPlayer = urlDecrypted != nil
} }
} }
.onChange(of: m.activeCallViewIsCollapsed) { _ in
showFullScreenPlayer = false
}
if !decryptionInProgress { if !decryptionInProgress {
Button { Button {
decrypt(file: file) { decrypt(file: file) {
@@ -171,9 +168,6 @@ struct CIVideoView: View {
default: () default: ()
} }
} }
.onChange(of: m.activeCallViewIsCollapsed) { _ in
showFullScreenPlayer = false
}
if !videoPlaying { if !videoPlaying {
Button { Button {
m.stopPreviousRecPlay = url m.stopPreviousRecPlay = url

View File

@@ -65,8 +65,6 @@ struct MarkedDeletedItemView: View {
} }
} }
// same texts are in markedDeletedText in ChatPreviewView, but it returns String;
// can be refactored into a single function if functions calling these are changed to return same type
var markedDeletedText: LocalizedStringKey { var markedDeletedText: LocalizedStringKey {
switch chatItem.meta.itemDeleted { switch chatItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)" case let .moderated(_, byGroupMember): "moderated by \(byGroupMember.displayName)"

View File

@@ -159,17 +159,12 @@ struct ChatView: View {
switch cInfo { switch cInfo {
case let .direct(contact): case let .direct(contact):
HStack { HStack {
let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser if contact.allowsFeature(.calls) {
if callsPrefEnabled { callButton(contact, .audio, imageName: "phone")
if chatModel.activeCall == nil { .disabled(!contact.ready || !contact.active)
callButton(contact, .audio, imageName: "phone")
.disabled(!contact.ready || !contact.active)
} else if let call = chatModel.activeCall, call.contact.id == cInfo.id {
endCallButton(call)
}
} }
Menu { Menu {
if callsPrefEnabled && chatModel.activeCall == nil { if contact.allowsFeature(.calls) {
Button { Button {
CallController.shared.startCall(contact, .video) CallController.shared.startCall(contact, .video)
} label: { } label: {
@@ -426,19 +421,7 @@ struct ChatView: View {
Image(systemName: imageName) Image(systemName: imageName)
} }
} }
private func endCallButton(_ call: Call) -> some View {
Button {
if let uuid = call.callkitUUID {
CallController.shared.endCall(callUUID: uuid)
} else {
CallController.shared.endCall(call: call) {}
}
} label: {
Image(systemName: "phone.down.fill").tint(.red)
}
}
private func searchButton() -> some View { private func searchButton() -> some View {
Button { Button {
searchMode = true searchMode = true
@@ -765,9 +748,7 @@ struct ChatView: View {
if ci.meta.editable && !mc.isVoice && !live { if ci.meta.editable && !mc.isVoice && !live {
menu.append(editAction(ci)) menu.append(editAction(ci))
} }
if !ci.isLiveDummy { menu.append(viewInfoUIAction(ci))
menu.append(viewInfoUIAction(ci))
}
if revealed { if revealed {
menu.append(hideUIAction()) menu.append(hideUIAction())
} }

View File

@@ -978,9 +978,6 @@ struct ComposeView: View {
} }
private func cancelLinkPreview() { private func cancelLinkPreview() {
if let pendingLink = pendingLinkUrl?.absoluteString {
cancelledLinks.insert(pendingLink)
}
if let uri = composeState.linkPreview?.uri.absoluteString { if let uri = composeState.linkPreview?.uri.absoluteString {
cancelledLinks.insert(uri) cancelledLinks.insert(uri)
} }

View File

@@ -234,29 +234,39 @@ struct GroupChatInfoView: View {
Spacer() Spacer()
memberInfo(member) memberInfo(member)
} }
// revert from this:
if user { if user {
v v
} else if groupInfo.membership.memberRole >= .admin { } else if member.canBeRemoved(groupInfo: groupInfo) {
// TODO if there are more actions, refactor with lists of swipeActions removeSwipe(member, blockSwipe(member, v))
let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo)
let canRemove = member.canBeRemoved(groupInfo: groupInfo)
if canBlockForAll && canRemove {
removeSwipe(member, blockForAllSwipe(member, v))
} else if canBlockForAll {
blockForAllSwipe(member, v)
} else if canRemove {
removeSwipe(member, v)
} else {
v
}
} else { } else {
if !member.blockedByAdmin { blockSwipe(member, v)
blockSwipe(member, v)
} else {
v
}
} }
// revert to this: vvv
// if user {
// v
// } else if groupInfo.membership.memberRole >= .admin {
// // TODO if there are more actions, refactor with lists of swipeActions
// let canBlockForAll = member.canBlockForAll(groupInfo: groupInfo)
// let canRemove = member.canBeRemoved(groupInfo: groupInfo)
// if canBlockForAll && canRemove {
// removeSwipe(member, blockForAllSwipe(member, v))
// } else if canBlockForAll {
// blockForAllSwipe(member, v)
// } else if canRemove {
// removeSwipe(member, v)
// } else {
// v
// }
// } else {
// if !member.blockedByAdmin {
// blockSwipe(member, v)
// } else {
// v
// }
// }
// ^^^
} }
@ViewBuilder private func memberInfo(_ member: GroupMember) -> some View { @ViewBuilder private func memberInfo(_ member: GroupMember) -> some View {
@@ -360,11 +370,7 @@ struct GroupChatInfoView: View {
private func addOrEditWelcomeMessage() -> some View { private func addOrEditWelcomeMessage() -> some View {
NavigationLink { NavigationLink {
GroupWelcomeView( GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo)
groupInfo: $groupInfo,
groupProfile: groupInfo.groupProfile,
welcomeText: groupInfo.groupProfile.description ?? ""
)
.navigationTitle("Welcome message") .navigationTitle("Welcome message")
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
} label: { } label: {

View File

@@ -168,11 +168,24 @@ struct GroupMemberInfoView: View {
} }
} }
if groupInfo.membership.memberRole >= .admin { // revert from this:
adminDestructiveSection(member) Section {
} else { if member.memberSettings.showMessages {
nonAdminBlockSection(member) blockMemberButton(member)
} else {
unblockMemberButton(member)
}
if member.canBeRemoved(groupInfo: groupInfo) {
removeMemberButton(member)
}
} }
// revert to this: vvv
// if groupInfo.membership.memberRole >= .admin {
// adminDestructiveSection(member)
// } else {
// nonAdminBlockSection(member)
// }
// ^^^
if developerTools { if developerTools {
Section("For console") { Section("For console") {

View File

@@ -11,32 +11,29 @@ import SimpleXChat
struct GroupWelcomeView: View { struct GroupWelcomeView: View {
@Environment(\.dismiss) var dismiss: DismissAction @Environment(\.dismiss) var dismiss: DismissAction
@EnvironmentObject private var m: ChatModel
var groupId: Int64
@Binding var groupInfo: GroupInfo @Binding var groupInfo: GroupInfo
@State var groupProfile: GroupProfile @State private var welcomeText: String = ""
@State var welcomeText: String
@State private var editMode = true @State private var editMode = true
@FocusState private var keyboardVisible: Bool @FocusState private var keyboardVisible: Bool
@State private var showSaveDialog = false @State private var showSaveDialog = false
let maxByteCount = 1200
var body: some View { var body: some View {
VStack { VStack {
if groupInfo.canEdit { if groupInfo.canEdit {
editorView() editorView()
.modifier(BackButton { .modifier(BackButton {
if welcomeTextUnchanged() { if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) {
dismiss() dismiss()
} else { } else {
showSaveDialog = true showSaveDialog = true
} }
}) })
.confirmationDialog( .confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) {
welcomeTextFitsLimit() ? "Save welcome message?" : "Welcome message is too long", Button("Save and update group profile") {
isPresented: $showSaveDialog save()
) { dismiss()
if welcomeTextFitsLimit() {
Button("Save and update group profile") { save() }
} }
Button("Exit without saving") { dismiss() } Button("Exit without saving") { dismiss() }
} }
@@ -50,15 +47,14 @@ struct GroupWelcomeView: View {
} }
} }
.onAppear { .onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { welcomeText = groupInfo.groupProfile.description ?? ""
keyboardVisible = true keyboardVisible = true
}
} }
} }
private func textPreview() -> some View { private func textPreview() -> some View {
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false) messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false)
.frame(minHeight: 130, alignment: .topLeading) .frame(minHeight: 140, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
@@ -78,7 +74,7 @@ struct GroupWelcomeView: View {
} }
.padding(.horizontal, -5) .padding(.horizontal, -5)
.padding(.top, -8) .padding(.top, -8)
.frame(height: 130, alignment: .topLeading) .frame(height: 140, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
} }
} else { } else {
@@ -97,9 +93,6 @@ struct GroupWelcomeView: View {
} }
.disabled(welcomeText.isEmpty) .disabled(welcomeText.isEmpty)
copyButton() copyButton()
} footer: {
Text(!welcomeTextFitsLimit() ? "Message too large" : "")
.foregroundColor(.red)
} }
Section { Section {
@@ -120,15 +113,7 @@ struct GroupWelcomeView: View {
Button("Save and update group profile") { Button("Save and update group profile") {
save() save()
} }
.disabled(welcomeTextUnchanged() || !welcomeTextFitsLimit()) .disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil))
}
private func welcomeTextUnchanged() -> Bool {
welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil)
}
private func welcomeTextFitsLimit() -> Bool {
chatJsonLength(welcomeText) <= maxByteCount
} }
private func save() { private func save() {
@@ -138,13 +123,11 @@ struct GroupWelcomeView: View {
if welcome?.count == 0 { if welcome?.count == 0 {
welcome = nil welcome = nil
} }
groupProfile.description = welcome var groupProfileUpdated = groupInfo.groupProfile
let gInfo = try await apiUpdateGroup(groupInfo.groupId, groupProfile) groupProfileUpdated.description = welcome
await MainActor.run { groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated)
groupInfo = gInfo m.updateGroup(groupInfo)
ChatModel.shared.updateGroup(gInfo) welcomeText = welcome ?? ""
dismiss()
}
} catch let error { } catch let error {
logger.error("apiUpdateGroup error: \(responseError(error))") logger.error("apiUpdateGroup error: \(responseError(error))")
} }
@@ -154,6 +137,6 @@ struct GroupWelcomeView: View {
struct GroupWelcomeView_Previews: PreviewProvider { struct GroupWelcomeView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
GroupProfileView(groupInfo: Binding.constant(GroupInfo.sampleData), groupProfile: GroupProfile.sampleData) GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData))
} }
} }

View File

@@ -34,7 +34,7 @@ struct ChatPreviewView: View {
HStack(alignment: .top) { HStack(alignment: .top) {
chatPreviewTitle() chatPreviewTitle()
Spacer() Spacer()
(cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs)) (cItem?.timestampText ?? formatTimestampText(chat.chatInfo.updatedAt))
.font(.subheadline) .font(.subheadline)
.frame(minWidth: 60, alignment: .trailing) .frame(minWidth: 60, alignment: .trailing)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@@ -171,21 +171,10 @@ struct ChatPreviewView: View {
} }
func chatItemPreview(_ cItem: ChatItem) -> Text { func chatItemPreview(_ cItem: ChatItem) -> Text {
let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText() let itemText = cItem.meta.itemDeleted == nil ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil let itemFormattedText = cItem.meta.itemDeleted == nil ? cItem.formattedText : nil
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false) return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true, showSecrets: false)
// same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey;
// can be refactored into a single function if functions calling these are changed to return same type
func markedDeletedText() -> String {
switch cItem.meta.itemDeleted {
case let .moderated(_, byGroupMember): String.localizedStringWithFormat(NSLocalizedString("moderated by %@", comment: "marked deleted chat item preview text"), byGroupMember.displayName)
case .blocked: NSLocalizedString("blocked", comment: "marked deleted chat item preview text")
case .blockedByAdmin: NSLocalizedString("blocked by admin", comment: "marked deleted chat item preview text")
case .deleted, nil: NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
}
}
func attachment() -> String? { func attachment() -> String? {
switch cItem.content.msgContent { switch cItem.content.msgContent {
case .file: return "doc.fill" case .file: return "doc.fill"

View File

@@ -76,10 +76,6 @@ struct NotificationsView: View {
Text(m.notificationPreview.label) Text(m.notificationPreview.label)
} }
} }
if let server = m.notificationServer {
smpServers("Push server", [server])
}
} header: { } header: {
Text("Push notifications") Text("Push notifications")
} footer: { } footer: {
@@ -91,9 +87,6 @@ struct NotificationsView: View {
} }
} }
.disabled(legacyDatabase) .disabled(legacyDatabase)
.onAppear {
(m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken()
}
} }
private func notificationAlert(_ alert: NotificationAlert, _ token: DeviceToken) -> Alert { private func notificationAlert(_ alert: NotificationAlert, _ token: DeviceToken) -> Alert {
@@ -132,7 +125,6 @@ struct NotificationsView: View {
m.tokenStatus = .new m.tokenStatus = .new
notificationMode = .off notificationMode = .off
m.notificationMode = .off m.notificationMode = .off
m.notificationServer = nil
} }
} catch let error { } catch let error {
await MainActor.run { await MainActor.run {
@@ -143,13 +135,11 @@ struct NotificationsView: View {
} }
default: default:
do { do {
let _ = try await apiRegisterToken(token: token, notificationMode: mode) let status = try await apiRegisterToken(token: token, notificationMode: mode)
let (_, tknStatus, ntfMode, ntfServer) = apiGetNtfToken()
await MainActor.run { await MainActor.run {
m.tokenStatus = tknStatus m.tokenStatus = status
notificationMode = ntfMode notificationMode = mode
m.notificationMode = ntfMode m.notificationMode = mode
m.notificationServer = ntfServer
} }
} catch let error { } catch let error {
await MainActor.run { await MainActor.run {

View File

@@ -219,7 +219,6 @@
</trans-unit> </trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve"> <trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source> <source>%lld messages blocked by admin</source>
<target>%lld Nachrichten wurden vom Administrator blockiert</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve"> <trans-unit id="%lld messages marked deleted" xml:space="preserve">
@@ -645,7 +644,6 @@
</trans-unit> </trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>All messages will be deleted - this cannot be undone!</source> <source>All messages will be deleted - this cannot be undone!</source>
<target>Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
@@ -655,7 +653,7 @@
</trans-unit> </trans-unit>
<trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve"> <trans-unit id="All new messages from %@ will be hidden!" xml:space="preserve">
<source>All new messages from %@ will be hidden!</source> <source>All new messages from %@ will be hidden!</source>
<target>Von %@ werden alle neuen Nachrichten ausgeblendet!</target> <target>Alle neuen Nachrichten von %@ werden verborgen!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="All your contacts will remain connected." xml:space="preserve"> <trans-unit id="All your contacts will remain connected." xml:space="preserve">
@@ -925,7 +923,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Block for all" xml:space="preserve"> <trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source> <source>Block for all</source>
<target>Für Alle blockieren</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block group members" xml:space="preserve"> <trans-unit id="Block group members" xml:space="preserve">
@@ -940,7 +937,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve"> <trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source> <source>Block member for all?</source>
<target>Mitglied für Alle blockieren?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block member?" xml:space="preserve"> <trans-unit id="Block member?" xml:space="preserve">
@@ -950,7 +946,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve"> <trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source> <source>Blocked by admin</source>
<target>wurde vom Administrator blockiert</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
@@ -1166,7 +1161,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Clear private notes?" xml:space="preserve"> <trans-unit id="Clear private notes?" xml:space="preserve">
<source>Clear private notes?</source> <source>Clear private notes?</source>
<target>Private Notizen löschen?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Clear verification" xml:space="preserve"> <trans-unit id="Clear verification" xml:space="preserve">
@@ -1465,12 +1459,10 @@ Das ist Ihr eigener Einmal-Link!</target>
</trans-unit> </trans-unit>
<trans-unit id="Created at" xml:space="preserve"> <trans-unit id="Created at" xml:space="preserve">
<source>Created at</source> <source>Created at</source>
<target>Erstellt um</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Created at: %@" xml:space="preserve"> <trans-unit id="Created at: %@" xml:space="preserve">
<source>Created at: %@</source> <source>Created at: %@</source>
<target>Erstellt um: %@</target>
<note>copied message info</note> <note>copied message info</note>
</trans-unit> </trans-unit>
<trans-unit id="Created on %@" xml:space="preserve"> <trans-unit id="Created on %@" xml:space="preserve">
@@ -2260,7 +2252,6 @@ Das kann nicht rückgängig gemacht werden!</target>
</trans-unit> </trans-unit>
<trans-unit id="Error creating message" xml:space="preserve"> <trans-unit id="Error creating message" xml:space="preserve">
<source>Error creating message</source> <source>Error creating message</source>
<target>Fehler beim Erstellen der Nachricht</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Error creating profile!" xml:space="preserve"> <trans-unit id="Error creating profile!" xml:space="preserve">
@@ -2935,7 +2926,6 @@ Das kann nicht rückgängig gemacht werden!</target>
</trans-unit> </trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve"> <trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source> <source>Improved message delivery</source>
<target>Verbesserte Zustellung von Nachrichten</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Improved privacy and security" xml:space="preserve"> <trans-unit id="Improved privacy and security" xml:space="preserve">
@@ -3163,7 +3153,6 @@ Das kann nicht rückgängig gemacht werden!</target>
</trans-unit> </trans-unit>
<trans-unit id="Join group conversations" xml:space="preserve"> <trans-unit id="Join group conversations" xml:space="preserve">
<source>Join group conversations</source> <source>Join group conversations</source>
<target>Gruppenunterhaltungen beitreten</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Join group?" xml:space="preserve"> <trans-unit id="Join group?" xml:space="preserve">
@@ -3874,7 +3863,6 @@ Das ist Ihr Link für die Gruppe %@!</target>
</trans-unit> </trans-unit>
<trans-unit id="Past member %@" xml:space="preserve"> <trans-unit id="Past member %@" xml:space="preserve">
<source>Past member %@</source> <source>Past member %@</source>
<target>Ehemaliges Mitglied %@</target>
<note>past/unknown group member</note> <note>past/unknown group member</note>
</trans-unit> </trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve"> <trans-unit id="Paste desktop address" xml:space="preserve">
@@ -3889,7 +3877,6 @@ Das ist Ihr Link für die Gruppe %@!</target>
</trans-unit> </trans-unit>
<trans-unit id="Paste link to connect!" xml:space="preserve"> <trans-unit id="Paste link to connect!" xml:space="preserve">
<source>Paste link to connect!</source> <source>Paste link to connect!</source>
<target>Zum Verbinden den Link einfügen!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve"> <trans-unit id="Paste the link you received" xml:space="preserve">
@@ -4026,7 +4013,6 @@ Fehler: %@</target>
</trans-unit> </trans-unit>
<trans-unit id="Private notes" xml:space="preserve"> <trans-unit id="Private notes" xml:space="preserve">
<source>Private notes</source> <source>Private notes</source>
<target>Private Notizen</target>
<note>name of notes to self</note> <note>name of notes to self</note>
</trans-unit> </trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve"> <trans-unit id="Profile and server connections" xml:space="preserve">
@@ -4211,7 +4197,6 @@ Fehler: %@</target>
</trans-unit> </trans-unit>
<trans-unit id="Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." xml:space="preserve"> <trans-unit id="Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." xml:space="preserve">
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source> <source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
<target>Aktueller Nachrichtenverlauf und verbesserter [Gruppenverzeichnis-Bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <trans-unit id="Recipients see updates as you type them." xml:space="preserve">
@@ -4501,7 +4486,6 @@ Fehler: %@</target>
</trans-unit> </trans-unit>
<trans-unit id="Saved message" xml:space="preserve"> <trans-unit id="Saved message" xml:space="preserve">
<source>Saved message</source> <source>Saved message</source>
<target>Gespeicherte Nachricht</target>
<note>message info title</note> <note>message info title</note>
</trans-unit> </trans-unit>
<trans-unit id="Scan QR code" xml:space="preserve"> <trans-unit id="Scan QR code" xml:space="preserve">
@@ -4536,7 +4520,6 @@ Fehler: %@</target>
</trans-unit> </trans-unit>
<trans-unit id="Search bar accepts invitation links." xml:space="preserve"> <trans-unit id="Search bar accepts invitation links." xml:space="preserve">
<source>Search bar accepts invitation links.</source> <source>Search bar accepts invitation links.</source>
<target>Von der Suchleiste werden Einladungslinks akzeptiert.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve"> <trans-unit id="Search or paste SimpleX link" xml:space="preserve">
@@ -5375,7 +5358,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
</trans-unit> </trans-unit>
<trans-unit id="Turkish interface" xml:space="preserve"> <trans-unit id="Turkish interface" xml:space="preserve">
<source>Turkish interface</source> <source>Turkish interface</source>
<target>Türkische Bedienoberfläche</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Turn off" xml:space="preserve"> <trans-unit id="Turn off" xml:space="preserve">
@@ -5400,7 +5382,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
</trans-unit> </trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve"> <trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source> <source>Unblock for all</source>
<target>Für Alle freigeben</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock member" xml:space="preserve"> <trans-unit id="Unblock member" xml:space="preserve">
@@ -5410,7 +5391,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
</trans-unit> </trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve"> <trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source> <source>Unblock member for all?</source>
<target>Mitglied für Alle freigeben?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve"> <trans-unit id="Unblock member?" xml:space="preserve">
@@ -5777,7 +5757,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
</trans-unit> </trans-unit>
<trans-unit id="With encrypted files and media." xml:space="preserve"> <trans-unit id="With encrypted files and media." xml:space="preserve">
<source>With encrypted files and media.</source> <source>With encrypted files and media.</source>
<target>Mit verschlüsselten Dateien und Medien.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="With optional welcome message." xml:space="preserve"> <trans-unit id="With optional welcome message." xml:space="preserve">
@@ -5787,7 +5766,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
</trans-unit> </trans-unit>
<trans-unit id="With reduced battery usage." xml:space="preserve"> <trans-unit id="With reduced battery usage." xml:space="preserve">
<source>With reduced battery usage.</source> <source>With reduced battery usage.</source>
<target>Mit reduziertem Akkuverbrauch.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve"> <trans-unit id="Wrong database passphrase" xml:space="preserve">
@@ -6290,17 +6268,15 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit> </trans-unit>
<trans-unit id="blocked" xml:space="preserve"> <trans-unit id="blocked" xml:space="preserve">
<source>blocked</source> <source>blocked</source>
<target>Blockiert</target> <target>blockiert</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="blocked %@" xml:space="preserve"> <trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source> <source>blocked %@</source>
<target>%@ wurde blockiert</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve"> <trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source> <source>blocked by admin</source>
<target>wurde vom Administrator blockiert</target>
<note>blocked chat item</note> <note>blocked chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="bold" xml:space="preserve"> <trans-unit id="bold" xml:space="preserve">
@@ -6425,7 +6401,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit> </trans-unit>
<trans-unit id="contact %@ changed to %@" xml:space="preserve"> <trans-unit id="contact %@ changed to %@" xml:space="preserve">
<source>contact %1$@ changed to %2$@</source> <source>contact %1$@ changed to %2$@</source>
<target>Der Kontaktname %1$@ wurde auf %2$@ geändert</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="contact has e2e encryption" xml:space="preserve"> <trans-unit id="contact has e2e encryption" xml:space="preserve">
@@ -6700,7 +6675,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit> </trans-unit>
<trans-unit id="member %@ changed to %@" xml:space="preserve"> <trans-unit id="member %@ changed to %@" xml:space="preserve">
<source>member %1$@ changed to %2$@</source> <source>member %1$@ changed to %2$@</source>
<target>Der Mitgliedsname %1$@ wurde auf %2$@ geändert</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="member connected" xml:space="preserve"> <trans-unit id="member connected" xml:space="preserve">
@@ -6827,12 +6801,10 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit> </trans-unit>
<trans-unit id="removed contact address" xml:space="preserve"> <trans-unit id="removed contact address" xml:space="preserve">
<source>removed contact address</source> <source>removed contact address</source>
<target>Kontaktadresse wurde entfernt</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="removed profile picture" xml:space="preserve"> <trans-unit id="removed profile picture" xml:space="preserve">
<source>removed profile picture</source> <source>removed profile picture</source>
<target>Profil-Bild wurde entfernt</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="removed you" xml:space="preserve"> <trans-unit id="removed you" xml:space="preserve">
@@ -6867,12 +6839,10 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit> </trans-unit>
<trans-unit id="set new contact address" xml:space="preserve"> <trans-unit id="set new contact address" xml:space="preserve">
<source>set new contact address</source> <source>set new contact address</source>
<target>Neue Kontaktadresse wurde festgelegt</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="set new profile picture" xml:space="preserve"> <trans-unit id="set new profile picture" xml:space="preserve">
<source>set new profile picture</source> <source>set new profile picture</source>
<target>Neues Profil-Bild wurde festgelegt</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="starting…" xml:space="preserve"> <trans-unit id="starting…" xml:space="preserve">
@@ -6892,7 +6862,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit> </trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve"> <trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source> <source>unblocked %@</source>
<target>%@ wurde freigegeben</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="unknown" xml:space="preserve"> <trans-unit id="unknown" xml:space="preserve">
@@ -6902,7 +6871,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit> </trans-unit>
<trans-unit id="unknown status" xml:space="preserve"> <trans-unit id="unknown status" xml:space="preserve">
<source>unknown status</source> <source>unknown status</source>
<target>unbekannter Gruppenmitglieds-Status</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="updated group profile" xml:space="preserve"> <trans-unit id="updated group profile" xml:space="preserve">
@@ -6912,7 +6880,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit> </trans-unit>
<trans-unit id="updated profile" xml:space="preserve"> <trans-unit id="updated profile" xml:space="preserve">
<source>updated profile</source> <source>updated profile</source>
<target>Das Profil wurde aktualisiert</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="v%@" xml:space="preserve"> <trans-unit id="v%@" xml:space="preserve">
@@ -6987,7 +6954,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit> </trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve"> <trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source> <source>you blocked %@</source>
<target>Sie haben %@ blockiert</target>
<note>snd group event chat item</note> <note>snd group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="you changed address" xml:space="preserve"> <trans-unit id="you changed address" xml:space="preserve">
@@ -7032,7 +6998,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
</trans-unit> </trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve"> <trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source> <source>you unblocked %@</source>
<target>Sie haben %@ freigegeben</target>
<note>snd group event chat item</note> <note>snd group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="you: " xml:space="preserve"> <trans-unit id="you: " xml:space="preserve">

View File

@@ -219,7 +219,6 @@
</trans-unit> </trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve"> <trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source> <source>%lld messages blocked by admin</source>
<target>%lld messaggi bloccati dall'amministratore</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve"> <trans-unit id="%lld messages marked deleted" xml:space="preserve">
@@ -925,7 +924,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Block for all" xml:space="preserve"> <trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source> <source>Block for all</source>
<target>Blocca per tutti</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block group members" xml:space="preserve"> <trans-unit id="Block group members" xml:space="preserve">
@@ -940,7 +938,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve"> <trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source> <source>Block member for all?</source>
<target>Bloccare il membro per tutti?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block member?" xml:space="preserve"> <trans-unit id="Block member?" xml:space="preserve">
@@ -950,7 +947,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve"> <trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source> <source>Blocked by admin</source>
<target>Bloccato dall'amministratore</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
@@ -5400,7 +5396,6 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio
</trans-unit> </trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve"> <trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source> <source>Unblock for all</source>
<target>Sblocca per tutti</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock member" xml:space="preserve"> <trans-unit id="Unblock member" xml:space="preserve">
@@ -5410,7 +5405,6 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio
</trans-unit> </trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve"> <trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source> <source>Unblock member for all?</source>
<target>Sbloccare il membro per tutti?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve"> <trans-unit id="Unblock member?" xml:space="preserve">
@@ -6295,12 +6289,10 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
</trans-unit> </trans-unit>
<trans-unit id="blocked %@" xml:space="preserve"> <trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source> <source>blocked %@</source>
<target>ha bloccato %@</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve"> <trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source> <source>blocked by admin</source>
<target>bloccato dall'amministratore</target>
<note>blocked chat item</note> <note>blocked chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="bold" xml:space="preserve"> <trans-unit id="bold" xml:space="preserve">
@@ -6892,7 +6884,6 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
</trans-unit> </trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve"> <trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source> <source>unblocked %@</source>
<target>ha sbloccato %@</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="unknown" xml:space="preserve"> <trans-unit id="unknown" xml:space="preserve">
@@ -6987,7 +6978,6 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
</trans-unit> </trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve"> <trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source> <source>you blocked %@</source>
<target>hai bloccato %@</target>
<note>snd group event chat item</note> <note>snd group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="you changed address" xml:space="preserve"> <trans-unit id="you changed address" xml:space="preserve">
@@ -7032,7 +7022,6 @@ I server di SimpleX non possono vedere il tuo profilo.</target>
</trans-unit> </trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve"> <trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source> <source>you unblocked %@</source>
<target>hai sbloccato %@</target>
<note>snd group event chat item</note> <note>snd group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="you: " xml:space="preserve"> <trans-unit id="you: " xml:space="preserve">

View File

@@ -219,7 +219,6 @@
</trans-unit> </trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve"> <trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source> <source>%lld messages blocked by admin</source>
<target>%lld berichten geblokkeerd door beheerder</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve"> <trans-unit id="%lld messages marked deleted" xml:space="preserve">
@@ -925,7 +924,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Block for all" xml:space="preserve"> <trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source> <source>Block for all</source>
<target>Blokkeren voor iedereen</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block group members" xml:space="preserve"> <trans-unit id="Block group members" xml:space="preserve">
@@ -940,7 +938,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve"> <trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source> <source>Block member for all?</source>
<target>Lid voor iedereen blokkeren?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block member?" xml:space="preserve"> <trans-unit id="Block member?" xml:space="preserve">
@@ -950,7 +947,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve"> <trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source> <source>Blocked by admin</source>
<target>Geblokkeerd door beheerder</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
@@ -3163,7 +3159,7 @@ Dit kan niet ongedaan gemaakt worden!</target>
</trans-unit> </trans-unit>
<trans-unit id="Join group conversations" xml:space="preserve"> <trans-unit id="Join group conversations" xml:space="preserve">
<source>Join group conversations</source> <source>Join group conversations</source>
<target>Neem deel aan groepsgesprekken</target> <target>Neem deel aan groep gesprekken</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Join group?" xml:space="preserve"> <trans-unit id="Join group?" xml:space="preserve">
@@ -3889,7 +3885,7 @@ Dit is jouw link voor groep %@!</target>
</trans-unit> </trans-unit>
<trans-unit id="Paste link to connect!" xml:space="preserve"> <trans-unit id="Paste link to connect!" xml:space="preserve">
<source>Paste link to connect!</source> <source>Paste link to connect!</source>
<target>Plak een link om te verbinden!</target> <target>Plak link om te verbinden!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve"> <trans-unit id="Paste the link you received" xml:space="preserve">
@@ -4541,7 +4537,7 @@ Fout: %@</target>
</trans-unit> </trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve"> <trans-unit id="Search or paste SimpleX link" xml:space="preserve">
<source>Search or paste SimpleX link</source> <source>Search or paste SimpleX link</source>
<target>Zoek of plak een SimpleX link</target> <target>Zoek of plak de SimpleX link</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Secure queue" xml:space="preserve"> <trans-unit id="Secure queue" xml:space="preserve">
@@ -5400,7 +5396,6 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc
</trans-unit> </trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve"> <trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source> <source>Unblock for all</source>
<target>Deblokkeer voor iedereen</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock member" xml:space="preserve"> <trans-unit id="Unblock member" xml:space="preserve">
@@ -5410,7 +5405,6 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc
</trans-unit> </trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve"> <trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source> <source>Unblock member for all?</source>
<target>Lid voor iedereen deblokkeren?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve"> <trans-unit id="Unblock member?" xml:space="preserve">
@@ -6295,12 +6289,10 @@ SimpleX servers kunnen uw profiel niet zien.</target>
</trans-unit> </trans-unit>
<trans-unit id="blocked %@" xml:space="preserve"> <trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source> <source>blocked %@</source>
<target>geblokkeerd %@</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve"> <trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source> <source>blocked by admin</source>
<target>geblokkeerd door beheerder</target>
<note>blocked chat item</note> <note>blocked chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="bold" xml:space="preserve"> <trans-unit id="bold" xml:space="preserve">
@@ -6892,7 +6884,6 @@ SimpleX servers kunnen uw profiel niet zien.</target>
</trans-unit> </trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve"> <trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source> <source>unblocked %@</source>
<target>gedeblokkeerd %@</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="unknown" xml:space="preserve"> <trans-unit id="unknown" xml:space="preserve">
@@ -6987,7 +6978,6 @@ SimpleX servers kunnen uw profiel niet zien.</target>
</trans-unit> </trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve"> <trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source> <source>you blocked %@</source>
<target>je hebt %@ geblokkeerd</target>
<note>snd group event chat item</note> <note>snd group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="you changed address" xml:space="preserve"> <trans-unit id="you changed address" xml:space="preserve">
@@ -7032,7 +7022,6 @@ SimpleX servers kunnen uw profiel niet zien.</target>
</trans-unit> </trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve"> <trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source> <source>you unblocked %@</source>
<target>je hebt %@ gedeblokkeerd</target>
<note>snd group event chat item</note> <note>snd group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="you: " xml:space="preserve"> <trans-unit id="you: " xml:space="preserve">

View File

@@ -219,7 +219,6 @@
</trans-unit> </trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve"> <trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source> <source>%lld messages blocked by admin</source>
<target>%lld wiadomości zablokowanych przez admina</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve"> <trans-unit id="%lld messages marked deleted" xml:space="preserve">
@@ -645,7 +644,6 @@
</trans-unit> </trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve"> <trans-unit id="All messages will be deleted - this cannot be undone!" xml:space="preserve">
<source>All messages will be deleted - this cannot be undone!</source> <source>All messages will be deleted - this cannot be undone!</source>
<target>Wszystkie wiadomości zostaną usunięte nie można tego cofnąć!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
@@ -925,7 +923,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Block for all" xml:space="preserve"> <trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source> <source>Block for all</source>
<target>Zablokuj dla wszystkich</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block group members" xml:space="preserve"> <trans-unit id="Block group members" xml:space="preserve">
@@ -940,7 +937,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve"> <trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source> <source>Block member for all?</source>
<target>Zablokować członka dla wszystkich?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block member?" xml:space="preserve"> <trans-unit id="Block member?" xml:space="preserve">
@@ -950,7 +946,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve"> <trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source> <source>Blocked by admin</source>
<target>Zablokowany przez admina</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
@@ -1166,7 +1161,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Clear private notes?" xml:space="preserve"> <trans-unit id="Clear private notes?" xml:space="preserve">
<source>Clear private notes?</source> <source>Clear private notes?</source>
<target>Wyczyścić prywatne notatki?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Clear verification" xml:space="preserve"> <trans-unit id="Clear verification" xml:space="preserve">
@@ -1465,12 +1459,10 @@ To jest twój jednorazowy link!</target>
</trans-unit> </trans-unit>
<trans-unit id="Created at" xml:space="preserve"> <trans-unit id="Created at" xml:space="preserve">
<source>Created at</source> <source>Created at</source>
<target>Utworzony o</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Created at: %@" xml:space="preserve"> <trans-unit id="Created at: %@" xml:space="preserve">
<source>Created at: %@</source> <source>Created at: %@</source>
<target>Utworzony o: %@</target>
<note>copied message info</note> <note>copied message info</note>
</trans-unit> </trans-unit>
<trans-unit id="Created on %@" xml:space="preserve"> <trans-unit id="Created on %@" xml:space="preserve">
@@ -1965,7 +1957,6 @@ To nie może być cofnięte!</target>
</trans-unit> </trans-unit>
<trans-unit id="Do not send history to new members." xml:space="preserve"> <trans-unit id="Do not send history to new members." xml:space="preserve">
<source>Do not send history to new members.</source> <source>Do not send history to new members.</source>
<target>Nie wysyłaj historii do nowych członków.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Don't create address" xml:space="preserve"> <trans-unit id="Don't create address" xml:space="preserve">
@@ -2260,7 +2251,6 @@ To nie może być cofnięte!</target>
</trans-unit> </trans-unit>
<trans-unit id="Error creating message" xml:space="preserve"> <trans-unit id="Error creating message" xml:space="preserve">
<source>Error creating message</source> <source>Error creating message</source>
<target>Błąd tworzenia wiadomości</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Error creating profile!" xml:space="preserve"> <trans-unit id="Error creating profile!" xml:space="preserve">
@@ -2840,7 +2830,6 @@ To nie może być cofnięte!</target>
</trans-unit> </trans-unit>
<trans-unit id="History is not sent to new members." xml:space="preserve"> <trans-unit id="History is not sent to new members." xml:space="preserve">
<source>History is not sent to new members.</source> <source>History is not sent to new members.</source>
<target>Historia nie jest wysyłana do nowych członków.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="How SimpleX works" xml:space="preserve"> <trans-unit id="How SimpleX works" xml:space="preserve">
@@ -2935,7 +2924,6 @@ To nie może być cofnięte!</target>
</trans-unit> </trans-unit>
<trans-unit id="Improved message delivery" xml:space="preserve"> <trans-unit id="Improved message delivery" xml:space="preserve">
<source>Improved message delivery</source> <source>Improved message delivery</source>
<target>Ulepszona dostawa wiadomości</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Improved privacy and security" xml:space="preserve"> <trans-unit id="Improved privacy and security" xml:space="preserve">
@@ -3052,7 +3040,6 @@ To nie może być cofnięte!</target>
</trans-unit> </trans-unit>
<trans-unit id="Invalid display name!" xml:space="preserve"> <trans-unit id="Invalid display name!" xml:space="preserve">
<source>Invalid display name!</source> <source>Invalid display name!</source>
<target>Nieprawidłowa nazwa wyświetlana!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Invalid link" xml:space="preserve"> <trans-unit id="Invalid link" xml:space="preserve">
@@ -3163,7 +3150,6 @@ To nie może być cofnięte!</target>
</trans-unit> </trans-unit>
<trans-unit id="Join group conversations" xml:space="preserve"> <trans-unit id="Join group conversations" xml:space="preserve">
<source>Join group conversations</source> <source>Join group conversations</source>
<target>Dołącz do grupowej rozmowy</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Join group?" xml:space="preserve"> <trans-unit id="Join group?" xml:space="preserve">
@@ -3874,7 +3860,6 @@ To jest twój link do grupy %@!</target>
</trans-unit> </trans-unit>
<trans-unit id="Past member %@" xml:space="preserve"> <trans-unit id="Past member %@" xml:space="preserve">
<source>Past member %@</source> <source>Past member %@</source>
<target>Były członek %@</target>
<note>past/unknown group member</note> <note>past/unknown group member</note>
</trans-unit> </trans-unit>
<trans-unit id="Paste desktop address" xml:space="preserve"> <trans-unit id="Paste desktop address" xml:space="preserve">
@@ -3889,7 +3874,6 @@ To jest twój link do grupy %@!</target>
</trans-unit> </trans-unit>
<trans-unit id="Paste link to connect!" xml:space="preserve"> <trans-unit id="Paste link to connect!" xml:space="preserve">
<source>Paste link to connect!</source> <source>Paste link to connect!</source>
<target>Wklej link, aby połączyć!</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Paste the link you received" xml:space="preserve"> <trans-unit id="Paste the link you received" xml:space="preserve">
@@ -4026,7 +4010,6 @@ Błąd: %@</target>
</trans-unit> </trans-unit>
<trans-unit id="Private notes" xml:space="preserve"> <trans-unit id="Private notes" xml:space="preserve">
<source>Private notes</source> <source>Private notes</source>
<target>Prywatne notatki</target>
<note>name of notes to self</note> <note>name of notes to self</note>
</trans-unit> </trans-unit>
<trans-unit id="Profile and server connections" xml:space="preserve"> <trans-unit id="Profile and server connections" xml:space="preserve">
@@ -4211,7 +4194,6 @@ Błąd: %@</target>
</trans-unit> </trans-unit>
<trans-unit id="Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." xml:space="preserve"> <trans-unit id="Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." xml:space="preserve">
<source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source> <source>Recent history and improved [directory bot](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</source>
<target>Ostania historia i ulepszony [bot adresowy](simplex:/contact#/?v=1-4&amp;smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <trans-unit id="Recipients see updates as you type them." xml:space="preserve">
@@ -4501,7 +4483,6 @@ Błąd: %@</target>
</trans-unit> </trans-unit>
<trans-unit id="Saved message" xml:space="preserve"> <trans-unit id="Saved message" xml:space="preserve">
<source>Saved message</source> <source>Saved message</source>
<target>Zachowano wiadomość</target>
<note>message info title</note> <note>message info title</note>
</trans-unit> </trans-unit>
<trans-unit id="Scan QR code" xml:space="preserve"> <trans-unit id="Scan QR code" xml:space="preserve">
@@ -4536,7 +4517,6 @@ Błąd: %@</target>
</trans-unit> </trans-unit>
<trans-unit id="Search bar accepts invitation links." xml:space="preserve"> <trans-unit id="Search bar accepts invitation links." xml:space="preserve">
<source>Search bar accepts invitation links.</source> <source>Search bar accepts invitation links.</source>
<target>Pasek wyszukiwania akceptuje linki zaproszenia.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Search or paste SimpleX link" xml:space="preserve"> <trans-unit id="Search or paste SimpleX link" xml:space="preserve">
@@ -4651,7 +4631,6 @@ Błąd: %@</target>
</trans-unit> </trans-unit>
<trans-unit id="Send up to 100 last messages to new members." xml:space="preserve"> <trans-unit id="Send up to 100 last messages to new members." xml:space="preserve">
<source>Send up to 100 last messages to new members.</source> <source>Send up to 100 last messages to new members.</source>
<target>Wysyłaj do 100 ostatnich wiadomości do nowych członków.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <trans-unit id="Sender cancelled file transfer." xml:space="preserve">
@@ -5268,7 +5247,6 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom
</trans-unit> </trans-unit>
<trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve"> <trans-unit id="This display name is invalid. Please choose another name." xml:space="preserve">
<source>This display name is invalid. Please choose another name.</source> <source>This display name is invalid. Please choose another name.</source>
<target>Nazwa wyświetlana jest nieprawidłowa. Proszę wybrać inną nazwę.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve">
@@ -5375,7 +5353,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
</trans-unit> </trans-unit>
<trans-unit id="Turkish interface" xml:space="preserve"> <trans-unit id="Turkish interface" xml:space="preserve">
<source>Turkish interface</source> <source>Turkish interface</source>
<target>Turecki interfejs</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Turn off" xml:space="preserve"> <trans-unit id="Turn off" xml:space="preserve">
@@ -5400,7 +5377,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
</trans-unit> </trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve"> <trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source> <source>Unblock for all</source>
<target>Odblokuj dla wszystkich</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock member" xml:space="preserve"> <trans-unit id="Unblock member" xml:space="preserve">
@@ -5410,7 +5386,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta
</trans-unit> </trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve"> <trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source> <source>Unblock member for all?</source>
<target>Odblokować członka dla wszystkich?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve"> <trans-unit id="Unblock member?" xml:space="preserve">
@@ -5512,7 +5487,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
</trans-unit> </trans-unit>
<trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve"> <trans-unit id="Up to 100 last messages are sent to new members." xml:space="preserve">
<source>Up to 100 last messages are sent to new members.</source> <source>Up to 100 last messages are sent to new members.</source>
<target>Do nowych członków wysyłanych jest do 100 ostatnich wiadomości.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Update" xml:space="preserve"> <trans-unit id="Update" xml:space="preserve">
@@ -5687,7 +5661,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
</trans-unit> </trans-unit>
<trans-unit id="Visible history" xml:space="preserve"> <trans-unit id="Visible history" xml:space="preserve">
<source>Visible history</source> <source>Visible history</source>
<target>Widoczna historia</target>
<note>chat feature</note> <note>chat feature</note>
</trans-unit> </trans-unit>
<trans-unit id="Voice messages" xml:space="preserve"> <trans-unit id="Voice messages" xml:space="preserve">
@@ -5777,7 +5750,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
</trans-unit> </trans-unit>
<trans-unit id="With encrypted files and media." xml:space="preserve"> <trans-unit id="With encrypted files and media." xml:space="preserve">
<source>With encrypted files and media.</source> <source>With encrypted files and media.</source>
<target>Z zaszyfrowanymi plikami i multimediami.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="With optional welcome message." xml:space="preserve"> <trans-unit id="With optional welcome message." xml:space="preserve">
@@ -5787,7 +5759,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc
</trans-unit> </trans-unit>
<trans-unit id="With reduced battery usage." xml:space="preserve"> <trans-unit id="With reduced battery usage." xml:space="preserve">
<source>With reduced battery usage.</source> <source>With reduced battery usage.</source>
<target>Ze zmniejszonym zużyciem baterii.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Wrong database passphrase" xml:space="preserve"> <trans-unit id="Wrong database passphrase" xml:space="preserve">
@@ -6295,12 +6266,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit> </trans-unit>
<trans-unit id="blocked %@" xml:space="preserve"> <trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source> <source>blocked %@</source>
<target>zablokowany %@</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve"> <trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source> <source>blocked by admin</source>
<target>zablokowany przez admina</target>
<note>blocked chat item</note> <note>blocked chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="bold" xml:space="preserve"> <trans-unit id="bold" xml:space="preserve">
@@ -6425,7 +6394,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit> </trans-unit>
<trans-unit id="contact %@ changed to %@" xml:space="preserve"> <trans-unit id="contact %@ changed to %@" xml:space="preserve">
<source>contact %1$@ changed to %2$@</source> <source>contact %1$@ changed to %2$@</source>
<target>kontakt %1$@ zmieniony na %2$@</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="contact has e2e encryption" xml:space="preserve"> <trans-unit id="contact has e2e encryption" xml:space="preserve">
@@ -6700,7 +6668,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit> </trans-unit>
<trans-unit id="member %@ changed to %@" xml:space="preserve"> <trans-unit id="member %@ changed to %@" xml:space="preserve">
<source>member %1$@ changed to %2$@</source> <source>member %1$@ changed to %2$@</source>
<target>członek %1$@ zmieniony na %2$@</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="member connected" xml:space="preserve"> <trans-unit id="member connected" xml:space="preserve">
@@ -6827,12 +6794,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit> </trans-unit>
<trans-unit id="removed contact address" xml:space="preserve"> <trans-unit id="removed contact address" xml:space="preserve">
<source>removed contact address</source> <source>removed contact address</source>
<target>usunięto adres kontaktu</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="removed profile picture" xml:space="preserve"> <trans-unit id="removed profile picture" xml:space="preserve">
<source>removed profile picture</source> <source>removed profile picture</source>
<target>usunięto zdjęcie profilu</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="removed you" xml:space="preserve"> <trans-unit id="removed you" xml:space="preserve">
@@ -6867,12 +6832,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit> </trans-unit>
<trans-unit id="set new contact address" xml:space="preserve"> <trans-unit id="set new contact address" xml:space="preserve">
<source>set new contact address</source> <source>set new contact address</source>
<target>ustaw nowy adres kontaktu</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="set new profile picture" xml:space="preserve"> <trans-unit id="set new profile picture" xml:space="preserve">
<source>set new profile picture</source> <source>set new profile picture</source>
<target>ustaw nowe zdjęcie profilu</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="starting…" xml:space="preserve"> <trans-unit id="starting…" xml:space="preserve">
@@ -6892,7 +6855,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit> </trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve"> <trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source> <source>unblocked %@</source>
<target>odblokowano %@</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="unknown" xml:space="preserve"> <trans-unit id="unknown" xml:space="preserve">
@@ -6902,7 +6864,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit> </trans-unit>
<trans-unit id="unknown status" xml:space="preserve"> <trans-unit id="unknown status" xml:space="preserve">
<source>unknown status</source> <source>unknown status</source>
<target>nieznany status</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="updated group profile" xml:space="preserve"> <trans-unit id="updated group profile" xml:space="preserve">
@@ -6912,7 +6873,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit> </trans-unit>
<trans-unit id="updated profile" xml:space="preserve"> <trans-unit id="updated profile" xml:space="preserve">
<source>updated profile</source> <source>updated profile</source>
<target>zaktualizowano profil</target>
<note>profile update event chat item</note> <note>profile update event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="v%@" xml:space="preserve"> <trans-unit id="v%@" xml:space="preserve">
@@ -6987,7 +6947,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit> </trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve"> <trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source> <source>you blocked %@</source>
<target>zablokowałeś %@</target>
<note>snd group event chat item</note> <note>snd group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="you changed address" xml:space="preserve"> <trans-unit id="you changed address" xml:space="preserve">
@@ -7032,7 +6991,6 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
</trans-unit> </trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve"> <trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source> <source>you unblocked %@</source>
<target>odblokowałeś %@</target>
<note>snd group event chat item</note> <note>snd group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="you: " xml:space="preserve"> <trans-unit id="you: " xml:space="preserve">

View File

@@ -219,7 +219,6 @@
</trans-unit> </trans-unit>
<trans-unit id="%lld messages blocked by admin" xml:space="preserve"> <trans-unit id="%lld messages blocked by admin" xml:space="preserve">
<source>%lld messages blocked by admin</source> <source>%lld messages blocked by admin</source>
<target>%lld сообщений заблокировано администратором</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="%lld messages marked deleted" xml:space="preserve"> <trans-unit id="%lld messages marked deleted" xml:space="preserve">
@@ -925,7 +924,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Block for all" xml:space="preserve"> <trans-unit id="Block for all" xml:space="preserve">
<source>Block for all</source> <source>Block for all</source>
<target>Заблокировать для всех</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block group members" xml:space="preserve"> <trans-unit id="Block group members" xml:space="preserve">
@@ -940,7 +938,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Block member for all?" xml:space="preserve"> <trans-unit id="Block member for all?" xml:space="preserve">
<source>Block member for all?</source> <source>Block member for all?</source>
<target>Заблокировать члена для всех?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Block member?" xml:space="preserve"> <trans-unit id="Block member?" xml:space="preserve">
@@ -950,7 +947,6 @@
</trans-unit> </trans-unit>
<trans-unit id="Blocked by admin" xml:space="preserve"> <trans-unit id="Blocked by admin" xml:space="preserve">
<source>Blocked by admin</source> <source>Blocked by admin</source>
<target>Заблокирован администратором</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
@@ -5400,7 +5396,6 @@ You will be prompted to complete authentication before this feature is enabled.<
</trans-unit> </trans-unit>
<trans-unit id="Unblock for all" xml:space="preserve"> <trans-unit id="Unblock for all" xml:space="preserve">
<source>Unblock for all</source> <source>Unblock for all</source>
<target>Разблокировать для всех</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock member" xml:space="preserve"> <trans-unit id="Unblock member" xml:space="preserve">
@@ -5410,7 +5405,6 @@ You will be prompted to complete authentication before this feature is enabled.<
</trans-unit> </trans-unit>
<trans-unit id="Unblock member for all?" xml:space="preserve"> <trans-unit id="Unblock member for all?" xml:space="preserve">
<source>Unblock member for all?</source> <source>Unblock member for all?</source>
<target>Разблокировать члена для всех?</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unblock member?" xml:space="preserve"> <trans-unit id="Unblock member?" xml:space="preserve">
@@ -6295,12 +6289,10 @@ SimpleX серверы не могут получить доступ к Ваше
</trans-unit> </trans-unit>
<trans-unit id="blocked %@" xml:space="preserve"> <trans-unit id="blocked %@" xml:space="preserve">
<source>blocked %@</source> <source>blocked %@</source>
<target>%@ заблокирован</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="blocked by admin" xml:space="preserve"> <trans-unit id="blocked by admin" xml:space="preserve">
<source>blocked by admin</source> <source>blocked by admin</source>
<target>заблокировано администратором</target>
<note>blocked chat item</note> <note>blocked chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="bold" xml:space="preserve"> <trans-unit id="bold" xml:space="preserve">
@@ -6892,7 +6884,6 @@ SimpleX серверы не могут получить доступ к Ваше
</trans-unit> </trans-unit>
<trans-unit id="unblocked %@" xml:space="preserve"> <trans-unit id="unblocked %@" xml:space="preserve">
<source>unblocked %@</source> <source>unblocked %@</source>
<target>%@ разблокирован</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="unknown" xml:space="preserve"> <trans-unit id="unknown" xml:space="preserve">
@@ -6987,7 +6978,6 @@ SimpleX серверы не могут получить доступ к Ваше
</trans-unit> </trans-unit>
<trans-unit id="you blocked %@" xml:space="preserve"> <trans-unit id="you blocked %@" xml:space="preserve">
<source>you blocked %@</source> <source>you blocked %@</source>
<target>Вы заблокировали %@</target>
<note>snd group event chat item</note> <note>snd group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="you changed address" xml:space="preserve"> <trans-unit id="you changed address" xml:space="preserve">
@@ -7032,7 +7022,6 @@ SimpleX серверы не могут получить доступ к Ваше
</trans-unit> </trans-unit>
<trans-unit id="you unblocked %@" xml:space="preserve"> <trans-unit id="you unblocked %@" xml:space="preserve">
<source>you unblocked %@</source> <source>you unblocked %@</source>
<target>Вы разблокировали %@</target>
<note>snd group event chat item</note> <note>snd group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="you: " xml:space="preserve"> <trans-unit id="you: " xml:space="preserve">

View File

@@ -14,17 +14,17 @@
</trans-unit> </trans-unit>
<trans-unit id=" " xml:space="preserve"> <trans-unit id=" " xml:space="preserve">
<source> </source> <source> </source>
<target> </target> <target> . </target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id=" " xml:space="preserve"> <trans-unit id=" " xml:space="preserve">
<source> </source> <source> </source>
<target> </target> <target> </target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id=" " xml:space="preserve"> <trans-unit id=" " xml:space="preserve">
<source> </source> <source> </source>
<target> </target> <target> . </target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id=" (" xml:space="preserve"> <trans-unit id=" (" xml:space="preserve">

View File

@@ -90,11 +90,6 @@
5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */; }; 5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */; };
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; }; 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; };
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA992827FD8800B3292C /* HowItWorks.swift */; }; 5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA992827FD8800B3292C /* HowItWorks.swift */; };
5CB1CE882B8259EB00963938 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE832B8259EB00963938 /* libgmpxx.a */; };
5CB1CE892B8259EB00963938 /* libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE842B8259EB00963938 /* libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE.a */; };
5CB1CE8A2B8259EB00963938 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE852B8259EB00963938 /* libffi.a */; };
5CB1CE8B2B8259EB00963938 /* libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE862B8259EB00963938 /* libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE-ghc9.6.3.a */; };
5CB1CE8C2B8259EB00963938 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB1CE872B8259EB00963938 /* libgmp.a */; };
5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2084E28DA4B4800D024EC /* RTCServers.swift */; }; 5CB2084F28DA4B4800D024EC /* RTCServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB2084E28DA4B4800D024EC /* RTCServers.swift */; };
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; }; 5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; };
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; }; 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; };
@@ -117,6 +112,11 @@
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; }; 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; }; 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; };
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
5CCD2C462B5C800E00F76440 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C412B5C800E00F76440 /* libgmpxx.a */; };
5CCD2C472B5C800E00F76440 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C422B5C800E00F76440 /* libgmp.a */; };
5CCD2C482B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C432B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a */; };
5CCD2C492B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C442B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a */; };
5CCD2C4A2B5C800E00F76440 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CCD2C452B5C800E00F76440 /* libffi.a */; };
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; }; 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; }; 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
@@ -372,11 +372,6 @@
5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXInfo.swift; sourceTree = "<group>"; }; 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXInfo.swift; sourceTree = "<group>"; };
5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = "<group>"; }; 5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = "<group>"; };
5CB0BA992827FD8800B3292C /* HowItWorks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = "<group>"; }; 5CB0BA992827FD8800B3292C /* HowItWorks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HowItWorks.swift; sourceTree = "<group>"; };
5CB1CE832B8259EB00963938 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CB1CE842B8259EB00963938 /* libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE.a"; sourceTree = "<group>"; };
5CB1CE852B8259EB00963938 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CB1CE862B8259EB00963938 /* libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE-ghc9.6.3.a"; sourceTree = "<group>"; };
5CB1CE872B8259EB00963938 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CB2084E28DA4B4800D024EC /* RTCServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCServers.swift; sourceTree = "<group>"; }; 5CB2084E28DA4B4800D024EC /* RTCServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTCServers.swift; sourceTree = "<group>"; };
5CB2085428DE647400D024EC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; }; 5CB2085428DE647400D024EC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; }; 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; };
@@ -403,6 +398,11 @@
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; 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>"; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; };
5CCD2C412B5C800E00F76440 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CCD2C422B5C800E00F76440 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CCD2C432B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a"; sourceTree = "<group>"; };
5CCD2C442B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a"; sourceTree = "<group>"; };
5CCD2C452B5C800E00F76440 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = "<group>"; };
5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = "<group>"; }; 5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = "<group>"; };
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -514,13 +514,13 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5CB1CE882B8259EB00963938 /* libgmpxx.a in Frameworks */, 5CCD2C462B5C800E00F76440 /* libgmpxx.a in Frameworks */,
5CB1CE8C2B8259EB00963938 /* libgmp.a in Frameworks */, 5CCD2C492B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a in Frameworks */,
5CCD2C482B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5CCD2C4A2B5C800E00F76440 /* libffi.a in Frameworks */,
5CCD2C472B5C800E00F76440 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5CB1CE8A2B8259EB00963938 /* libffi.a in Frameworks */,
5CB1CE892B8259EB00963938 /* libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE.a in Frameworks */,
5CB1CE8B2B8259EB00963938 /* libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE-ghc9.6.3.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -582,11 +582,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5CB1CE852B8259EB00963938 /* libffi.a */, 5CCD2C452B5C800E00F76440 /* libffi.a */,
5CB1CE872B8259EB00963938 /* libgmp.a */, 5CCD2C422B5C800E00F76440 /* libgmp.a */,
5CB1CE832B8259EB00963938 /* libgmpxx.a */, 5CCD2C412B5C800E00F76440 /* libgmpxx.a */,
5CB1CE862B8259EB00963938 /* libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE-ghc9.6.3.a */, 5CCD2C432B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl-ghc9.6.3.a */,
5CB1CE842B8259EB00963938 /* libHSsimplex-chat-5.5.3.0-1R6yZC1upSP6aXGrPWvhZE.a */, 5CCD2C442B5C800E00F76440 /* libHSsimplex-chat-5.5.0.3-3tzCJyUgrnK8CcHdWjtxcl.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1509,7 +1509,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199; CURRENT_PROJECT_VERSION = 191;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1531,7 +1531,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 5.5.4; MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1552,7 +1552,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199; CURRENT_PROJECT_VERSION = 191;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1574,7 +1574,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 5.5.4; MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1633,7 +1633,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199; CURRENT_PROJECT_VERSION = 191;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1646,7 +1646,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 5.5.4; MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1665,7 +1665,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199; CURRENT_PROJECT_VERSION = 191;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -1678,7 +1678,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 5.5.4; MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1697,7 +1697,7 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199; CURRENT_PROJECT_VERSION = 191;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1721,7 +1721,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
MARKETING_VERSION = 5.5.4; MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1743,7 +1743,7 @@
APPLICATION_EXTENSION_API_ONLY = YES; APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 199; CURRENT_PROJECT_VERSION = 191;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1767,7 +1767,7 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
MARKETING_VERSION = 5.5.4; MARKETING_VERSION = 5.5;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;

View File

@@ -105,11 +105,6 @@ public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
return nil return nil
} }
public func chatJsonLength(_ s: String) -> Int {
var c = s.cString(using: .utf8)!
return Int(chat_json_length(&c))
}
struct ParsedMarkdown: Decodable { struct ParsedMarkdown: Decodable {
var formattedText: [FormattedText]? var formattedText: [FormattedText]?
} }

View File

@@ -613,7 +613,7 @@ public enum ChatResponse: Decodable, Error {
case callEnded(user: UserRef, contact: Contact) case callEnded(user: UserRef, contact: Contact)
case callInvitations(callInvitations: [RcvCallInvitation]) case callInvitations(callInvitations: [RcvCallInvitation])
case ntfTokenStatus(status: NtfTknStatus) case ntfTokenStatus(status: NtfTknStatus)
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String) case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo) case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo)
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
@@ -912,7 +912,7 @@ public enum ChatResponse: Decodable, Error {
case let .callEnded(u, contact): return withUser(u, "contact: \(contact.id)") case let .callEnded(u, contact): return withUser(u, "contact: \(contact.id)")
case let .callInvitations(invs): return String(describing: invs) case let .callInvitations(invs): return String(describing: invs)
case let .ntfTokenStatus(status): return String(describing: status) case let .ntfTokenStatus(status): return String(describing: status)
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)" case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))") case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))") case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))

View File

@@ -1367,17 +1367,6 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat {
} }
} }
public var chatTs: Date {
switch self {
case let .direct(contact): return contact.chatTs ?? contact.updatedAt
case let .group(groupInfo): return groupInfo.chatTs ?? groupInfo.updatedAt
case let .local(noteFolder): return noteFolder.chatTs
case let .contactRequest(contactRequest): return contactRequest.updatedAt
case let .contactConnection(contactConnection): return contactConnection.updatedAt
case .invalidJSON: return .now
}
}
public struct SampleData { public struct SampleData {
public var direct: ChatInfo public var direct: ChatInfo
public var group: ChatInfo public var group: ChatInfo
@@ -1436,7 +1425,6 @@ public struct Contact: Identifiable, Decodable, NamedChat {
public var mergedPreferences: ContactUserPreferences public var mergedPreferences: ContactUserPreferences
var createdAt: Date var createdAt: Date
var updatedAt: Date var updatedAt: Date
var chatTs: Date?
var contactGroupMemberId: Int64? var contactGroupMemberId: Int64?
var contactGrpInvSent: Bool var contactGrpInvSent: Bool
@@ -1756,7 +1744,6 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat {
public var chatSettings: ChatSettings public var chatSettings: ChatSettings
var createdAt: Date var createdAt: Date
var updatedAt: Date var updatedAt: Date
var chatTs: Date?
public var id: ChatId { get { "#\(groupId)" } } public var id: ChatId { get { "#\(groupId)" } }
public var apiId: Int64 { get { groupId } } public var apiId: Int64 { get { groupId } }
@@ -2062,7 +2049,6 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
public var unread: Bool public var unread: Bool
var createdAt: Date var createdAt: Date
public var updatedAt: Date public var updatedAt: Date
var chatTs: Date
public var id: ChatId { get { "*\(noteFolderId)" } } public var id: ChatId { get { "*\(noteFolderId)" } }
public var apiId: Int64 { get { noteFolderId } } public var apiId: Int64 { get { noteFolderId } }
@@ -2084,8 +2070,7 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat {
favorite: false, favorite: false,
unread: false, unread: false,
createdAt: .now, createdAt: .now,
updatedAt: .now, updatedAt: .now
chatTs: .now
) )
} }

View File

@@ -25,7 +25,6 @@ extern char *chat_parse_markdown(char *str);
extern char *chat_parse_server(char *str); extern char *chat_parse_server(char *str);
extern char *chat_password_hash(char *pwd, char *salt); extern char *chat_password_hash(char *pwd, char *salt);
extern char *chat_valid_name(char *name); extern char *chat_valid_name(char *name);
extern int chat_json_length(char *str);
extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len); extern char *chat_encrypt_media(chat_ctrl ctl, char *key, char *frame, int len);
extern char *chat_decrypt_media(char *key, char *frame, int len); extern char *chat_decrypt_media(char *key, char *frame, int len);

View File

@@ -202,9 +202,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"%lld messages blocked" = "%lld Nachrichten blockiert"; "%lld messages blocked" = "%lld Nachrichten blockiert";
/* No comment provided by engineer. */
"%lld messages blocked by admin" = "%lld Nachrichten wurden vom Administrator blockiert";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"%lld messages marked deleted" = "%lld Nachrichten als gelöscht markiert"; "%lld messages marked deleted" = "%lld Nachrichten als gelöscht markiert";
@@ -401,14 +398,11 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden."; "All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden.";
/* No comment provided by engineer. */
"All messages will be deleted - this cannot be undone!" = "Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht."; "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"All new messages from %@ will be hidden!" = "Von %@ werden alle neuen Nachrichten ausgeblendet!"; "All new messages from %@ will be hidden!" = "Alle neuen Nachrichten von %@ werden verborgen!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"All your contacts will remain connected." = "Alle Ihre Kontakte bleiben verbunden."; "All your contacts will remain connected." = "Alle Ihre Kontakte bleiben verbunden.";
@@ -587,32 +581,17 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block" = "Blockieren"; "Block" = "Blockieren";
/* No comment provided by engineer. */
"Block for all" = "Für Alle blockieren";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block group members" = "Gruppenmitglieder blockieren"; "Block group members" = "Gruppenmitglieder blockieren";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block member" = "Mitglied blockieren"; "Block member" = "Mitglied blockieren";
/* No comment provided by engineer. */
"Block member for all?" = "Mitglied für Alle blockieren?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block member?" = "Mitglied blockieren?"; "Block member?" = "Mitglied blockieren?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"blocked" = "Blockiert"; "blocked" = "blockiert";
/* rcv group event chat item */
"blocked %@" = "%@ wurde blockiert";
/* blocked chat item */
"blocked by admin" = "wurde vom Administrator blockiert";
/* No comment provided by engineer. */
"Blocked by admin" = "wurde vom Administrator blockiert";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"bold" = "fett"; "bold" = "fett";
@@ -771,9 +750,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Clear conversation?" = "Unterhaltung löschen?"; "Clear conversation?" = "Unterhaltung löschen?";
/* No comment provided by engineer. */
"Clear private notes?" = "Private Notizen löschen?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Clear verification" = "Überprüfung zurücknehmen"; "Clear verification" = "Überprüfung zurücknehmen";
@@ -912,9 +888,6 @@
/* connection information */ /* connection information */
"connection:%@" = "Verbindung:%@"; "connection:%@" = "Verbindung:%@";
/* profile update event chat item */
"contact %@ changed to %@" = "Der Kontaktname %1$@ wurde auf %2$@ geändert";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Contact allows" = "Der Kontakt erlaubt"; "Contact allows" = "Der Kontakt erlaubt";
@@ -999,12 +972,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Create your profile" = "Erstellen Sie Ihr Profil"; "Create your profile" = "Erstellen Sie Ihr Profil";
/* No comment provided by engineer. */
"Created at" = "Erstellt um";
/* copied message info */
"Created at: %@" = "Erstellt um: %@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Created on %@" = "Erstellt am %@"; "Created on %@" = "Erstellt am %@";
@@ -1557,9 +1524,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Error creating member contact" = "Fehler beim Anlegen eines Mitglied-Kontaktes"; "Error creating member contact" = "Fehler beim Anlegen eines Mitglied-Kontaktes";
/* No comment provided by engineer. */
"Error creating message" = "Fehler beim Erstellen der Nachricht";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Error creating profile!" = "Fehler beim Erstellen des Profils!"; "Error creating profile!" = "Fehler beim Erstellen des Profils!";
@@ -1974,9 +1938,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Import database" = "Datenbank importieren"; "Import database" = "Datenbank importieren";
/* No comment provided by engineer. */
"Improved message delivery" = "Verbesserte Zustellung von Nachrichten";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Improved privacy and security" = "Verbesserte Privatsphäre und Sicherheit"; "Improved privacy and security" = "Verbesserte Privatsphäre und Sicherheit";
@@ -2154,9 +2115,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Join group" = "Treten Sie der Gruppe bei"; "Join group" = "Treten Sie der Gruppe bei";
/* No comment provided by engineer. */
"Join group conversations" = "Gruppenunterhaltungen beitreten";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Join group?" = "Der Gruppe beitreten?"; "Join group?" = "Der Gruppe beitreten?";
@@ -2292,9 +2250,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Member" = "Mitglied"; "Member" = "Mitglied";
/* profile update event chat item */
"member %@ changed to %@" = "Der Mitgliedsname %1$@ wurde auf %2$@ geändert";
/* rcv group event chat item */ /* rcv group event chat item */
"member connected" = "ist der Gruppe beigetreten"; "member connected" = "ist der Gruppe beigetreten";
@@ -2639,18 +2594,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Password to show" = "Passwort anzeigen"; "Password to show" = "Passwort anzeigen";
/* past/unknown group member */
"Past member %@" = "Ehemaliges Mitglied %@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Paste desktop address" = "Desktop-Adresse einfügen"; "Paste desktop address" = "Desktop-Adresse einfügen";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Paste image" = "Bild einfügen"; "Paste image" = "Bild einfügen";
/* No comment provided by engineer. */
"Paste link to connect!" = "Zum Verbinden den Link einfügen!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Paste the link you received" = "Fügen Sie den erhaltenen Link ein"; "Paste the link you received" = "Fügen Sie den erhaltenen Link ein";
@@ -2738,9 +2687,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Private filenames" = "Neutrale Dateinamen"; "Private filenames" = "Neutrale Dateinamen";
/* name of notes to self */
"Private notes" = "Private Notizen";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Profile and server connections" = "Profil und Serververbindungen"; "Profile and server connections" = "Profil und Serververbindungen";
@@ -2855,9 +2801,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Receiving via" = "Empfangen über"; "Receiving via" = "Empfangen über";
/* No comment provided by engineer. */
"Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." = "Aktueller Nachrichtenverlauf und verbesserter [Gruppenverzeichnis-Bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Recipients see updates as you type them." = "Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben."; "Recipients see updates as you type them." = "Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben.";
@@ -2912,12 +2855,6 @@
/* rcv group event chat item */ /* rcv group event chat item */
"removed %@" = "hat %@ aus der Gruppe entfernt"; "removed %@" = "hat %@ aus der Gruppe entfernt";
/* profile update event chat item */
"removed contact address" = "Kontaktadresse wurde entfernt";
/* profile update event chat item */
"removed profile picture" = "Profil-Bild wurde entfernt";
/* rcv group event chat item */ /* rcv group event chat item */
"removed you" = "hat Sie aus der Gruppe entfernt"; "removed you" = "hat Sie aus der Gruppe entfernt";
@@ -3041,9 +2978,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Save welcome message?" = "Begrüßungsmeldung speichern?"; "Save welcome message?" = "Begrüßungsmeldung speichern?";
/* message info title */
"Saved message" = "Gespeicherte Nachricht";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Saved WebRTC ICE servers will be removed" = "Gespeicherte WebRTC ICE-Server werden entfernt"; "Saved WebRTC ICE servers will be removed" = "Gespeicherte WebRTC ICE-Server werden entfernt";
@@ -3065,9 +2999,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Search" = "Suche"; "Search" = "Suche";
/* No comment provided by engineer. */
"Search bar accepts invitation links." = "Von der Suchleiste werden Einladungslinks akzeptiert.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Search or paste SimpleX link" = "Suchen oder fügen Sie den SimpleX-Link ein"; "Search or paste SimpleX link" = "Suchen oder fügen Sie den SimpleX-Link ein";
@@ -3224,12 +3155,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Set it instead of system authentication." = "Anstelle der System-Authentifizierung festlegen."; "Set it instead of system authentication." = "Anstelle der System-Authentifizierung festlegen.";
/* profile update event chat item */
"set new contact address" = "Neue Kontaktadresse wurde festgelegt";
/* profile update event chat item */
"set new profile picture" = "Neues Profil-Bild wurde festgelegt";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Set passcode" = "Zugangscode einstellen"; "Set passcode" = "Zugangscode einstellen";
@@ -3599,9 +3524,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Trying to connect to the server used to receive messages from this contact." = "Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird."; "Trying to connect to the server used to receive messages from this contact." = "Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.";
/* No comment provided by engineer. */
"Turkish interface" = "Türkische Bedienoberfläche";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Turn off" = "Abschalten"; "Turn off" = "Abschalten";
@@ -3614,21 +3536,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock" = "Freigeben"; "Unblock" = "Freigeben";
/* No comment provided by engineer. */
"Unblock for all" = "Für Alle freigeben";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock member" = "Mitglied freigeben"; "Unblock member" = "Mitglied freigeben";
/* No comment provided by engineer. */
"Unblock member for all?" = "Mitglied für Alle freigeben?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock member?" = "Mitglied freigeben?"; "Unblock member?" = "Mitglied freigeben?";
/* rcv group event chat item */
"unblocked %@" = "%@ wurde freigegeben";
/* item status description */ /* item status description */
"Unexpected error: %@" = "Unerwarteter Fehler: %@"; "Unexpected error: %@" = "Unerwarteter Fehler: %@";
@@ -3662,9 +3575,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unknown error" = "Unbekannter Fehler"; "Unknown error" = "Unbekannter Fehler";
/* No comment provided by engineer. */
"unknown status" = "unbekannter Gruppenmitglieds-Status";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Aktivieren Sie den Modus \"Bitte nicht stören\", um Unterbrechungen zu vermeiden, es sei denn, Sie verwenden die iOS Anrufschnittstelle."; "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Aktivieren Sie den Modus \"Bitte nicht stören\", um Unterbrechungen zu vermeiden, es sei denn, Sie verwenden die iOS Anrufschnittstelle.";
@@ -3710,9 +3620,6 @@
/* rcv group event chat item */ /* rcv group event chat item */
"updated group profile" = "Aktualisiertes Gruppenprofil"; "updated group profile" = "Aktualisiertes Gruppenprofil";
/* profile update event chat item */
"updated profile" = "Das Profil wurde aktualisiert";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Updating settings will re-connect the client to all servers." = "Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden."; "Updating settings will re-connect the client to all servers." = "Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden.";
@@ -3887,15 +3794,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden."; "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil auch für die Gruppen verwendet, für die Sie von diesem Kontakt eingeladen werden.";
/* No comment provided by engineer. */
"With encrypted files and media." = "Mit verschlüsselten Dateien und Medien.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"With optional welcome message." = "Mit optionaler Begrüßungsmeldung."; "With optional welcome message." = "Mit optionaler Begrüßungsmeldung.";
/* No comment provided by engineer. */
"With reduced battery usage." = "Mit reduziertem Akkuverbrauch.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Wrong database passphrase" = "Falsches Datenbank-Passwort"; "Wrong database passphrase" = "Falsches Datenbank-Passwort";
@@ -3956,9 +3857,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"you are observer" = "Sie sind Beobachter"; "you are observer" = "Sie sind Beobachter";
/* snd group event chat item */
"you blocked %@" = "Sie haben %@ blockiert";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You can accept calls from lock screen, without device and app authentication." = "Sie können Anrufe ohne Geräte- und App-Authentifizierung vom Sperrbildschirm aus annehmen."; "You can accept calls from lock screen, without device and app authentication." = "Sie können Anrufe ohne Geräte- und App-Authentifizierung vom Sperrbildschirm aus annehmen.";
@@ -4070,9 +3968,6 @@
/* chat list item description */ /* chat list item description */
"you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt"; "you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt";
/* snd group event chat item */
"you unblocked %@" = "Sie haben %@ freigegeben";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You will be connected to group when the group host's device is online, please wait or check later!" = "Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!"; "You will be connected to group when the group host's device is online, please wait or check later!" = "Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!";

View File

@@ -202,9 +202,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"%lld messages blocked" = "%lld messaggi bloccati"; "%lld messages blocked" = "%lld messaggi bloccati";
/* No comment provided by engineer. */
"%lld messages blocked by admin" = "%lld messaggi bloccati dall'amministratore";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"%lld messages marked deleted" = "%lld messaggi contrassegnati eliminati"; "%lld messages marked deleted" = "%lld messaggi contrassegnati eliminati";
@@ -587,33 +584,18 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block" = "Blocca"; "Block" = "Blocca";
/* No comment provided by engineer. */
"Block for all" = "Blocca per tutti";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block group members" = "Blocca i membri dei gruppi"; "Block group members" = "Blocca i membri dei gruppi";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block member" = "Blocca membro"; "Block member" = "Blocca membro";
/* No comment provided by engineer. */
"Block member for all?" = "Bloccare il membro per tutti?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block member?" = "Bloccare il membro?"; "Block member?" = "Bloccare il membro?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"blocked" = "bloccato"; "blocked" = "bloccato";
/* rcv group event chat item */
"blocked %@" = "ha bloccato %@";
/* blocked chat item */
"blocked by admin" = "bloccato dall'amministratore";
/* No comment provided by engineer. */
"Blocked by admin" = "Bloccato dall'amministratore";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"bold" = "grassetto"; "bold" = "grassetto";
@@ -3614,21 +3596,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock" = "Sblocca"; "Unblock" = "Sblocca";
/* No comment provided by engineer. */
"Unblock for all" = "Sblocca per tutti";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock member" = "Sblocca membro"; "Unblock member" = "Sblocca membro";
/* No comment provided by engineer. */
"Unblock member for all?" = "Sbloccare il membro per tutti?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock member?" = "Sbloccare il membro?"; "Unblock member?" = "Sbloccare il membro?";
/* rcv group event chat item */
"unblocked %@" = "ha sbloccato %@";
/* item status description */ /* item status description */
"Unexpected error: %@" = "Errore imprevisto: % @"; "Unexpected error: %@" = "Errore imprevisto: % @";
@@ -3956,9 +3929,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"you are observer" = "sei un osservatore"; "you are observer" = "sei un osservatore";
/* snd group event chat item */
"you blocked %@" = "hai bloccato %@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You can accept calls from lock screen, without device and app authentication." = "Puoi accettare chiamate dalla schermata di blocco, senza l'autenticazione del dispositivo e dell'app."; "You can accept calls from lock screen, without device and app authentication." = "Puoi accettare chiamate dalla schermata di blocco, senza l'autenticazione del dispositivo e dell'app.";
@@ -4070,9 +4040,6 @@
/* chat list item description */ /* chat list item description */
"you shared one-time link incognito" = "hai condiviso un link incognito una tantum"; "you shared one-time link incognito" = "hai condiviso un link incognito una tantum";
/* snd group event chat item */
"you unblocked %@" = "hai sbloccato %@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You will be connected to group when the group host's device is online, please wait or check later!" = "Verrai connesso/a al gruppo quando il dispositivo dell'host del gruppo sarà in linea, attendi o controlla più tardi!"; "You will be connected to group when the group host's device is online, please wait or check later!" = "Verrai connesso/a al gruppo quando il dispositivo dell'host del gruppo sarà in linea, attendi o controlla più tardi!";

View File

@@ -202,9 +202,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"%lld messages blocked" = "%lld berichten geblokkeerd"; "%lld messages blocked" = "%lld berichten geblokkeerd";
/* No comment provided by engineer. */
"%lld messages blocked by admin" = "%lld berichten geblokkeerd door beheerder";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"%lld messages marked deleted" = "%lld berichten gemarkeerd als verwijderd"; "%lld messages marked deleted" = "%lld berichten gemarkeerd als verwijderd";
@@ -587,33 +584,18 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block" = "Blokkeren"; "Block" = "Blokkeren";
/* No comment provided by engineer. */
"Block for all" = "Blokkeren voor iedereen";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block group members" = "Groepsleden blokkeren"; "Block group members" = "Groepsleden blokkeren";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block member" = "Lid blokkeren"; "Block member" = "Lid blokkeren";
/* No comment provided by engineer. */
"Block member for all?" = "Lid voor iedereen blokkeren?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block member?" = "Lid blokkeren?"; "Block member?" = "Lid blokkeren?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"blocked" = "geblokkeerd"; "blocked" = "geblokkeerd";
/* rcv group event chat item */
"blocked %@" = "geblokkeerd %@";
/* blocked chat item */
"blocked by admin" = "geblokkeerd door beheerder";
/* No comment provided by engineer. */
"Blocked by admin" = "Geblokkeerd door beheerder";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"bold" = "vetgedrukt"; "bold" = "vetgedrukt";
@@ -2155,7 +2137,7 @@
"Join group" = "Word lid van groep"; "Join group" = "Word lid van groep";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Join group conversations" = "Neem deel aan groepsgesprekken"; "Join group conversations" = "Neem deel aan groep gesprekken";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Join group?" = "Deelnemen aan groep?"; "Join group?" = "Deelnemen aan groep?";
@@ -2649,7 +2631,7 @@
"Paste image" = "Afbeelding plakken"; "Paste image" = "Afbeelding plakken";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Paste link to connect!" = "Plak een link om te verbinden!"; "Paste link to connect!" = "Plak link om te verbinden!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Paste the link you received" = "Plak de link die je hebt ontvangen"; "Paste the link you received" = "Plak de link die je hebt ontvangen";
@@ -3069,7 +3051,7 @@
"Search bar accepts invitation links." = "Zoekbalk accepteert uitnodigingslinks."; "Search bar accepts invitation links." = "Zoekbalk accepteert uitnodigingslinks.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Search or paste SimpleX link" = "Zoek of plak een SimpleX link"; "Search or paste SimpleX link" = "Zoek of plak de SimpleX link";
/* network option */ /* network option */
"sec" = "sec"; "sec" = "sec";
@@ -3614,21 +3596,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock" = "Deblokkeren"; "Unblock" = "Deblokkeren";
/* No comment provided by engineer. */
"Unblock for all" = "Deblokkeer voor iedereen";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock member" = "Lid deblokkeren"; "Unblock member" = "Lid deblokkeren";
/* No comment provided by engineer. */
"Unblock member for all?" = "Lid voor iedereen deblokkeren?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock member?" = "Lid deblokkeren?"; "Unblock member?" = "Lid deblokkeren?";
/* rcv group event chat item */
"unblocked %@" = "gedeblokkeerd %@";
/* item status description */ /* item status description */
"Unexpected error: %@" = "Onverwachte fout: %@"; "Unexpected error: %@" = "Onverwachte fout: %@";
@@ -3956,9 +3929,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"you are observer" = "jij bent waarnemer"; "you are observer" = "jij bent waarnemer";
/* snd group event chat item */
"you blocked %@" = "je hebt %@ geblokkeerd";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You can accept calls from lock screen, without device and app authentication." = "U kunt oproepen van het vergrendelingsscherm accepteren, zonder apparaat- en app-verificatie."; "You can accept calls from lock screen, without device and app authentication." = "U kunt oproepen van het vergrendelingsscherm accepteren, zonder apparaat- en app-verificatie.";
@@ -4070,9 +4040,6 @@
/* chat list item description */ /* chat list item description */
"you shared one-time link incognito" = "je hebt een eenmalige link incognito gedeeld"; "you shared one-time link incognito" = "je hebt een eenmalige link incognito gedeeld";
/* snd group event chat item */
"you unblocked %@" = "je hebt %@ gedeblokkeerd";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You will be connected to group when the group host's device is online, please wait or check later!" = "Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later!"; "You will be connected to group when the group host's device is online, please wait or check later!" = "Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later!";

View File

@@ -202,9 +202,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"%lld messages blocked" = "%lld wiadomości zablokowanych"; "%lld messages blocked" = "%lld wiadomości zablokowanych";
/* No comment provided by engineer. */
"%lld messages blocked by admin" = "%lld wiadomości zablokowanych przez admina";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"%lld messages marked deleted" = "%lld wiadomości oznaczonych do usunięcia"; "%lld messages marked deleted" = "%lld wiadomości oznaczonych do usunięcia";
@@ -401,9 +398,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"All group members will remain connected." = "Wszyscy członkowie grupy pozostaną połączeni."; "All group members will remain connected." = "Wszyscy członkowie grupy pozostaną połączeni.";
/* No comment provided by engineer. */
"All messages will be deleted - this cannot be undone!" = "Wszystkie wiadomości zostaną usunięte nie można tego cofnąć!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Wiadomości zostaną usunięte TYLKO dla Ciebie."; "All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Wszystkie wiadomości zostaną usunięte - nie można tego cofnąć! Wiadomości zostaną usunięte TYLKO dla Ciebie.";
@@ -587,33 +581,18 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block" = "Zablokuj"; "Block" = "Zablokuj";
/* No comment provided by engineer. */
"Block for all" = "Zablokuj dla wszystkich";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block group members" = "Blokuj członków grupy"; "Block group members" = "Blokuj członków grupy";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block member" = "Zablokuj członka"; "Block member" = "Zablokuj członka";
/* No comment provided by engineer. */
"Block member for all?" = "Zablokować członka dla wszystkich?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block member?" = "Zablokować członka?"; "Block member?" = "Zablokować członka?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"blocked" = "zablokowany"; "blocked" = "zablokowany";
/* rcv group event chat item */
"blocked %@" = "zablokowany %@";
/* blocked chat item */
"blocked by admin" = "zablokowany przez admina";
/* No comment provided by engineer. */
"Blocked by admin" = "Zablokowany przez admina";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"bold" = "pogrubiona"; "bold" = "pogrubiona";
@@ -771,9 +750,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Clear conversation?" = "Wyczyścić rozmowę?"; "Clear conversation?" = "Wyczyścić rozmowę?";
/* No comment provided by engineer. */
"Clear private notes?" = "Wyczyścić prywatne notatki?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Clear verification" = "Wyczyść weryfikację"; "Clear verification" = "Wyczyść weryfikację";
@@ -912,9 +888,6 @@
/* connection information */ /* connection information */
"connection:%@" = "połączenie: %@"; "connection:%@" = "połączenie: %@";
/* profile update event chat item */
"contact %@ changed to %@" = "kontakt %1$@ zmieniony na %2$@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Contact allows" = "Kontakt pozwala"; "Contact allows" = "Kontakt pozwala";
@@ -999,12 +972,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Create your profile" = "Utwórz swój profil"; "Create your profile" = "Utwórz swój profil";
/* No comment provided by engineer. */
"Created at" = "Utworzony o";
/* copied message info */
"Created at: %@" = "Utworzony o: %@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Created on %@" = "Utworzony w dniu %@"; "Created on %@" = "Utworzony w dniu %@";
@@ -1329,9 +1296,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Do it later" = "Zrób to później"; "Do it later" = "Zrób to później";
/* No comment provided by engineer. */
"Do not send history to new members." = "Nie wysyłaj historii do nowych członków.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Do NOT use SimpleX for emergency calls." = "NIE używaj SimpleX do połączeń alarmowych."; "Do NOT use SimpleX for emergency calls." = "NIE używaj SimpleX do połączeń alarmowych.";
@@ -1557,9 +1521,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Error creating member contact" = "Błąd tworzenia kontaktu członka"; "Error creating member contact" = "Błąd tworzenia kontaktu członka";
/* No comment provided by engineer. */
"Error creating message" = "Błąd tworzenia wiadomości";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Error creating profile!" = "Błąd tworzenia profilu!"; "Error creating profile!" = "Błąd tworzenia profilu!";
@@ -1914,9 +1875,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"History" = "Historia"; "History" = "Historia";
/* No comment provided by engineer. */
"History is not sent to new members." = "Historia nie jest wysyłana do nowych członków.";
/* time unit */ /* time unit */
"hours" = "godziny"; "hours" = "godziny";
@@ -1974,9 +1932,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Import database" = "Importuj bazę danych"; "Import database" = "Importuj bazę danych";
/* No comment provided by engineer. */
"Improved message delivery" = "Ulepszona dostawa wiadomości";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Improved privacy and security" = "Zwiększona prywatność i bezpieczeństwo"; "Improved privacy and security" = "Zwiększona prywatność i bezpieczeństwo";
@@ -2061,9 +2016,6 @@
/* invalid chat item */ /* invalid chat item */
"invalid data" = "nieprawidłowe dane"; "invalid data" = "nieprawidłowe dane";
/* No comment provided by engineer. */
"Invalid display name!" = "Nieprawidłowa nazwa wyświetlana!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Invalid link" = "Nieprawidłowy link"; "Invalid link" = "Nieprawidłowy link";
@@ -2154,9 +2106,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Join group" = "Dołącz do grupy"; "Join group" = "Dołącz do grupy";
/* No comment provided by engineer. */
"Join group conversations" = "Dołącz do grupowej rozmowy";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Join group?" = "Dołączyć do grupy?"; "Join group?" = "Dołączyć do grupy?";
@@ -2292,9 +2241,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Member" = "Członek"; "Member" = "Członek";
/* profile update event chat item */
"member %@ changed to %@" = "członek %1$@ zmieniony na %2$@";
/* rcv group event chat item */ /* rcv group event chat item */
"member connected" = "połączony"; "member connected" = "połączony";
@@ -2639,18 +2585,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Password to show" = "Hasło do wyświetlenia"; "Password to show" = "Hasło do wyświetlenia";
/* past/unknown group member */
"Past member %@" = "Były członek %@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Paste desktop address" = "Wklej adres komputera"; "Paste desktop address" = "Wklej adres komputera";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Paste image" = "Wklej obraz"; "Paste image" = "Wklej obraz";
/* No comment provided by engineer. */
"Paste link to connect!" = "Wklej link, aby połączyć!";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Paste the link you received" = "Wklej link, który otrzymałeś"; "Paste the link you received" = "Wklej link, który otrzymałeś";
@@ -2738,9 +2678,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Private filenames" = "Prywatne nazwy plików"; "Private filenames" = "Prywatne nazwy plików";
/* name of notes to self */
"Private notes" = "Prywatne notatki";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Profile and server connections" = "Profil i połączenia z serwerem"; "Profile and server connections" = "Profil i połączenia z serwerem";
@@ -2855,9 +2792,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Receiving via" = "Odbieranie przez"; "Receiving via" = "Odbieranie przez";
/* No comment provided by engineer. */
"Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." = "Ostania historia i ulepszony [bot adresowy](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion).";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Recipients see updates as you type them." = "Odbiorcy widzą aktualizacje podczas ich wpisywania."; "Recipients see updates as you type them." = "Odbiorcy widzą aktualizacje podczas ich wpisywania.";
@@ -2912,12 +2846,6 @@
/* rcv group event chat item */ /* rcv group event chat item */
"removed %@" = "usunięto %@"; "removed %@" = "usunięto %@";
/* profile update event chat item */
"removed contact address" = "usunięto adres kontaktu";
/* profile update event chat item */
"removed profile picture" = "usunięto zdjęcie profilu";
/* rcv group event chat item */ /* rcv group event chat item */
"removed you" = "usunął cię"; "removed you" = "usunął cię";
@@ -3041,9 +2969,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Save welcome message?" = "Zapisać wiadomość powitalną?"; "Save welcome message?" = "Zapisać wiadomość powitalną?";
/* message info title */
"Saved message" = "Zachowano wiadomość";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Saved WebRTC ICE servers will be removed" = "Zapisane serwery WebRTC ICE zostaną usunięte"; "Saved WebRTC ICE servers will be removed" = "Zapisane serwery WebRTC ICE zostaną usunięte";
@@ -3065,9 +2990,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Search" = "Szukaj"; "Search" = "Szukaj";
/* No comment provided by engineer. */
"Search bar accepts invitation links." = "Pasek wyszukiwania akceptuje linki zaproszenia.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Search or paste SimpleX link" = "Wyszukaj lub wklej link SimpleX"; "Search or paste SimpleX link" = "Wyszukaj lub wklej link SimpleX";
@@ -3149,9 +3071,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Send them from gallery or custom keyboards." = "Wyślij je z galerii lub niestandardowych klawiatur."; "Send them from gallery or custom keyboards." = "Wyślij je z galerii lub niestandardowych klawiatur.";
/* No comment provided by engineer. */
"Send up to 100 last messages to new members." = "Wysyłaj do 100 ostatnich wiadomości do nowych członków.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Sender cancelled file transfer." = "Nadawca anulował transfer pliku."; "Sender cancelled file transfer." = "Nadawca anulował transfer pliku.";
@@ -3224,12 +3143,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Set it instead of system authentication." = "Ustaw go zamiast uwierzytelniania systemowego."; "Set it instead of system authentication." = "Ustaw go zamiast uwierzytelniania systemowego.";
/* profile update event chat item */
"set new contact address" = "ustaw nowy adres kontaktu";
/* profile update event chat item */
"set new profile picture" = "ustaw nowe zdjęcie profilu";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Set passcode" = "Ustaw pin"; "Set passcode" = "Ustaw pin";
@@ -3536,9 +3449,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"This device name" = "Nazwa tego urządzenia"; "This device name" = "Nazwa tego urządzenia";
/* No comment provided by engineer. */
"This display name is invalid. Please choose another name." = "Nazwa wyświetlana jest nieprawidłowa. Proszę wybrać inną nazwę.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"This group has over %lld members, delivery receipts are not sent." = "Ta grupa ma ponad %lld członków, potwierdzenia dostawy nie są wysyłane."; "This group has over %lld members, delivery receipts are not sent." = "Ta grupa ma ponad %lld członków, potwierdzenia dostawy nie są wysyłane.";
@@ -3599,9 +3509,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Trying to connect to the server used to receive messages from this contact." = "Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu."; "Trying to connect to the server used to receive messages from this contact." = "Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu.";
/* No comment provided by engineer. */
"Turkish interface" = "Turecki interfejs";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Turn off" = "Wyłącz"; "Turn off" = "Wyłącz";
@@ -3614,21 +3521,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock" = "Odblokuj"; "Unblock" = "Odblokuj";
/* No comment provided by engineer. */
"Unblock for all" = "Odblokuj dla wszystkich";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock member" = "Odblokuj członka"; "Unblock member" = "Odblokuj członka";
/* No comment provided by engineer. */
"Unblock member for all?" = "Odblokować członka dla wszystkich?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock member?" = "Odblokować członka?"; "Unblock member?" = "Odblokować członka?";
/* rcv group event chat item */
"unblocked %@" = "odblokowano %@";
/* item status description */ /* item status description */
"Unexpected error: %@" = "Nieoczekiwany błąd: %@"; "Unexpected error: %@" = "Nieoczekiwany błąd: %@";
@@ -3662,9 +3560,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unknown error" = "Nieznany błąd"; "Unknown error" = "Nieznany błąd";
/* No comment provided by engineer. */
"unknown status" = "nieznany status";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "O ile nie korzystasz z interfejsu połączeń systemu iOS, włącz tryb Nie przeszkadzać, aby uniknąć przerywania."; "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "O ile nie korzystasz z interfejsu połączeń systemu iOS, włącz tryb Nie przeszkadzać, aby uniknąć przerywania.";
@@ -3689,9 +3584,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unread" = "Nieprzeczytane"; "Unread" = "Nieprzeczytane";
/* No comment provided by engineer. */
"Up to 100 last messages are sent to new members." = "Do nowych członków wysyłanych jest do 100 ostatnich wiadomości.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Update" = "Aktualizuj"; "Update" = "Aktualizuj";
@@ -3710,9 +3602,6 @@
/* rcv group event chat item */ /* rcv group event chat item */
"updated group profile" = "zaktualizowano profil grupy"; "updated group profile" = "zaktualizowano profil grupy";
/* profile update event chat item */
"updated profile" = "zaktualizowano profil";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Updating settings will re-connect the client to all servers." = "Aktualizacja ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami."; "Updating settings will re-connect the client to all servers." = "Aktualizacja ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami.";
@@ -3821,9 +3710,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"View security code" = "Pokaż kod bezpieczeństwa"; "View security code" = "Pokaż kod bezpieczeństwa";
/* chat feature */
"Visible history" = "Widoczna historia";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Voice message…" = "Wiadomość głosowa…"; "Voice message…" = "Wiadomość głosowa…";
@@ -3887,15 +3773,9 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Gdy udostępnisz komuś profil incognito, będzie on używany w grupach, do których Cię zaprosi."; "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Gdy udostępnisz komuś profil incognito, będzie on używany w grupach, do których Cię zaprosi.";
/* No comment provided by engineer. */
"With encrypted files and media." = "Z zaszyfrowanymi plikami i multimediami.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"With optional welcome message." = "Z opcjonalną wiadomością powitalną."; "With optional welcome message." = "Z opcjonalną wiadomością powitalną.";
/* No comment provided by engineer. */
"With reduced battery usage." = "Ze zmniejszonym zużyciem baterii.";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Wrong database passphrase" = "Nieprawidłowe hasło bazy danych"; "Wrong database passphrase" = "Nieprawidłowe hasło bazy danych";
@@ -3956,9 +3836,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"you are observer" = "jesteś obserwatorem"; "you are observer" = "jesteś obserwatorem";
/* snd group event chat item */
"you blocked %@" = "zablokowałeś %@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You can accept calls from lock screen, without device and app authentication." = "Możesz przyjmować połączenia z ekranu blokady, bez uwierzytelniania urządzenia i aplikacji."; "You can accept calls from lock screen, without device and app authentication." = "Możesz przyjmować połączenia z ekranu blokady, bez uwierzytelniania urządzenia i aplikacji.";
@@ -4070,9 +3947,6 @@
/* chat list item description */ /* chat list item description */
"you shared one-time link incognito" = "udostępniłeś jednorazowy link incognito"; "you shared one-time link incognito" = "udostępniłeś jednorazowy link incognito";
/* snd group event chat item */
"you unblocked %@" = "odblokowałeś %@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You will be connected to group when the group host's device is online, please wait or check later!" = "Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później!"; "You will be connected to group when the group host's device is online, please wait or check later!" = "Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później!";

View File

@@ -202,9 +202,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"%lld messages blocked" = "%lld сообщений заблокировано"; "%lld messages blocked" = "%lld сообщений заблокировано";
/* No comment provided by engineer. */
"%lld messages blocked by admin" = "%lld сообщений заблокировано администратором";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"%lld messages marked deleted" = "%lld сообщений помечено удалёнными"; "%lld messages marked deleted" = "%lld сообщений помечено удалёнными";
@@ -587,33 +584,18 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block" = "Заблокировать"; "Block" = "Заблокировать";
/* No comment provided by engineer. */
"Block for all" = "Заблокировать для всех";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block group members" = "Блокируйте членов группы"; "Block group members" = "Блокируйте членов группы";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block member" = "Заблокировать члена группы"; "Block member" = "Заблокировать члена группы";
/* No comment provided by engineer. */
"Block member for all?" = "Заблокировать члена для всех?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Block member?" = "Заблокировать члена группы?"; "Block member?" = "Заблокировать члена группы?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"blocked" = "заблокировано"; "blocked" = "заблокировано";
/* rcv group event chat item */
"blocked %@" = "%@ заблокирован";
/* blocked chat item */
"blocked by admin" = "заблокировано администратором";
/* No comment provided by engineer. */
"Blocked by admin" = "Заблокирован администратором";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"bold" = "жирный"; "bold" = "жирный";
@@ -3614,21 +3596,12 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock" = "Разблокировать"; "Unblock" = "Разблокировать";
/* No comment provided by engineer. */
"Unblock for all" = "Разблокировать для всех";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock member" = "Разблокировать члена группы"; "Unblock member" = "Разблокировать члена группы";
/* No comment provided by engineer. */
"Unblock member for all?" = "Разблокировать члена для всех?";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Unblock member?" = "Разблокировать члена группы?"; "Unblock member?" = "Разблокировать члена группы?";
/* rcv group event chat item */
"unblocked %@" = "%@ разблокирован";
/* item status description */ /* item status description */
"Unexpected error: %@" = "Неожиданная ошибка: %@"; "Unexpected error: %@" = "Неожиданная ошибка: %@";
@@ -3956,9 +3929,6 @@
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"you are observer" = "только чтение сообщений"; "you are observer" = "только чтение сообщений";
/* snd group event chat item */
"you blocked %@" = "Вы заблокировали %@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You can accept calls from lock screen, without device and app authentication." = "Вы можете принимать звонки на экране блокировки, без аутентификации."; "You can accept calls from lock screen, without device and app authentication." = "Вы можете принимать звонки на экране блокировки, без аутентификации.";
@@ -4070,9 +4040,6 @@
/* chat list item description */ /* chat list item description */
"you shared one-time link incognito" = "Вы создали ссылку инкогнито"; "you shared one-time link incognito" = "Вы создали ссылку инкогнито";
/* snd group event chat item */
"you unblocked %@" = "Вы разблокировали %@";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"You will be connected to group when the group host's device is online, please wait or check later!" = "Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!"; "You will be connected to group when the group host's device is online, please wait or check later!" = "Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!";

View File

@@ -2,13 +2,13 @@
"\n" = "\n"; "\n" = "\n";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
" " = " "; " " = " . ";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
" " = " "; " " = " ";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
" " = " "; " " = " . ";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
" (" = " ("; " (" = " (";

View File

@@ -103,14 +103,11 @@
</intent-filter> </intent-filter>
</activity-alias> </activity-alias>
<activity android:name=".views.call.CallActivity"
<activity android:name=".views.call.IncomingCallActivity"
android:showOnLockScreen="true" android:showOnLockScreen="true"
android:exported="false" android:exported="false"
android:launchMode="singleInstance" android:launchMode="singleTask"/>
android:supportsPictureInPicture="true"
android:autoRemoveFromRecents="true"
android:screenOrientation="portrait"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"/>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
@@ -136,18 +133,6 @@
android:stopWithTask="false"></service> android:stopWithTask="false"></service>
<!-- SimplexService restart on reboot --> <!-- SimplexService restart on reboot -->
<service
android:name=".CallService"
android:enabled="true"
android:exported="false"
android:stopWithTask="false"/>
<receiver
android:name=".CallService$CallActionReceiver"
android:enabled="true"
android:exported="false" />
<receiver <receiver
android:name=".SimplexService$StartReceiver" android:name=".SimplexService$StartReceiver"
android:enabled="true" android:enabled="true"

View File

@@ -1,176 +0,0 @@
package chat.simplex.app
import android.app.*
import android.content.*
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.*
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import chat.simplex.app.model.NtfManager.EndCallAction
import chat.simplex.app.views.call.CallActivity
import chat.simplex.common.model.NotificationPreviewMode
import chat.simplex.common.platform.*
import chat.simplex.common.views.call.CallState
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.datetime.Instant
class CallService: Service() {
private var wakeLock: PowerManager.WakeLock? = null
private var notificationManager: NotificationManager? = null
private var serviceNotification: Notification? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand startId: $startId")
if (intent != null) {
val action = intent.action
Log.d(TAG, "intent action $action")
when (action) {
Action.START.name -> startService()
else -> Log.e(TAG, "No action in the intent")
}
} else {
Log.d(TAG, "null intent. Probably restarted by the system.")
}
startForeground(CALL_SERVICE_ID, serviceNotification)
return START_STICKY
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Call service created")
notificationManager = createNotificationChannel()
updateNotification()
startForeground(CALL_SERVICE_ID, serviceNotification)
}
override fun onDestroy() {
Log.d(TAG, "Call service destroyed")
try {
wakeLock?.let {
while (it.isHeld) it.release() // release all, in case acquired more than once
}
wakeLock = null
} catch (e: Exception) {
Log.d(TAG, "Exception while releasing wakelock: ${e.message}")
}
super.onDestroy()
}
private fun startService() {
Log.d(TAG, "CallService startService")
if (wakeLock != null) return
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply {
acquire()
}
}
}
fun updateNotification() {
val call = chatModel.activeCall.value
val previewMode = appPreferences.notificationPreviewMode.get()
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
generalGetString(MR.strings.notification_preview_somebody)
else
call?.contact?.profile?.displayName ?: ""
val text = generalGetString(if (call?.supportsVideo() == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
val image = call?.contact?.image
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
BitmapFactory.decodeResource(resources, R.drawable.icon)
else
base64ToBitmap(image).asAndroidBitmap()
serviceNotification = createNotification(title, text, largeIcon, call?.connectedAt)
startForeground(CALL_SERVICE_ID, serviceNotification)
}
private fun createNotificationChannel(): NotificationManager? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID, CALL_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(channel)
return notificationManager
}
return null
}
private fun createNotification(title: String, text: String, icon: Bitmap, connectedAt: Instant? = null): Notification {
val pendingIntent: PendingIntent = Intent(this, CallActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
}
val endCallPendingIntent: PendingIntent = Intent(this, CallActionReceiver::class.java).let { notificationIntent ->
notificationIntent.setAction(EndCallAction)
PendingIntent.getBroadcast(this, 1, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
val builder = NotificationCompat.Builder(this, CALL_NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ntf_icon)
.setLargeIcon(icon.clipToCircle())
.setColor(0x88FFFF)
.setContentTitle(title)
.setContentText(text)
.setContentIntent(pendingIntent)
.setSilent(true)
.addAction(R.drawable.ntf_icon, generalGetString(MR.strings.call_service_notification_end_call), endCallPendingIntent)
if (connectedAt != null) {
builder.setUsesChronometer(true)
builder.setWhen(connectedAt.epochSeconds * 1000)
}
return builder.build()
}
override fun onBind(intent: Intent): IBinder {
return CallServiceBinder()
}
inner class CallServiceBinder : Binder() {
fun getService() = this@CallService
}
enum class Action {
START,
}
class CallActionReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
EndCallAction -> {
val call = chatModel.activeCall.value
if (call != null) {
withBGApi {
chatModel.callManager.endCall(call)
}
}
}
else -> {
Log.e(TAG, "Unknown action. Make sure you provided an action")
}
}
}
}
companion object {
const val TAG = "CALL_SERVICE"
const val CALL_NOTIFICATION_CHANNEL_ID = "chat.simplex.app.CALL_SERVICE_NOTIFICATION"
const val CALL_NOTIFICATION_CHANNEL_NAME = "SimpleX Chat call service"
const val CALL_SERVICE_ID = 6788
const val WAKE_LOCK_TAG = "CallService::lock"
fun startService(): Intent {
Log.d(TAG, "CallService start")
return Intent(androidAppContext, CallService::class.java).also {
it.action = Action.START.name
ContextCompat.startForegroundService(androidAppContext, it)
}
}
fun stopService() {
androidAppContext.stopService(Intent(androidAppContext, CallService::class.java))
}
}
}

View File

@@ -1,12 +1,11 @@
package chat.simplex.app package chat.simplex.app
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.ui.platform.ClipboardManager import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.getUserIdFromIntent import chat.simplex.app.model.NtfManager.getUserIdFromIntent
@@ -59,17 +58,6 @@ class MainActivity: FragmentActivity() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
AppLock.recheckAuthState() AppLock.recheckAuthState()
withApi {
delay(1000)
if (!isAppOnForeground) return@withApi
/**
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
* access to clipboard because the app considered in background.
* This will ensure that the app will get the event on resume
* */
val service = getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
chatModel.clipboardHasText.value = service.hasPrimaryClip()
}
} }
override fun onPause() { override fun onPause() {

View File

@@ -1,15 +1,14 @@
package chat.simplex.app package chat.simplex.app
import android.app.* import android.app.Application
import android.content.Context import android.content.Context
import androidx.compose.ui.platform.ClipboardManager
import chat.simplex.common.platform.Log import chat.simplex.common.platform.Log
import android.content.Intent import android.app.UiModeManager
import android.os.* import android.os.*
import androidx.lifecycle.* import androidx.lifecycle.*
import androidx.work.* import androidx.work.*
import chat.simplex.app.model.NtfManager 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.APPLICATION_ID import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.requiresIgnoringBattery import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.* import chat.simplex.common.model.*
@@ -19,7 +18,6 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.ui.theme.DefaultTheme import chat.simplex.common.ui.theme.DefaultTheme
import chat.simplex.common.views.call.RcvCallInvitation import chat.simplex.common.views.call.RcvCallInvitation
import chat.simplex.common.views.call.activeCallDestroyWebView
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.onboarding.OnboardingStage
import com.jakewharton.processphoenix.ProcessPhoenix import com.jakewharton.processphoenix.ProcessPhoenix
@@ -73,7 +71,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
Log.d(TAG, "onStateChanged: $event") Log.d(TAG, "onStateChanged: $event")
withLongRunningApi { withBGApi {
when (event) { when (event) {
Lifecycle.Event.ON_START -> { Lifecycle.Event.ON_START -> {
isAppOnForeground = true isAppOnForeground = true
@@ -99,6 +97,13 @@ class SimplexApp: Application(), LifecycleEventObserver {
} }
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {
isAppOnForeground = true isAppOnForeground = true
/**
* When the app calls [ClipboardManager.shareText] and a user copies text in clipboard, Android denies
* access to clipboard because the app considered in background.
* This will ensure that the app will get the event on resume
* */
val service = androidAppContext.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
chatModel.clipboardHasText.value = service.hasPrimaryClip()
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) { if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && chatModel.currentUser.value != null) {
SimplexService.showBackgroundServiceNoticeIfNeeded() SimplexService.showBackgroundServiceNoticeIfNeeded()
} }
@@ -186,28 +191,16 @@ class SimplexApp: Application(), LifecycleEventObserver {
SimplexService.safeStopService() SimplexService.safeStopService()
} }
override fun androidCallServiceSafeStop() {
CallService.stopService()
}
override fun androidNotificationsModeChanged(mode: NotificationsMode) { override fun androidNotificationsModeChanged(mode: NotificationsMode) {
if (mode.requiresIgnoringBattery && !SimplexService.isBackgroundAllowed()) { if (mode.requiresIgnoringBattery && !SimplexService.isBackgroundAllowed()) {
appPrefs.backgroundServiceNoticeShown.set(false) appPrefs.backgroundServiceNoticeShown.set(false)
} }
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE) SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
if (mode == NotificationsMode.SERVICE) { if (mode == NotificationsMode.SERVICE)
SimplexService.start() SimplexService.start()
// Sometimes, when we change modes fast from one to another, system destroys the service after start. else
// We can wait a little and restart the service, and it will work in 100% of cases
delay(2000)
if (!SimplexService.isServiceStarted && appPrefs.notificationsMode.get() == NotificationsMode.SERVICE) {
Log.i(TAG, "Service tried to start but destroyed by system, repeating once more")
SimplexService.start()
}
} else {
SimplexService.safeStopService() SimplexService.safeStopService()
}
} }
if (mode != NotificationsMode.PERIODIC) { if (mode != NotificationsMode.PERIODIC) {
@@ -260,28 +253,6 @@ class SimplexApp: Application(), LifecycleEventObserver {
uiModeManager.setApplicationNightMode(mode) uiModeManager.setApplicationNightMode(mode)
} }
override fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long?, chatId: ChatId?) {
val context = mainActivity.get() ?: return
val intent = Intent(context, CallActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
if (acceptCall) {
intent.setAction(AcceptCallAction)
.putExtra("remoteHostId", remoteHostId)
.putExtra("chatId", chatId)
}
intent.flags += Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT
context.startActivity(intent)
}
override fun androidPictureInPictureAllowed(): Boolean {
val appOps = androidAppContext.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
return appOps.checkOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, Process.myUid(), packageName) == AppOpsManager.MODE_ALLOWED
}
override fun androidCallEnded() {
activeCallDestroyWebView()
}
override suspend fun androidAskToAllowBackgroundCalls(): Boolean { override suspend fun androidAskToAllowBackgroundCalls(): Boolean {
if (SimplexService.isBackgroundRestricted()) { if (SimplexService.isBackgroundRestricted()) {
val userChoice: CompletableDeferred<Boolean> = CompletableDeferred() val userChoice: CompletableDeferred<Boolean> = CompletableDeferred()

View File

@@ -34,13 +34,12 @@ import kotlin.system.exitProcess
class SimplexService: Service() { class SimplexService: Service() {
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
private var isCheckingNewMessages = false private var isStartingService = false
private var notificationManager: NotificationManager? = null private var notificationManager: NotificationManager? = null
private var serviceNotification: Notification? = null private var serviceNotification: Notification? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand startId: $startId") Log.d(TAG, "onStartCommand startId: $startId")
isServiceStarting = false
if (intent != null) { if (intent != null) {
val action = intent.action val action = intent.action
Log.d(TAG, "intent action $action") Log.d(TAG, "intent action $action")
@@ -72,7 +71,6 @@ class SimplexService: Service() {
stopForeground(true) stopForeground(true)
stopSelf() stopSelf()
} else { } else {
isServiceStarting = false
isServiceStarted = true isServiceStarted = true
// In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here // In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here
if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) { if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) {
@@ -91,7 +89,6 @@ class SimplexService: Service() {
} catch (e: Exception) { } catch (e: Exception) {
Log.d(TAG, "Exception while releasing wakelock: ${e.message}") Log.d(TAG, "Exception while releasing wakelock: ${e.message}")
} }
isServiceStarting = false
isServiceStarted = false isServiceStarted = false
stopAfterStart = false stopAfterStart = false
saveServiceState(this, ServiceState.STOPPED) saveServiceState(this, ServiceState.STOPPED)
@@ -104,10 +101,10 @@ class SimplexService: Service() {
private fun startService() { private fun startService() {
Log.d(TAG, "SimplexService startService") Log.d(TAG, "SimplexService startService")
if (wakeLock != null || isCheckingNewMessages) return if (wakeLock != null || isStartingService) return
val self = this val self = this
isCheckingNewMessages = true isStartingService = true
withLongRunningApi { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
val chatController = ChatController val chatController = ChatController
waitDbMigrationEnds(chatController) waitDbMigrationEnds(chatController)
try { try {
@@ -126,7 +123,7 @@ class SimplexService: Service() {
} }
} }
} finally { } finally {
isCheckingNewMessages = false isStartingService = false
} }
} }
} }
@@ -265,8 +262,7 @@ class SimplexService: Service() {
private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE" private const val SHARED_PREFS_SERVICE_STATE = "SIMPLEX_SERVICE_STATE"
private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce" private const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
var isServiceStarting = false private var isServiceStarted = false
var isServiceStarted = false
private var stopAfterStart = false private var stopAfterStart = false
fun scheduleStart(context: Context) { fun scheduleStart(context: Context) {
@@ -285,7 +281,7 @@ class SimplexService: Service() {
fun safeStopService() { fun safeStopService() {
if (isServiceStarted) { if (isServiceStarted) {
androidAppContext.stopService(Intent(androidAppContext, SimplexService::class.java)) androidAppContext.stopService(Intent(androidAppContext, SimplexService::class.java))
} else if (isServiceStarting) { } else {
stopAfterStart = true stopAfterStart = true
} }
} }
@@ -295,7 +291,6 @@ class SimplexService: Service() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Intent(androidAppContext, SimplexService::class.java).also { Intent(androidAppContext, SimplexService::class.java).also {
it.action = action.name it.action = action.name
isServiceStarting = true
ContextCompat.startForegroundService(androidAppContext, it) ContextCompat.startForegroundService(androidAppContext, it)
} }
} }

View File

@@ -13,7 +13,7 @@ import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.core.app.* import androidx.core.app.*
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.TAG import chat.simplex.app.TAG
import chat.simplex.app.views.call.CallActivity import chat.simplex.app.views.call.IncomingCallActivity
import chat.simplex.app.views.call.getKeyguardManager import chat.simplex.app.views.call.getKeyguardManager
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
@@ -33,7 +33,6 @@ object NtfManager {
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_2" const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_2"
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL" const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL" const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
const val EndCallAction: String = "chat.simplex.app.END_CALL"
const val CallNotificationId: Int = -1 const val CallNotificationId: Int = -1
private const val UserIdKey: String = "userId" private const val UserIdKey: String = "userId"
private const val ChatIdKey: String = "chatId" private const val ChatIdKey: String = "chatId"
@@ -158,7 +157,7 @@ object NtfManager {
val screenOff = displayManager.displays.all { it.state != Display.STATE_ON } val screenOff = displayManager.displays.all { it.state != Display.STATE_ON }
var ntfBuilder = var ntfBuilder =
if ((keyguardManager.isKeyguardLocked || screenOff) && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) { if ((keyguardManager.isKeyguardLocked || screenOff) && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) {
val fullScreenIntent = Intent(context, CallActivity::class.java) val fullScreenIntent = Intent(context, IncomingCallActivity::class.java)
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
NotificationCompat.Builder(context, CallChannel) NotificationCompat.Builder(context, CallChannel)
.setFullScreenIntent(fullScreenPendingIntent, true) .setFullScreenIntent(fullScreenPendingIntent, true)

View File

@@ -1,18 +1,17 @@
package chat.simplex.app.views.call package chat.simplex.app.views.call
import android.app.* import android.app.Activity
import android.content.* import android.app.KeyguardManager
import android.content.res.Configuration import android.content.Context
import android.graphics.Rect import android.content.Intent
import android.os.* import android.os.Build
import android.util.Rational import android.os.Bundle
import android.view.* import chat.simplex.common.platform.Log
import android.view.WindowManager
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.trackPipAnimationHintView
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
@@ -23,115 +22,33 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.app.model.NtfManager.OpenChatAction
import chat.simplex.common.platform.ntfManager
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import java.lang.ref.WeakReference
import chat.simplex.common.platform.chatModel as m
class CallActivity: ComponentActivity(), ServiceConnection { class IncomingCallActivity: ComponentActivity() {
var boundService: CallService? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
callActivity = WeakReference(this) setContent { IncomingCallActivityView(ChatModel) }
when (intent?.action) { unlockForIncomingCall()
AcceptCallAction -> {
val remoteHostId = intent.getLongExtra("remoteHostId", -1).takeIf { it != -1L }
val chatId = intent.getStringExtra("chatId")
val invitation = (m.callInvitations.values + m.activeCallInvitation.value).lastOrNull {
it?.remoteHostId == remoteHostId && it?.contact?.id == chatId
}
if (invitation != null) {
m.callManager.acceptIncomingCall(invitation = invitation)
}
}
}
setContent { CallActivityView() }
if (isOnLockScreenNow()) {
unlockForIncomingCall()
}
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
if (isOnLockScreenNow()) { lockAfterIncomingCall()
lockAfterIncomingCall()
}
try {
unbindService(this)
} catch (e: Exception) {
Log.i(TAG, "Unable to unbind service: " + e.stackTraceToString())
}
}
private fun isOnLockScreenNow() = getKeyguardManager(this).isKeyguardLocked
fun setPipParams(video: Boolean, sourceRectHint: Rect? = null, viewRatio: Rational? = null) {
// By manually specifying source rect we exclude empty background while toggling PiP
val builder = PictureInPictureParams.Builder()
.setAspectRatio(viewRatio)
.setSourceRectHint(sourceRectHint)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setAutoEnterEnabled(video)
}
setPictureInPictureParams(builder.build())
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
m.activeCallViewIsCollapsed.value = isInPictureInPictureMode
val layoutType = if (!isInPictureInPictureMode) {
LayoutType.Default
} else {
LayoutType.RemoteVideo
}
m.callCommand.add(WCallCommand.Layout(layoutType))
}
override fun onBackPressed() {
if (isOnLockScreenNow()) {
super.onBackPressed()
} else {
m.activeCallViewIsCollapsed.value = true
}
}
override fun onPictureInPictureRequested(): Boolean {
Log.d(TAG, "Requested picture-in-picture from the system")
return super.onPictureInPictureRequested()
}
override fun onUserLeaveHint() {
// On Android 12+ PiP is enabled automatically when a user hides the app
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
enterPictureInPictureMode()
}
}
override fun onResume() {
super.onResume()
m.activeCallViewIsCollapsed.value = false
} }
private fun unlockForIncomingCall() { private fun unlockForIncomingCall() {
@@ -155,23 +72,6 @@ class CallActivity: ComponentActivity(), ServiceConnection {
} }
} }
fun startServiceAndBind() {
/**
* On Android 12 there is a bug that prevents starting activity after pressing back button
* (the error says that it denies to start activity in background).
* Workaround is to bind to a service
* */
bindService(CallService.startService(), this, 0)
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
boundService = (service as CallService.CallServiceBinder).getService()
}
override fun onServiceDisconnected(name: ComponentName?) {
boundService = null
}
companion object { companion object {
const val activityFlags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON const val activityFlags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
} }
@@ -180,96 +80,38 @@ class CallActivity: ComponentActivity(), ServiceConnection {
fun getKeyguardManager(context: Context): KeyguardManager = fun getKeyguardManager(context: Context): KeyguardManager =
context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
private fun callSupportsVideo() = m.activeCall.value?.supportsVideo() == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
@Composable @Composable
fun CallActivityView() { fun IncomingCallActivityView(m: ChatModel) {
val switchingCall = m.switchingCall.value val switchingCall = m.switchingCall.value
val invitation = m.activeCallInvitation.value val invitation = m.activeCallInvitation.value
val call = remember { m.activeCall }.value val call = m.activeCall.value
val showCallView = m.showCallView.value val showCallView = m.showCallView.value
val activity = LocalContext.current as CallActivity val activity = LocalContext.current as Activity
LaunchedEffect(Unit) {
snapshotFlow { m.activeCallViewIsCollapsed.value }
.collect { collapsed ->
when {
collapsed -> {
if (!platform.androidPictureInPictureAllowed() || !callSupportsVideo()) {
activity.moveTaskToBack(true)
activity.startActivity(Intent(activity, MainActivity::class.java))
} else if (!activity.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.RESUMED) {
// User pressed back button, show MainActivity
activity.startActivity(Intent(activity, MainActivity::class.java))
activity.enterPictureInPictureMode()
}
}
callSupportsVideo() && !platform.androidPictureInPictureAllowed() -> {
// PiP disabled by user
platform.androidStartCallActivity(false)
}
activity.isInPictureInPictureMode -> {
platform.androidStartCallActivity(false)
}
}
}
}
SimpleXTheme {
var prevCall by remember { mutableStateOf(call) }
KeyChangeEffect(m.activeCall.value) {
if (m.activeCall.value != null) {
prevCall = m.activeCall.value
activity.boundService?.updateNotification()
}
}
Box(Modifier.background(Color.Black)) {
if (call != null) {
val view = LocalView.current
ActiveCallView()
if (callSupportsVideo()) {
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
scope.launch {
activity.setPipParams(callSupportsVideo(), viewRatio = Rational(view.width, view.height))
activity.trackPipAnimationHintView(view)
}
}
}
} else if (prevCall != null) {
prevCall?.let { ActiveCallOverlayDisabled(it) }
}
if (invitation != null) {
if (call == null) {
Surface(
Modifier
.fillMaxSize(),
color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
) {
IncomingCallLockScreenAlert(invitation, m)
}
} else {
IncomingCallAlertView(invitation, m)
}
}
}
}
LaunchedEffect(call == null) {
if (call != null) {
activity.startServiceAndBind()
}
}
LaunchedEffect(invitation, call, switchingCall, showCallView) { LaunchedEffect(invitation, call, switchingCall, showCallView) {
if (!switchingCall && invitation == null && (!showCallView || call == null)) { if (!switchingCall && invitation == null && (!showCallView || call == null)) {
Log.d(TAG, "CallActivityView: finishing activity") Log.d(TAG, "IncomingCallActivityView: finishing activity")
activity.finish() activity.finish()
} }
} }
SimpleXTheme {
Surface(
Modifier
.fillMaxSize(),
color = MaterialTheme.colors.background,
contentColor = LocalContentColor.current
) {
if (showCallView) {
Box {
ActiveCallView()
if (invitation != null) IncomingCallAlertView(invitation, m)
}
} else if (invitation != null) {
IncomingCallLockScreenAlert(invitation, m)
}
}
}
} }
/**
* Related to lockscreen
* */
@Composable @Composable
fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel) { fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager val cm = chatModel.callManager
@@ -293,7 +135,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
acceptCall = { cm.acceptIncomingCall(invitation = invitation) }, acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
openApp = { openApp = {
val intent = Intent(context, MainActivity::class.java) val intent = Intent(context, MainActivity::class.java)
.setAction(NtfManager.OpenChatAction) .setAction(OpenChatAction)
.putExtra("userId", invitation.user.userId) .putExtra("userId", invitation.user.userId)
.putExtra("chatId", invitation.contact.id) .putExtra("chatId", invitation.contact.id)
context.startActivity(intent) context.startActivity(intent)

View File

@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.LocalServerSocket import android.net.LocalServerSocket
import android.util.Log import android.util.Log
import androidx.activity.ComponentActivity
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import chat.simplex.common.* import chat.simplex.common.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
@@ -26,8 +25,7 @@ val defaultLocale: Locale = Locale.getDefault()
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
lateinit var androidAppContext: Context lateinit var androidAppContext: Context
var mainActivity: WeakReference<FragmentActivity> = WeakReference(null) lateinit var mainActivity: WeakReference<FragmentActivity>
var callActivity: WeakReference<ComponentActivity> = WeakReference(null)
fun initHaskell() { fun initHaskell() {
val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000) val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000)

View File

@@ -61,16 +61,6 @@ actual fun cropToSquare(image: ImageBitmap): ImageBitmap {
return Bitmap.createBitmap(image.asAndroidBitmap(), xOffset, yOffset, side, side).asImageBitmap() return Bitmap.createBitmap(image.asAndroidBitmap(), xOffset, yOffset, side, side).asImageBitmap()
} }
fun Bitmap.clipToCircle(): Bitmap {
val circle = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val path = android.graphics.Path()
path.addCircle(width / 2f, height / 2f, min(width, height) / 2f, android.graphics.Path.Direction.CCW)
val canvas = android.graphics.Canvas(circle)
canvas.clipPath(path)
canvas.drawBitmap(this, 0f, 0f, null)
return circle
}
actual fun compressImageStr(bitmap: ImageBitmap): String { actual fun compressImageStr(bitmap: ImageBitmap): String {
val usePng = bitmap.hasAlpha() val usePng = bitmap.hasAlpha()
val ext = if (usePng) "png" else "jpg" val ext = if (usePng) "png" else "jpg"

View File

@@ -14,27 +14,17 @@ import chat.simplex.common.views.helpers.*
import java.io.BufferedOutputStream import java.io.BufferedOutputStream
import java.io.File import java.io.File
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlin.math.min
actual fun ClipboardManager.shareText(text: String) { actual fun ClipboardManager.shareText(text: String) {
var text = text val sendIntent: Intent = Intent().apply {
for (i in 10 downTo 1) { action = Intent.ACTION_SEND
try { putExtra(Intent.EXTRA_TEXT, text)
val sendIntent: Intent = Intent().apply { type = "text/plain"
action = Intent.ACTION_SEND flags = FLAG_ACTIVITY_NEW_TASK
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
flags = FLAG_ACTIVITY_NEW_TASK
}
val shareIntent = Intent.createChooser(sendIntent, null)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent)
break
} catch (e: Exception) {
Log.e(TAG, "Failed to share text: ${e.stackTraceToString()}")
text = text.substring(0, min(i * 1000, text.length))
}
} }
val shareIntent = Intent.createChooser(sendIntent, null)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent)
} }
actual fun shareFile(text: String, fileSource: CryptoFile) { actual fun shareFile(text: String, fileSource: CryptoFile) {

View File

@@ -12,8 +12,6 @@ import androidx.activity.compose.setContent
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import chat.simplex.common.AppScreen import chat.simplex.common.AppScreen
import chat.simplex.common.model.clear
import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import androidx.compose.ui.platform.LocalContext as LocalContext1 import androidx.compose.ui.platform.LocalContext as LocalContext1
import chat.simplex.res.MR import chat.simplex.res.MR
@@ -114,8 +112,7 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.app_was_crashed), title = generalGetString(MR.strings.app_was_crashed),
text = e.stackTraceToString(), text = e.stackTraceToString()
shareText = true
) )
} }
} }

View File

@@ -28,7 +28,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@@ -51,30 +50,20 @@ import kotlinx.datetime.Clock
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
// Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak
@SuppressLint("StaticFieldLeak")
private var staticWebView: WebView? = null
// WebView methods must be called on Main thread
fun activeCallDestroyWebView() = withApi {
// Stop it when call ended
platform.androidCallServiceSafeStop()
staticWebView?.destroy()
staticWebView = null
Log.d(TAG, "CallView: webview was destroyed")
}
@SuppressLint("SourceLockedOrientationActivity") @SuppressLint("SourceLockedOrientationActivity")
@Composable @Composable
actual fun ActiveCallView() { actual fun ActiveCallView() {
val chatModel = ChatModel
BackHandler(onBack = {
val call = chatModel.activeCall.value
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
})
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) } val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
val proximityLock = remember { val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE }
val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager) LaunchedEffect(Unit) {
if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { // Start service when call happening since it's not already started.
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock") // It's needed to prevent Android from shutting down a microphone after a minute or so when screen is off
} else { if (!ntfModeService) platform.androidServiceStart()
null
}
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
@@ -104,24 +93,22 @@ actual fun ActiveCallView() {
} }
} }
am.registerAudioDeviceCallback(audioCallback, null) am.registerAudioDeviceCallback(audioCallback, null)
val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager)
val proximityLock = if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock")
} else {
null
}
proximityLock?.acquire()
onDispose { onDispose {
// Stop it when call ended
if (!ntfModeService) platform.androidServiceSafeStop()
dropAudioManagerOverrides() dropAudioManagerOverrides()
am.unregisterAudioDeviceCallback(audioCallback) am.unregisterAudioDeviceCallback(audioCallback)
if (proximityLock?.isHeld == true) { proximityLock?.release()
proximityLock.release()
}
}
}
LaunchedEffect(chatModel.activeCallViewIsCollapsed.value) {
if (chatModel.activeCallViewIsCollapsed.value) {
if (proximityLock?.isHeld == true) proximityLock.release()
} else {
delay(1000)
if (proximityLock?.isHeld == false) proximityLock.acquire()
} }
} }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val call = chatModel.activeCall.value
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
WebRTCView(chatModel.callCommand) { apiMsg -> WebRTCView(chatModel.callCommand) { apiMsg ->
Log.d(TAG, "received from WebRTCView: $apiMsg") Log.d(TAG, "received from WebRTCView: $apiMsg")
@@ -133,15 +120,15 @@ actual fun ActiveCallView() {
is WCallResponse.Capabilities -> withBGApi { is WCallResponse.Capabilities -> withBGApi {
val callType = CallType(call.localMedia, r.capabilities) val callType = CallType(call.localMedia, r.capabilities)
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType) chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) } chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
} }
is WCallResponse.Offer -> withBGApi { is WCallResponse.Offer -> withBGApi {
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities) chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) } chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
} }
is WCallResponse.Answer -> withBGApi { is WCallResponse.Answer -> withBGApi {
chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates) chatModel.controller.apiSendCallAnswer(callRh, call.contact, r.answer, r.iceCandidates)
updateActiveCall(call) { it.copy(callState = CallState.Negotiated) } chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
} }
is WCallResponse.Ice -> withBGApi { is WCallResponse.Ice -> withBGApi {
chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates) chatModel.controller.apiSendCallExtraInfo(callRh, call.contact, r.iceCandidates)
@@ -150,7 +137,7 @@ actual fun ActiveCallView() {
try { try {
val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"") val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"")
if (callStatus == WebRTCCallStatus.Connected) { if (callStatus == WebRTCCallStatus.Connected) {
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectedAt = Clock.System.now()) } chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now())
setCallSound(call.soundSpeaker, audioViaBluetooth) setCallSound(call.soundSpeaker, audioViaBluetooth)
} }
withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) } withBGApi { chatModel.controller.apiCallStatus(callRh, call.contact, callStatus) }
@@ -158,7 +145,7 @@ actual fun ActiveCallView() {
Log.d(TAG,"call status ${r.state.connectionState} not used") Log.d(TAG,"call status ${r.state.connectionState} not used")
} }
is WCallResponse.Connected -> { is WCallResponse.Connected -> {
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) } chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo)
scope.launch { scope.launch {
setCallSound(call.soundSpeaker, audioViaBluetooth) setCallSound(call.soundSpeaker, audioViaBluetooth)
} }
@@ -167,29 +154,27 @@ actual fun ActiveCallView() {
withBGApi { chatModel.callManager.endCall(call) } withBGApi { chatModel.callManager.endCall(call) }
} }
is WCallResponse.Ended -> { is WCallResponse.Ended -> {
updateActiveCall(call) { it.copy(callState = CallState.Ended) } chatModel.activeCall.value = call.copy(callState = CallState.Ended)
withBGApi { chatModel.callManager.endCall(call) } withBGApi { chatModel.callManager.endCall(call) }
chatModel.showCallView.value = false
} }
is WCallResponse.Ok -> when (val cmd = apiMsg.command) { is WCallResponse.Ok -> when (val cmd = apiMsg.command) {
is WCallCommand.Answer -> is WCallCommand.Answer ->
updateActiveCall(call) { it.copy(callState = CallState.Negotiated) } chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
is WCallCommand.Media -> { is WCallCommand.Media -> {
updateActiveCall(call) { when (cmd.media) {
when (cmd.media) { CallMediaType.Video -> chatModel.activeCall.value = call.copy(videoEnabled = cmd.enable)
CallMediaType.Video -> it.copy(videoEnabled = cmd.enable) CallMediaType.Audio -> chatModel.activeCall.value = call.copy(audioEnabled = cmd.enable)
CallMediaType.Audio -> it.copy(audioEnabled = cmd.enable)
}
} }
} }
is WCallCommand.Camera -> { is WCallCommand.Camera -> {
updateActiveCall(call) { it.copy(localCamera = cmd.camera) } chatModel.activeCall.value = call.copy(localCamera = cmd.camera)
if (!call.audioEnabled) { if (!call.audioEnabled) {
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false)) chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
} }
} }
is WCallCommand.End -> { is WCallCommand.End ->
withBGApi { chatModel.callManager.endCall(call) } chatModel.showCallView.value = false
}
else -> {} else -> {}
} }
is WCallResponse.Error -> { is WCallResponse.Error -> {
@@ -198,16 +183,8 @@ actual fun ActiveCallView() {
} }
} }
} }
val showOverlay = when { val call = chatModel.activeCall.value
call == null -> false if (call != null) ActiveCallOverlay(call, chatModel, audioViaBluetooth)
!platform.androidPictureInPictureAllowed() -> true
!call.supportsVideo() -> true
!chatModel.activeCallViewIsCollapsed.value -> true
else -> false
}
if (call != null && showOverlay) {
ActiveCallOverlay(call, chatModel, audioViaBluetooth)
}
} }
val context = LocalContext.current val context = LocalContext.current
@@ -252,20 +229,6 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot
) )
} }
@Composable
fun ActiveCallOverlayDisabled(call: Call) {
ActiveCallOverlayLayout(
call = call,
speakerCanBeEnabled = false,
enabled = false,
dismiss = {},
toggleAudio = {},
toggleVideo = {},
toggleSound = {},
flipCamera = {}
)
}
private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) { private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) {
val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker") Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker")
@@ -308,69 +271,59 @@ private fun dropAudioManagerOverrides() {
private fun ActiveCallOverlayLayout( private fun ActiveCallOverlayLayout(
call: Call, call: Call,
speakerCanBeEnabled: Boolean, speakerCanBeEnabled: Boolean,
enabled: Boolean = true,
dismiss: () -> Unit, dismiss: () -> Unit,
toggleAudio: () -> Unit, toggleAudio: () -> Unit,
toggleVideo: () -> Unit, toggleVideo: () -> Unit,
toggleSound: () -> Unit, toggleSound: () -> Unit,
flipCamera: () -> Unit flipCamera: () -> Unit
) { ) {
Column { Column(Modifier.padding(DEFAULT_PADDING)) {
val media = call.peerMedia ?: call.localMedia when (call.peerMedia ?: call.localMedia) {
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) { CallMediaType.Video -> {
if (media == CallMediaType.Video) { CallInfoView(call, alignment = Alignment.Start)
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1) Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
} DisabledBackgroundCallsButton()
} }
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
when (media) { ToggleAudioButton(call, toggleAudio)
CallMediaType.Video -> { Spacer(Modifier.size(40.dp))
VideoCallInfoView(call) IconButton(onClick = dismiss) {
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) { Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
DisabledBackgroundCallsButton()
} }
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { if (call.videoEnabled) {
ToggleAudioButton(call, enabled, toggleAudio) ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, flipCamera)
Spacer(Modifier.size(40.dp)) ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, toggleVideo)
IconButton(onClick = dismiss, enabled = enabled) { } else {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp)) Spacer(Modifier.size(48.dp))
} ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, toggleVideo)
if (call.videoEnabled) {
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera)
ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo)
} else {
Spacer(Modifier.size(48.dp))
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo)
}
} }
} }
}
CallMediaType.Audio -> { CallMediaType.Audio -> {
Spacer(Modifier.fillMaxHeight().weight(1f)) Spacer(Modifier.fillMaxHeight().weight(1f))
Column( Column(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
ProfileImage(size = 192.dp, image = call.contact.profile.image) ProfileImage(size = 192.dp, image = call.contact.profile.image)
AudioCallInfoView(call) CallInfoView(call, alignment = Alignment.CenterHorizontally)
} }
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) { Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton() DisabledBackgroundCallsButton()
} }
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) { Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
IconButton(onClick = dismiss, enabled = enabled) { IconButton(onClick = dismiss) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp)) Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
}
} }
Box(Modifier.padding(start = 32.dp)) { }
ToggleAudioButton(call, enabled, toggleAudio) Box(Modifier.padding(start = 32.dp)) {
} ToggleAudioButton(call, toggleAudio)
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { }
Box(Modifier.padding(end = 32.dp)) { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
ToggleSoundButton(call, speakerCanBeEnabled && enabled, toggleSound) Box(Modifier.padding(end = 32.dp)) {
} ToggleSoundButton(call, speakerCanBeEnabled, toggleSound)
} }
} }
} }
@@ -380,7 +333,7 @@ private fun ActiveCallOverlayLayout(
} }
@Composable @Composable
private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit) { private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, action: () -> Unit, enabled: Boolean = true) {
if (call.hasMedia) { if (call.hasMedia) {
IconButton(onClick = action, enabled = enabled) { IconButton(onClick = action, enabled = enabled) {
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp)) Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp))
@@ -391,26 +344,28 @@ private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, e
} }
@Composable @Composable
private fun ToggleAudioButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit) { private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) {
if (call.audioEnabled) { if (call.audioEnabled) {
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio) ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, toggleAudio)
} else { } else {
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio) ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, toggleAudio)
} }
} }
@Composable @Composable
private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) { private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) {
if (call.soundSpeaker) { if (call.soundSpeaker) {
ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound) ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, toggleSound, enabled)
} else { } else {
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, enabled, toggleSound) ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, toggleSound, enabled)
} }
} }
@Composable @Composable
fun AudioCallInfoView(call: Call) { fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { @Composable fun InfoText(text: String, style: TextStyle = MaterialTheme.typography.body2) =
Text(text, color = Color(0xFFFFFFD8), style = style)
Column(horizontalAlignment = alignment) {
InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2) InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
InfoText(call.callState.text) InfoText(call.callState.text)
@@ -420,21 +375,6 @@ fun AudioCallInfoView(call: Call) {
} }
} }
@Composable
fun VideoCallInfoView(call: Call) {
Column(horizontalAlignment = Alignment.Start) {
InfoText(call.callState.text)
val connInfo = call.connectionInfo
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
InfoText(call.encryptionStatus + connInfoText)
}
}
@Composable
fun InfoText(text: String, modifier: Modifier = Modifier, style: TextStyle = MaterialTheme.typography.body2) =
Text(text, modifier, color = Color(0xFFFFFFD8), style = style)
@Composable @Composable
private fun DisabledBackgroundCallsButton() { private fun DisabledBackgroundCallsButton() {
var show by remember { mutableStateOf(!platform.androidIsBackgroundCallAllowed()) } var show by remember { mutableStateOf(!platform.androidIsBackgroundCallAllowed()) }
@@ -512,6 +452,7 @@ private fun DisabledBackgroundCallsButton() {
@Composable @Composable
fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) { fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) {
val scope = rememberCoroutineScope()
val webView = remember { mutableStateOf<WebView?>(null) } val webView = remember { mutableStateOf<WebView?>(null) }
val permissionsState = rememberMultiplePermissionsState( val permissionsState = rememberMultiplePermissionsState(
permissions = listOf( permissions = listOf(
@@ -534,10 +475,10 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
} }
lifecycleOwner.lifecycle.addObserver(observer) lifecycleOwner.lifecycle.addObserver(observer)
onDispose { onDispose {
val wv = webView.value
if (wv != null) processCommand(wv, WCallCommand.End)
lifecycleOwner.lifecycle.removeObserver(observer) lifecycleOwner.lifecycle.removeObserver(observer)
// val wv = webView.value webView.value?.destroy()
// if (wv != null) processCommand(wv, WCallCommand.End)
// webView.value?.destroy()
webView.value = null webView.value = null
} }
} }
@@ -564,7 +505,7 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
AndroidView( AndroidView(
factory = { AndroidViewContext -> factory = { AndroidViewContext ->
(staticWebView ?: WebView(androidAppContext)).apply { WebView(AndroidViewContext).apply {
layoutParams = ViewGroup.LayoutParams( layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
@@ -589,11 +530,7 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
webViewSettings.javaScriptEnabled = true webViewSettings.javaScriptEnabled = true
webViewSettings.mediaPlaybackRequiresUserGesture = false webViewSettings.mediaPlaybackRequiresUserGesture = false
webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE
if (staticWebView == null) { this.loadUrl("file:android_asset/www/android/call.html")
this.loadUrl("file:android_asset/www/android/call.html")
} else {
webView.value = this
}
} }
} }
) { /* WebView */ } ) { /* WebView */ }
@@ -617,15 +554,6 @@ class WebRTCInterface(private val onResponse: (WVAPIMessage) -> Unit) {
} }
} }
private fun updateActiveCall(initial: Call, transform: (Call) -> Call) {
val activeCall = chatModel.activeCall.value
if (activeCall != null && activeCall.contact.apiId == initial.contact.apiId) {
chatModel.activeCall.value = transform(activeCall)
} else {
Log.d(TAG, "withActiveCall: ignoring, not in call with the contact ${activeCall?.contact?.id}")
}
}
private class LocalContentWebViewClient(val webView: MutableState<WebView?>, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() { private class LocalContentWebViewClient(val webView: MutableState<WebView?>, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
override fun shouldInterceptRequest( override fun shouldInterceptRequest(
view: WebView, view: WebView,
@@ -638,7 +566,6 @@ private class LocalContentWebViewClient(val webView: MutableState<WebView?>, pri
super.onPageFinished(view, url) super.onPageFinished(view, url)
view.evaluateJavascript("sendMessageToNative = (msg) => WebRTCInterface.postMessage(JSON.stringify(msg))", null) view.evaluateJavascript("sendMessageToNative = (msg) => WebRTCInterface.postMessage(JSON.stringify(msg))", null)
webView.value = view webView.value = view
staticWebView = view
Log.d(TAG, "WebRTCView: webview ready") Log.d(TAG, "WebRTCView: webview ready")
// for debugging // for debugging
// view.evaluateJavascript("sendMessageToNative = ({resp}) => WebRTCInterface.postMessage(JSON.stringify({command: resp}))", null) // view.evaluateJavascript("sendMessageToNative = ({resp}) => WebRTCInterface.postMessage(JSON.stringify({command: resp}))", null)
@@ -652,7 +579,6 @@ fun PreviewActiveCallOverlayVideo() {
ActiveCallOverlayLayout( ActiveCallOverlayLayout(
call = Call( call = Call(
remoteHostId = null, remoteHostId = null,
userProfile = Profile.sampleData,
contact = Contact.sampleData, contact = Contact.sampleData,
callState = CallState.Negotiated, callState = CallState.Negotiated,
localMedia = CallMediaType.Video, localMedia = CallMediaType.Video,
@@ -679,7 +605,6 @@ fun PreviewActiveCallOverlayAudio() {
ActiveCallOverlayLayout( ActiveCallOverlayLayout(
call = Call( call = Call(
remoteHostId = null, remoteHostId = null,
userProfile = Profile.sampleData,
contact = Contact.sampleData, contact = Contact.sampleData,
callState = CallState.Negotiated, callState = CallState.Negotiated,
localMedia = CallMediaType.Audio, localMedia = CallMediaType.Audio,

View File

@@ -1,112 +1,8 @@
package chat.simplex.common.views.chatlist package chat.simplex.common.views.chatlist
import android.app.Activity
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
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.graphics.toArgb
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.ANDROID_CALL_TOP_PADDING
import chat.simplex.common.model.durationText
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.datetime.Clock
private val CALL_INTERACTIVE_AREA_HEIGHT = 74.dp
private val CALL_TOP_OFFSET = (-10).dp
private val CALL_TOP_GREEN_LINE_HEIGHT = ANDROID_CALL_TOP_PADDING - CALL_TOP_OFFSET
private val CALL_BOTTOM_ICON_OFFSET = (-15).dp
private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM_ICON_OFFSET
@Composable @Composable
actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>) { actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) {}
val onClick = { platform.androidStartCallActivity(false) }
Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) {
val source = remember { MutableInteractionSource() }
val indication = rememberRipple(bounded = true, 3000.dp)
Box(Modifier.height(CALL_TOP_GREEN_LINE_HEIGHT).clickable(onClick = onClick, indication = indication, interactionSource = source)) {
GreenLine(call)
}
Box(
Modifier
.offset(y = CALL_BOTTOM_ICON_OFFSET)
.size(CALL_BOTTOM_ICON_HEIGHT)
.background(SimplexGreen, CircleShape)
.clip(CircleShape)
.clickable(onClick = onClick, indication = indication, interactionSource = source)
.align(Alignment.BottomCenter),
contentAlignment = Alignment.Center
) {
val media = call.peerMedia ?: call.localMedia
if (media == CallMediaType.Video) {
Icon(painterResource(MR.images.ic_videocam_filled), null, Modifier.size(27.dp).offset(x = 2.5.dp, y = 2.dp), tint = Color.White)
} else {
Icon(painterResource(MR.images.ic_call_filled), null, Modifier.size(27.dp).offset(x = -0.5.dp, y = 2.dp), tint = Color.White)
}
}
}
}
@Composable
private fun GreenLine(call: Call) {
Row(
Modifier
.fillMaxSize()
.background(SimplexGreen)
.padding(top = -CALL_TOP_OFFSET)
.padding(horizontal = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
ContactName(call.contact.displayName)
Spacer(Modifier.weight(1f))
CallDuration(call)
}
val window = (LocalContext.current as Activity).window
DisposableEffect(Unit) {
window.statusBarColor = SimplexGreen.toArgb()
onDispose {
window.statusBarColor = Color.Black.toArgb()
}
}
}
@Composable
private fun ContactName(name: String) {
Text(name, Modifier.width(windowWidth() * 0.35f), color = Color.White, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@Composable
private fun CallDuration(call: Call) {
val connectedAt = call.connectedAt
if (connectedAt != null) {
val time = remember { mutableStateOf(durationText(0)) }
LaunchedEffect(Unit) {
while (true) {
time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt())
delay(250)
}
}
val text = time.value
val sp40Or50 = with(LocalDensity.current) { if (text.length >= 6) 60.sp.toDp() else 42.sp.toDp() }
val offset = with(LocalDensity.current) { 7.sp.toDp() }
Text(text, Modifier.offset(x = offset).widthIn(min = sp40Or50), color = Color.White)
}
}

View File

@@ -1,106 +0,0 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.CustomTimeUnit
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import com.sd.lib.compose.wheel_picker.*
@Composable
actual fun CustomTimePicker(
selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits>
) {
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
}
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
val selectedDuration = remember { mutableStateOf(duration) }
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
val isTriggered = remember { mutableStateOf(false) }
LaunchedEffect(selectedUnit.value) {
// on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue
// (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120),
// selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition
if (isTriggered.value) {
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
if (maxValue != null && selectedDuration.value > maxValue) {
selectedDuration.value = maxValue
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
} else {
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
} else {
isTriggered.value = true
}
}
LaunchedEffect(selectedDuration.value) {
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.spacedBy(0.dp)
) {
Column(Modifier.weight(1f)) {
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
FVerticalWheelPicker(
count = selectedUnitValues.value.count(),
state = durationPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
selectedUnitValues.value[index].toString(),
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(durationPickerState) {
snapshotFlow { durationPickerState.currentIndex }
.collect {
selectedDuration.value = selectedUnitValues.value[it]
}
}
}
Column(Modifier.weight(1f)) {
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
FVerticalWheelPicker(
count = timeUnitsLimits.count(),
state = unitPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
timeUnitsLimits[index].timeUnit.text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(unitPickerState) {
snapshotFlow { unitPickerState.currentIndex }
.collect {
selectedUnit.value = timeUnitsLimits[it].timeUnit
}
}
}
}
}

View File

@@ -66,7 +66,6 @@ extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str); extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name); extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length); extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path); extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
@@ -164,14 +163,6 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
return res; return res;
} }
JNIEXPORT int JNICALL
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
int res = chat_json_length(_str);
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) { Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE); const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);

View File

@@ -39,7 +39,6 @@ extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str); extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_password_hash(const char *pwd, const char *salt);
extern char *chat_valid_name(const char *name); extern char *chat_valid_name(const char *name);
extern int chat_json_length(const char *str);
extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length); extern char *chat_write_file(chat_ctrl ctrl, const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce); extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path); extern char *chat_encrypt_file(chat_ctrl ctrl, const char *from_path, const char *to_path);
@@ -174,14 +173,6 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
return res; return res;
} }
JNIEXPORT int JNICALL
Java_chat_simplex_common_platform_CoreKt_chatJsonLength(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = encode_to_utf8_chars(env, str);
int res = chat_json_length(_str);
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) { Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jlong controller, jstring path, jobject buffer) {
const char *_path = encode_to_utf8_chars(env, path); const char *_path = encode_to_utf8_chars(env, path);

View File

@@ -1,19 +1,16 @@
package chat.simplex.common package chat.simplex.common
import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.* import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView
@@ -23,7 +20,8 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.CreateFirstProfile import chat.simplex.common.views.CreateFirstProfile
import chat.simplex.common.views.helpers.SimpleButton import chat.simplex.common.views.helpers.SimpleButton
import chat.simplex.common.views.SplashView import chat.simplex.common.views.SplashView
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.ActiveCallView
import chat.simplex.common.views.call.IncomingCallAlertView
import chat.simplex.common.views.chat.ChatView import chat.simplex.common.views.chat.ChatView
import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.database.DatabaseErrorView import chat.simplex.common.views.database.DatabaseErrorView
@@ -110,7 +108,6 @@ fun MainScreen() {
val localUserCreated = chatModel.localUserCreated.value val localUserCreated = chatModel.localUserCreated.value
var showInitializationView by remember { mutableStateOf(false) } var showInitializationView by remember { mutableStateOf(false) }
when { when {
chatModel.dbMigrationInProgress.value -> DefaultProgressView(stringResource(MR.strings.database_migration_in_progress))
chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database)) chatModel.chatDbStatus.value == null && showInitializationView -> DefaultProgressView(stringResource(MR.strings.opening_database))
showChatDatabaseError -> { showChatDatabaseError -> {
// Prevent showing keyboard on Android when: passcode enabled and database password not saved // Prevent showing keyboard on Android when: passcode enabled and database password not saved
@@ -171,17 +168,7 @@ fun MainScreen() {
} }
} else { } else {
if (chatModel.showCallView.value) { if (chatModel.showCallView.value) {
if (appPlatform.isAndroid) { ActiveCallView()
LaunchedEffect(Unit) {
// This if prevents running the activity in the following condition:
// - the activity already started before and was destroyed by collapsing active call (start audio call, press back button, go to a launcher)
if (!chatModel.activeCallViewIsCollapsed.value) {
platform.androidStartCallActivity(false)
}
}
} else {
ActiveCallView()
}
} else { } else {
// It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked // It's needed for privacy settings toggle, so it can be shown even if the app is passcode unlocked
ModalManager.fullscreen.showPasscodeInView() ModalManager.fullscreen.showPasscodeInView()
@@ -218,13 +205,9 @@ fun MainScreen() {
} }
} }
val ANDROID_CALL_TOP_PADDING = 40.dp
@Composable @Composable
fun AndroidScreen(settingsState: SettingsViewState) { fun AndroidScreen(settingsState: SettingsViewState) {
BoxWithConstraints { BoxWithConstraints {
val call = remember { chatModel.activeCall} .value
val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) } var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) } val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) }
Box( Box(
@@ -232,7 +215,6 @@ fun AndroidScreen(settingsState: SettingsViewState) {
.graphicsLayer { .graphicsLayer {
translationX = -offset.value.dp.toPx() translationX = -offset.value.dp.toPx()
} }
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
) { ) {
StartPartOfScreen(settingsState) StartPartOfScreen(settingsState)
} }
@@ -259,17 +241,11 @@ fun AndroidScreen(settingsState: SettingsViewState) {
} }
} }
} }
Box(Modifier Box(Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@{
.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
) Box2@{
currentChatId?.let { currentChatId?.let {
ChatView(it, chatModel, onComposed) ChatView(it, chatModel, onComposed)
} }
} }
if (call != null && showCallArea) {
ActiveCallInteractiveArea(call, remember { MutableStateFlow(AnimatedViewState.GONE) })
}
} }
} }

View File

@@ -2,7 +2,6 @@ package chat.simplex.common.model
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
@@ -49,7 +48,6 @@ object ChatModel {
val chatDbEncrypted = mutableStateOf<Boolean?>(false) val chatDbEncrypted = mutableStateOf<Boolean?>(false)
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null) val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
val ctrlInitInProgress = mutableStateOf(false) val ctrlInitInProgress = mutableStateOf(false)
val dbMigrationInProgress = mutableStateOf(false)
val chats = mutableStateListOf<Chat>() val chats = mutableStateListOf<Chat>()
// map of connections network statuses, key is agent connection id // map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>() val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
@@ -57,7 +55,7 @@ object ChatModel {
// current chat // current chat
val chatId = mutableStateOf<String?>(null) val chatId = mutableStateOf<String?>(null)
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>()) val chatItems = mutableStateListOf<ChatItem>()
// rhId, chatId // rhId, chatId
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList()) val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
val chatItemStatuses = mutableMapOf<Long, CIStatus>() val chatItemStatuses = mutableMapOf<Long, CIStatus>()
@@ -65,6 +63,8 @@ object ChatModel {
val terminalItems = mutableStateOf<List<TerminalItem>>(listOf()) val terminalItems = mutableStateOf<List<TerminalItem>>(listOf())
val userAddress = mutableStateOf<UserContactLinkRec?>(null) val userAddress = mutableStateOf<UserContactLinkRec?>(null)
// Allows to temporary save servers that are being edited on multiple screens
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None) val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
// set when app opened from external intent // set when app opened from external intent
@@ -96,7 +96,6 @@ object ChatModel {
val activeCallInvitation = mutableStateOf<RcvCallInvitation?>(null) val activeCallInvitation = mutableStateOf<RcvCallInvitation?>(null)
val activeCall = mutableStateOf<Call?>(null) val activeCall = mutableStateOf<Call?>(null)
val activeCallViewIsVisible = mutableStateOf<Boolean>(false) val activeCallViewIsVisible = mutableStateOf<Boolean>(false)
val activeCallViewIsCollapsed = mutableStateOf<Boolean>(false)
val callCommand = mutableStateListOf<WCallCommand>() val callCommand = mutableStateListOf<WCallCommand>()
val showCallView = mutableStateOf(false) val showCallView = mutableStateOf(false)
val switchingCall = mutableStateOf(false) val switchingCall = mutableStateOf(false)
@@ -270,15 +269,18 @@ object ChatModel {
} else { } else {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
} }
Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
// add to current chat // add to current chat
if (chatId.value == cInfo.id) { if (chatId.value == cInfo.id) {
Log.d(TAG, "TODOCHAT: addChatItem: chatIds are equal, size ${chatItems.size}")
// Prevent situation when chat item already in the list received from backend // Prevent situation when chat item already in the list received from backend
if (chatItems.value.none { it.id == cItem.id }) { if (chatItems.none { it.id == cItem.id }) {
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem) chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
} else { } else {
chatItems.add(cItem) chatItems.add(cItem)
Log.d(TAG, "TODOCHAT: addChatItem: added to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
} }
} }
} }
@@ -305,13 +307,14 @@ object ChatModel {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
res = true res = true
} }
Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
return withContext(Dispatchers.Main) { return withContext(Dispatchers.Main) {
// update current chat // update current chat
if (chatId.value == cInfo.id) { if (chatId.value == cInfo.id) {
val items = chatItems.value val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
val itemIndex = items.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) { if (itemIndex >= 0) {
items[itemIndex] = cItem chatItems[itemIndex] = cItem
Log.d(TAG, "TODOCHAT: upsertChatItem: updated in chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
false false
} else { } else {
val status = chatItemStatuses.remove(cItem.id) val status = chatItemStatuses.remove(cItem.id)
@@ -321,6 +324,7 @@ object ChatModel {
cItem cItem
} }
chatItems.add(ci) chatItems.add(ci)
Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
true true
} }
} else { } else {
@@ -332,10 +336,9 @@ object ChatModel {
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) { suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (chatId.value == cInfo.id) { if (chatId.value == cInfo.id) {
val items = chatItems.value val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
val itemIndex = items.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) { if (itemIndex >= 0) {
items[itemIndex] = cItem chatItems[itemIndex] = cItem
} }
} else if (status != null) { } else if (status != null) {
chatItemStatuses[cItem.id] = status chatItemStatuses[cItem.id] = status
@@ -359,10 +362,10 @@ object ChatModel {
} }
// remove from current chat // remove from current chat
if (chatId.value == cInfo.id) { if (chatId.value == cInfo.id) {
chatItems.removeAll { val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
val remove = it.id == cItem.id if (itemIndex >= 0) {
if (remove) { AudioPlayer.stop(it) } AudioPlayer.stop(chatItems[itemIndex])
remove chatItems.removeAt(itemIndex)
} }
} }
} }
@@ -403,7 +406,7 @@ object ChatModel {
} }
fun removeLiveDummy() { fun removeLiveDummy() {
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.removeLast() chatItems.removeLast()
} }
} }
@@ -435,14 +438,14 @@ object ChatModel {
var markedRead = 0 var markedRead = 0
if (chatId.value == cInfo.id) { if (chatId.value == cInfo.id) {
var i = 0 var i = 0
val items = chatItems.value Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marking read ${cInfo.id}, current chatId ${chatId.value}, size was ${chatItems.size}")
while (i < items.size) { while (i < chatItems.count()) {
val item = items[i] val item = chatItems[i]
if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) { if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) {
val newItem = item.withStatus(CIStatus.RcvRead()) val newItem = item.withStatus(CIStatus.RcvRead())
items[i] = newItem chatItems[i] = newItem
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) { if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
items[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy( chatItems[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS))) deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
) )
} }
@@ -450,6 +453,7 @@ object ChatModel {
} }
i += 1 i += 1
} }
Log.d(TAG, "TODOCHAT: markItemsReadInCurrentChat: marked read ${cInfo.id}, current chatId ${chatId.value}, size now ${chatItems.size}")
} }
return markedRead return markedRead
} }
@@ -640,8 +644,7 @@ object ChatModel {
} }
fun addTerminalItem(item: TerminalItem) { fun addTerminalItem(item: TerminalItem) {
val maxItems = if (appPreferences.developerTools.get()) 500 else 200 if (terminalItems.value.size >= 500) {
if (terminalItems.value.size >= maxItems) {
terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size) terminalItems.value = terminalItems.value.subList(1, terminalItems.value.size)
} }
terminalItems.value += item terminalItems.value += item
@@ -966,16 +969,6 @@ sealed class ChatInfo: SomeChat, NamedChat {
is Group -> groupInfo.chatSettings is Group -> groupInfo.chatSettings
else -> null else -> null
} }
val chatTs: Instant
get() = when(this) {
is Direct -> contact.chatTs ?: contact.updatedAt
is Group -> groupInfo.chatTs ?: groupInfo.updatedAt
is Local -> noteFolder.chatTs
is ContactRequest -> contactRequest.updatedAt
is ContactConnection -> contactConnection.updatedAt
is InvalidJSON -> updatedAt
}
} }
@Serializable @Serializable
@@ -1016,7 +1009,6 @@ data class Contact(
val mergedPreferences: ContactUserPreferences, val mergedPreferences: ContactUserPreferences,
override val createdAt: Instant, override val createdAt: Instant,
override val updatedAt: Instant, override val updatedAt: Instant,
val chatTs: Instant?,
val contactGroupMemberId: Long? = null, val contactGroupMemberId: Long? = null,
val contactGrpInvSent: Boolean val contactGrpInvSent: Boolean
): SomeChat, NamedChat { ): SomeChat, NamedChat {
@@ -1085,7 +1077,6 @@ data class Contact(
mergedPreferences = ContactUserPreferences.sampleData, mergedPreferences = ContactUserPreferences.sampleData,
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now(), updatedAt = Clock.System.now(),
chatTs = Clock.System.now(),
contactGrpInvSent = false contactGrpInvSent = false
) )
} }
@@ -1213,8 +1204,7 @@ data class GroupInfo (
val hostConnCustomUserProfileId: Long? = null, val hostConnCustomUserProfileId: Long? = null,
val chatSettings: ChatSettings, val chatSettings: ChatSettings,
override val createdAt: Instant, override val createdAt: Instant,
override val updatedAt: Instant, override val updatedAt: Instant
val chatTs: Instant?
): SomeChat, NamedChat { ): SomeChat, NamedChat {
override val chatType get() = ChatType.Group override val chatType get() = ChatType.Group
override val id get() = "#$groupId" override val id get() = "#$groupId"
@@ -1255,8 +1245,7 @@ data class GroupInfo (
hostConnCustomUserProfileId = null, hostConnCustomUserProfileId = null,
chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false), chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false),
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now(), updatedAt = Clock.System.now()
chatTs = Clock.System.now()
) )
} }
} }
@@ -1518,8 +1507,7 @@ class NoteFolder(
val favorite: Boolean, val favorite: Boolean,
val unread: Boolean, val unread: Boolean,
override val createdAt: Instant, override val createdAt: Instant,
override val updatedAt: Instant, override val updatedAt: Instant
val chatTs: Instant
): SomeChat, NamedChat { ): SomeChat, NamedChat {
override val chatType get() = ChatType.Local override val chatType get() = ChatType.Local
override val id get() = "*$noteFolderId" override val id get() = "*$noteFolderId"
@@ -1542,8 +1530,7 @@ class NoteFolder(
favorite = false, favorite = false,
unread = false, unread = false,
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now(), updatedAt = Clock.System.now()
chatTs = Clock.System.now()
) )
} }
} }
@@ -2003,46 +1990,6 @@ data class ChatItem (
} }
} }
fun MutableState<SnapshotStateList<ChatItem>>.add(index: Int, chatItem: ChatItem) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, chatItem) }
}
fun MutableState<SnapshotStateList<ChatItem>>.add(chatItem: ChatItem) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(chatItem) }
}
fun MutableState<SnapshotStateList<ChatItem>>.addAll(index: Int, chatItems: List<ChatItem>) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(index, chatItems) }
}
fun MutableState<SnapshotStateList<ChatItem>>.addAll(chatItems: List<ChatItem>) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); addAll(chatItems) }
}
fun MutableState<SnapshotStateList<ChatItem>>.removeAll(block: (ChatItem) -> Boolean) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAll(block) }
}
fun MutableState<SnapshotStateList<ChatItem>>.removeAt(index: Int) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeAt(index) }
}
fun MutableState<SnapshotStateList<ChatItem>>.removeLast() {
value = SnapshotStateList<ChatItem>().apply { addAll(value); removeLast() }
}
fun MutableState<SnapshotStateList<ChatItem>>.replaceAll(chatItems: List<ChatItem>) {
value = SnapshotStateList<ChatItem>().apply { addAll(chatItems) }
}
fun MutableState<SnapshotStateList<ChatItem>>.clear() {
value = SnapshotStateList<ChatItem>()
}
fun State<SnapshotStateList<ChatItem>>.asReversed(): MutableList<ChatItem> = value.asReversed()
val State<List<ChatItem>>.size: Int get() = value.size
enum class CIMergeCategory { enum class CIMergeCategory {
MemberConnected, MemberConnected,
RcvGroupEvent, RcvGroupEvent,

View File

@@ -451,21 +451,7 @@ object ChatController {
} }
try { try {
val msg = recvMsg(ctrl) val msg = recvMsg(ctrl)
if (msg != null) { if (msg != null) withSingleThreadContext { processReceivedMsg(msg) }
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
processReceivedMsg(msg)
}
if (finishedWithoutTimeout == null) {
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_slow_function_title),
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
shareText = true
)
}
}
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "ChatController recvMsg/processReceivedMsg exception: " + e.stackTraceToString()); Log.e(TAG, "ChatController recvMsg/processReceivedMsg exception: " + e.stackTraceToString());
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -1699,7 +1685,7 @@ object ChatController {
chatModel.networkStatuses[s.agentConnId] = s.networkStatus chatModel.networkStatuses[s.agentConnId] = s.networkStatus
} }
} }
is CR.NewChatItem -> withBGApi { is CR.NewChatItem -> {
val cInfo = r.chatItem.chatInfo val cInfo = r.chatItem.chatInfo
val cItem = r.chatItem.chatItem val cItem = r.chatItem.chatItem
if (active(r.user)) { if (active(r.user)) {
@@ -1714,7 +1700,7 @@ object ChatController {
((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV) || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
receiveFile(rhId, r.user, file.fileId, auto = true) withBGApi { receiveFile(rhId, r.user, file.fileId, auto = true) }
} }
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) { if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id || chatModel.remoteHostId() != rhId)) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem) ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
@@ -1914,8 +1900,10 @@ object ChatController {
if (invitation != null) { if (invitation != null) {
chatModel.callManager.reportCallRemoteEnded(invitation = invitation) chatModel.callManager.reportCallRemoteEnded(invitation = invitation)
} }
withCall(r, r.contact) { call -> withCall(r, r.contact) { _ ->
withBGApi { chatModel.callManager.endCall(call) } chatModel.callCommand.add(WCallCommand.End)
chatModel.activeCall.value = null
chatModel.showCallView.value = false
} }
} }
is CR.ContactSwitch -> is CR.ContactSwitch ->

View File

@@ -28,7 +28,6 @@ external fun chatParseMarkdown(str: String): String
external fun chatParseServer(str: String): String external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String external fun chatPasswordHash(pwd: String, salt: String): String
external fun chatValidName(name: String): String external fun chatValidName(name: String): String
external fun chatJsonLength(str: String): Int
external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String external fun chatWriteFile(ctrl: ChatCtrl, path: String, buffer: ByteBuffer): String
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any> external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String external fun chatEncryptFile(ctrl: ChatCtrl, fromPath: String, toPath: String): String
@@ -43,7 +42,7 @@ val appPreferences: AppPreferences
val chatController: ChatController = ChatController val chatController: ChatController = ChatController
fun initChatControllerAndRunMigrations() { fun initChatControllerAndRunMigrations() {
withLongRunningApi { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) {
initChatController(startChat = ::showStartChatAfterRestartAlert) initChatController(startChat = ::showStartChatAfterRestartAlert)
} else { } else {
@@ -59,23 +58,10 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
chatModel.ctrlInitInProgress.value = true chatModel.ctrlInitInProgress.value = true
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
var migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value) val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
var res: DBMigrationResult = runCatching { val res: DBMigrationResult = kotlin.runCatching {
json.decodeFromString<DBMigrationResult>(migrated[0] as String) json.decodeFromString<DBMigrationResult>(migrated[0] as String)
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
val rerunMigration = res is DBMigrationResult.ErrorMigration && when (res.migrationError) {
// we don't allow to run down migrations without confirmation in UI, so currently it won't be YesUpDown
is MigrationError.Upgrade -> confirm == MigrationConfirmation.YesUp || confirm == MigrationConfirmation.YesUpDown
is MigrationError.Downgrade -> confirm == MigrationConfirmation.YesUpDown
is MigrationError.Error -> false
}
if (rerunMigration) {
chatModel.dbMigrationInProgress.value = true
migrated = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value)
res = runCatching {
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
}
val ctrl = if (res is DBMigrationResult.OK) { val ctrl = if (res is DBMigrationResult.OK) {
migrated[1] as Long migrated[1] as Long
} else null } else null
@@ -133,7 +119,6 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
} }
} finally { } finally {
chatModel.ctrlInitInProgress.value = false chatModel.ctrlInitInProgress.value = false
chatModel.dbMigrationInProgress.value = false
} }
} }

View File

@@ -55,7 +55,7 @@ abstract class NtfManager {
} }
fun openChatAction(userId: Long?, chatId: ChatId) { fun openChatAction(userId: Long?, chatId: ChatId) {
withLongRunningApi { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
awaitChatStartedIfNeeded(chatModel) awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
// TODO include remote host ID in desktop notifications? // TODO include remote host ID in desktop notifications?
@@ -70,7 +70,7 @@ abstract class NtfManager {
} }
fun showChatsAction(userId: Long?) { fun showChatsAction(userId: Long?) {
withLongRunningApi { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
awaitChatStartedIfNeeded(chatModel) awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
// TODO include remote host ID in desktop notifications? // TODO include remote host ID in desktop notifications?

View File

@@ -1,21 +1,16 @@
package chat.simplex.common.platform package chat.simplex.common.platform
import chat.simplex.common.model.ChatId
import chat.simplex.common.model.NotificationsMode import chat.simplex.common.model.NotificationsMode
interface PlatformInterface { interface PlatformInterface {
suspend fun androidServiceStart() {} suspend fun androidServiceStart() {}
fun androidServiceSafeStop() {} fun androidServiceSafeStop() {}
fun androidCallServiceSafeStop() {}
fun androidNotificationsModeChanged(mode: NotificationsMode) {} fun androidNotificationsModeChanged(mode: NotificationsMode) {}
fun androidChatStartedAfterBeingOff() {} fun androidChatStartedAfterBeingOff() {}
fun androidChatStopped() {} fun androidChatStopped() {}
fun androidChatInitializedAndStarted() {} fun androidChatInitializedAndStarted() {}
fun androidIsBackgroundCallAllowed(): Boolean = true fun androidIsBackgroundCallAllowed(): Boolean = true
fun androidSetNightModeIfSupported() {} fun androidSetNightModeIfSupported() {}
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
fun androidPictureInPictureAllowed(): Boolean = true
fun androidCallEnded() {}
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
} }
/** /**

View File

@@ -1,6 +1,6 @@
package chat.simplex.common.views.call package chat.simplex.common.views.call
import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.withBGApi import chat.simplex.common.views.helpers.withBGApi
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@@ -23,29 +23,27 @@ class CallManager(val chatModel: ChatModel) {
} }
} }
fun acceptIncomingCall(invitation: RcvCallInvitation) = withBGApi { fun acceptIncomingCall(invitation: RcvCallInvitation) {
val call = chatModel.activeCall.value val call = chatModel.activeCall.value
val contactInfo = chatModel.controller.apiContactInfo(invitation.remoteHostId, invitation.contact.contactId) if (call == null) {
val profile = contactInfo?.second ?: invitation.user.profile.toProfile() justAcceptIncomingCall(invitation = invitation)
// In case the same contact calling while previous call didn't end yet (abnormal ending of call from the other side)
if (call == null || (call.remoteHostId == invitation.remoteHostId && call.contact.id == invitation.contact.id)) {
justAcceptIncomingCall(invitation = invitation, profile)
} else { } else {
chatModel.switchingCall.value = true withBGApi {
try { chatModel.switchingCall.value = true
endCall(call = call) try {
justAcceptIncomingCall(invitation = invitation, profile) endCall(call = call)
} finally { justAcceptIncomingCall(invitation = invitation)
chatModel.switchingCall.value = false } finally {
chatModel.switchingCall.value = false
}
} }
} }
} }
private fun justAcceptIncomingCall(invitation: RcvCallInvitation, userProfile: Profile) { private fun justAcceptIncomingCall(invitation: RcvCallInvitation) {
with (chatModel) { with (chatModel) {
activeCall.value = Call( activeCall.value = Call(
remoteHostId = invitation.remoteHostId, remoteHostId = invitation.remoteHostId,
userProfile = userProfile,
contact = invitation.contact, contact = invitation.contact,
callState = CallState.InvitationAccepted, callState = CallState.InvitationAccepted,
localMedia = invitation.callType.media, localMedia = invitation.callType.media,
@@ -70,23 +68,17 @@ class CallManager(val chatModel: ChatModel) {
} }
suspend fun endCall(call: Call) { suspend fun endCall(call: Call) {
with(chatModel) { with (chatModel) {
// If there is active call currently and it's with other contact, don't interrupt it
if (activeCall.value != null && !(activeCall.value?.remoteHostId == call.remoteHostId && activeCall.value?.contact?.id == call.contact.id)) return
// Don't destroy WebView if you plan to accept next call right after this one
if (!switchingCall.value) {
showCallView.value = false
activeCall.value = null
activeCallViewIsCollapsed.value = false
platform.androidCallEnded()
}
if (call.callState == CallState.Ended) { if (call.callState == CallState.Ended) {
Log.d(TAG, "CallManager.endCall: call ended") Log.d(TAG, "CallManager.endCall: call ended")
activeCall.value = null
showCallView.value = false
} else { } else {
Log.d(TAG, "CallManager.endCall: ending call...") Log.d(TAG, "CallManager.endCall: ending call...")
//callCommand.add(WCallCommand.End) callCommand.add(WCallCommand.End)
showCallView.value = false
controller.apiEndCall(call.remoteHostId, call.contact) controller.apiEndCall(call.remoteHostId, call.contact)
activeCall.value = null
} }
} }
} }

View File

@@ -7,11 +7,11 @@ import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.net.URI import java.net.URI
import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
data class Call( data class Call(
val remoteHostId: Long?, val remoteHostId: Long?,
val userProfile: Profile,
val contact: Contact, val contact: Contact,
val callState: CallState, val callState: CallState,
val localMedia: CallMediaType, val localMedia: CallMediaType,
@@ -23,7 +23,7 @@ data class Call(
val soundSpeaker: Boolean = localMedia == CallMediaType.Video, val soundSpeaker: Boolean = localMedia == CallMediaType.Video,
var localCamera: VideoCamera = VideoCamera.User, var localCamera: VideoCamera = VideoCamera.User,
val connectionInfo: ConnectionInfo? = null, val connectionInfo: ConnectionInfo? = null,
var connectedAt: Instant? = null, var connectedAt: Instant? = null
) { ) {
val encrypted: Boolean get() = localEncrypted && sharedKey != null val encrypted: Boolean get() = localEncrypted && sharedKey != null
val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
@@ -36,9 +36,6 @@ data class Call(
} }
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
fun supportsVideo(): Boolean = peerMedia == CallMediaType.Video || localMedia == CallMediaType.Video
} }
enum class CallState { enum class CallState {
@@ -78,7 +75,6 @@ sealed class WCallCommand {
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand() @Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand() @Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
@Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand() @Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand()
@Serializable @SerialName("layout") data class Layout(val layout: LayoutType): WCallCommand()
@Serializable @SerialName("end") object End: WCallCommand() @Serializable @SerialName("end") object End: WCallCommand()
} }
@@ -171,13 +167,6 @@ enum class VideoCamera {
val flipped: VideoCamera get() = if (this == User) Environment else User val flipped: VideoCamera get() = if (this == User) Environment else User
} }
@Serializable
enum class LayoutType {
@SerialName("default") Default,
@SerialName("localVideo") LocalVideo,
@SerialName("remoteVideo") RemoteVideo
}
@Serializable @Serializable
data class ConnectionState( data class ConnectionState(
val connectionState: String, val connectionState: String,

View File

@@ -324,7 +324,7 @@ fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, d
.fillMaxHeight(), .fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween verticalArrangement = Arrangement.SpaceBetween
) { ) {
LaunchedEffect(ciInfo) { LaunchedEffect(Unit) {
if (ciInfo.memberDeliveryStatuses != null) { if (ciInfo.memberDeliveryStatuses != null) {
selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses) selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)
} }

View File

@@ -67,13 +67,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
launch { launch {
snapshotFlow { chatModel.chatId.value } snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged() .distinctUntilChanged()
.filterNotNull() .onEach { Log.d(TAG, "TODOCHAT: chatId: activeChatId ${activeChat.value?.id} == new chatId $it ${activeChat.value?.id == it} ") }
.filter { it != null && activeChat.value?.id != it }
.collect { chatId -> .collect { chatId ->
if (activeChat.value?.id != chatId) { // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly // Also for situation when chatId changes after clicking in notification, etc
// Also for situation when chatId changes after clicking in notification, etc activeChat.value = chatModel.getChat(chatId!!)
activeChat.value = chatModel.getChat(chatId) Log.d(TAG, "TODOCHAT: chatId: activeChatId became ${activeChat.value?.id}")
}
markUnreadChatAsRead(activeChat, chatModel) markUnreadChatAsRead(activeChat, chatModel)
} }
} }
@@ -92,10 +92,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
} }
} }
.distinctUntilChanged() .distinctUntilChanged()
.onEach { Log.d(TAG, "TODOCHAT: chats: activeChatId ${activeChat.value?.id} == new chatId ${it?.id} ${activeChat.value?.id == it?.id} ") }
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions // Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
.filter { it != null && it.chatInfo != activeChat.value?.chatInfo } .filter { it != null && it?.chatInfo != activeChat.value?.chatInfo }
.collect { .collect {
activeChat.value = it activeChat.value = it
Log.d(TAG, "TODOCHAT: chats: activeChatId became ${activeChat.value?.id}")
} }
} }
} }
@@ -146,6 +148,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
}, },
attachmentOption, attachmentOption,
attachmentBottomSheetState, attachmentBottomSheetState,
chatModel.chatItems,
searchText, searchText,
useLinkPreviews = useLinkPreviews, useLinkPreviews = useLinkPreviews,
linkMode = chatModel.simplexLinkMode.value, linkMode = chatModel.simplexLinkMode.value,
@@ -223,17 +226,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
loadPrevMessages = { loadPrevMessages = {
if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout
val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout)
val firstId = chatModel.chatItems.value.firstOrNull()?.id val firstId = chatModel.chatItems.firstOrNull()?.id
if (c != null && firstId != null) { if (c != null && firstId != null) {
withBGApi { withBGApi {
Log.d(TAG, "TODOCHAT: loadPrevMessages: loading for ${c.id}, current chatId ${ChatModel.chatId.value}, size was ${ChatModel.chatItems.size}")
apiLoadPrevMessages(c, chatModel, firstId, searchText.value) apiLoadPrevMessages(c, chatModel, firstId, searchText.value)
Log.d(TAG, "TODOCHAT: loadPrevMessages: loaded for ${c.id}, current chatId ${ChatModel.chatId.value}, size now ${ChatModel.chatItems.size}")
} }
} }
}, },
deleteMessage = { itemId, mode -> deleteMessage = { itemId, mode ->
withBGApi { withBGApi {
val cInfo = chat.chatInfo val cInfo = chat.chatInfo
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId } val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId }
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo) val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
val groupInfo = toModerate?.first val groupInfo = toModerate?.first
val groupMember = toModerate?.second val groupMember = toModerate?.second
@@ -301,9 +306,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
withBGApi { withBGApi {
val cInfo = chat.chatInfo val cInfo = chat.chatInfo
if (cInfo is ChatInfo.Direct) { if (cInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId) chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
chatModel.showCallView.value = true chatModel.showCallView.value = true
chatModel.callCommand.add(WCallCommand.Capabilities(media)) chatModel.callCommand.add(WCallCommand.Capabilities(media))
} }
@@ -401,15 +404,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel)
} }
ModalManager.end.closeModals() ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(endButtons = { ModalManager.end.showModal(endButtons = {
ShareButton { ShareButton {
clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get())) clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
} }
}) { close -> }) {
ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
KeyChangeEffect(chatModel.chatId.value) {
close()
}
} }
} }
} }
@@ -495,6 +495,7 @@ fun ChatLayout(
composeView: (@Composable () -> Unit), composeView: (@Composable () -> Unit),
attachmentOption: MutableState<AttachmentOption?>, attachmentOption: MutableState<AttachmentOption?>,
attachmentBottomSheetState: ModalBottomSheetState, attachmentBottomSheetState: ModalBottomSheetState,
chatItems: List<ChatItem>,
searchValue: State<String>, searchValue: State<String>,
useLinkPreviews: Boolean, useLinkPreviews: Boolean,
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
@@ -581,7 +582,7 @@ fun ChatLayout(
.padding(contentPadding) .padding(contentPadding)
) { ) {
ChatItemsList( ChatItemsList(
chat, unreadCount, composeState, searchValue, chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
@@ -646,7 +647,7 @@ fun ChatInfoToolbar(
} }
} }
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.mergedPreferences.calls.enabled.forUser) { if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
if (activeCall == null) { if (activeCall == null) {
barButtons.add { barButtons.add {
if (appPlatform.isAndroid) { if (appPlatform.isAndroid) {
@@ -675,7 +676,7 @@ fun ChatInfoToolbar(
} }
} }
} }
} else if (activeCall?.contact?.id == chat.id && appPlatform.isDesktop) { } else if (activeCall?.contact?.id == chat.id) {
barButtons.add { barButtons.add {
val call = remember { chatModel.activeCall }.value val call = remember { chatModel.activeCall }.value
val connectedAt = call?.connectedAt val connectedAt = call?.connectedAt
@@ -839,6 +840,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
chat: Chat, chat: Chat,
unreadCount: State<Int>, unreadCount: State<Int>,
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
chatItems: List<ChatItem>,
searchValue: State<String>, searchValue: State<String>,
useLinkPreviews: Boolean, useLinkPreviews: Boolean,
linkMode: SimplexLinkMode, linkMode: SimplexLinkMode,
@@ -867,7 +869,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
ScrollToBottom(chat.id, listState, chatModel.chatItems) ScrollToBottom(chat.id, listState, chatItems)
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) } var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
// Scroll to bottom when search value changes from something to nothing and back // Scroll to bottom when search value changes from something to nothing and back
LaunchedEffect(searchValue.value.isEmpty()) { LaunchedEffect(searchValue.value.isEmpty()) {
@@ -884,7 +886,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages) PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages)
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(8.dp))
val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } } val reversedChatItems by remember { derivedStateOf { chatItems.reversed().toList() } }
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() } val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
val scrollToItem: (Long) -> Unit = { itemId: Long -> val scrollToItem: (Long) -> Unit = { itemId: Long ->
val index = reversedChatItems.indexOfFirst { it.id == itemId } val index = reversedChatItems.indexOfFirst { it.id == itemId }
@@ -937,7 +939,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
} }
} }
val provider = { val provider = {
providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed -> providerForGallery(i, chatItems, cItem.id) { indexInReversed ->
scope.launch { scope.launch {
listState.scrollToItem( listState.scrollToItem(
kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1), kotlin.math.min(reversedChatItems.lastIndex, indexInReversed + 1),
@@ -1060,11 +1062,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
} }
} }
} }
FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState) FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
} }
@Composable @Composable
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: State<List<ChatItem>>) { private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: List<ChatItem>) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// Helps to scroll to bottom after moving from Group to Direct chat // Helps to scroll to bottom after moving from Group to Direct chat
// and prevents scrolling to bottom on orientation change // and prevents scrolling to bottom on orientation change
@@ -1082,7 +1084,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
* When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise * When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise
* */ * */
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
snapshotFlow { chatItems.value.lastOrNull()?.id } snapshotFlow { chatItems.lastOrNull()?.id }
.distinctUntilChanged() .distinctUntilChanged()
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it } .filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
.collect { .collect {
@@ -1105,7 +1107,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
@Composable @Composable
fun BoxWithConstraintsScope.FloatingButtons( fun BoxWithConstraintsScope.FloatingButtons(
chatItems: State<List<ChatItem>>, chatItems: List<ChatItem>,
unreadCount: State<Int>, unreadCount: State<Int>,
minUnreadItemId: Long, minUnreadItemId: Long,
searchValue: State<String>, searchValue: State<String>,
@@ -1139,11 +1141,10 @@ fun BoxWithConstraintsScope.FloatingButtons(
val bottomUnreadCount by remember { val bottomUnreadCount by remember {
derivedStateOf { derivedStateOf {
if (unreadCount.value == 0) return@derivedStateOf 0 if (unreadCount.value == 0) return@derivedStateOf 0
val items = chatItems.value val from = chatItems.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems
val from = items.lastIndex - firstVisibleIndex - lastIndexOfVisibleItems if (chatItems.size <= from || from < 0) return@derivedStateOf 0
if (items.size <= from || from < 0) return@derivedStateOf 0
items.subList(from, items.size).count { it.isRcvNew } chatItems.subList(from, chatItems.size).count { it.isRcvNew }
} }
} }
val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt() val firstVisibleOffset = (-with(LocalDensity.current) { maxHeight.roundToPx() } * 0.8).toInt()
@@ -1189,7 +1190,7 @@ fun BoxWithConstraintsScope.FloatingButtons(
painterResource(MR.images.ic_check), painterResource(MR.images.ic_check),
onClick = { onClick = {
markRead( markRead(
CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
bottomUnreadCount bottomUnreadCount
) )
showDropDown.value = false showDropDown.value = false
@@ -1494,6 +1495,7 @@ fun PreviewChatLayout() {
composeView = {}, composeView = {},
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) }, attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
chatItems = chatItems,
searchValue, searchValue,
useLinkPreviews = true, useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION, linkMode = SimplexLinkMode.DESCRIPTION,
@@ -1566,6 +1568,7 @@ fun PreviewGroupChatLayout() {
composeView = {}, composeView = {},
attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) }, attachmentOption = remember { mutableStateOf<AttachmentOption?>(null) },
attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden),
chatItems = chatItems,
searchValue, searchValue,
useLinkPreviews = true, useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION, linkMode = SimplexLinkMode.DESCRIPTION,

View File

@@ -267,7 +267,7 @@ fun ComposeView(
fun loadLinkPreview(url: String, wait: Long? = null) { fun loadLinkPreview(url: String, wait: Long? = null) {
if (pendingLinkUrl.value == url) { if (pendingLinkUrl.value == url) {
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null)) composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(null))
withLongRunningApi(slow = 60_000) { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
if (wait != null) delay(wait) if (wait != null) delay(wait)
val lp = getLinkPreview(url) val lp = getLinkPreview(url)
if (lp != null && pendingLinkUrl.value == url) { if (lp != null && pendingLinkUrl.value == url) {
@@ -551,7 +551,7 @@ fun ComposeView(
} }
fun sendMessage(ttl: Int?) { fun sendMessage(ttl: Int?) {
withLongRunningApi(slow = 120_000) { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
sendMessageAsync(null, false, ttl) sendMessageAsync(null, false, ttl)
} }
} }
@@ -583,10 +583,6 @@ fun ComposeView(
} }
fun cancelLinkPreview() { fun cancelLinkPreview() {
val pendingLink = pendingLinkUrl.value
if (pendingLink != null) {
cancelledLinks.add(pendingLink)
}
val uri = composeState.value.linkPreview?.uri val uri = composeState.value.linkPreview?.uri
if (uri != null) { if (uri != null) {
cancelledLinks.add(uri) cancelledLinks.add(uri)
@@ -665,7 +661,7 @@ fun ComposeView(
fun editPrevMessage() { fun editPrevMessage() {
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
val lastEditable = chatModel.chatItems.value.findLast { it.meta.editable } val lastEditable = chatModel.chatItems.findLast { it.meta.editable }
if (lastEditable != null) { if (lastEditable != null) {
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews) composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
} }

View File

@@ -59,6 +59,14 @@ fun SendMsgView(
) { ) {
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) } val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
if (showCustomDisappearingMessageDialog.value) {
CustomDisappearingMessageDialog(
sendMessage = sendMessage,
setShowDialog = { showCustomDisappearingMessageDialog.value = it },
customDisappearingMessageTimePref = customDisappearingMessageTimePref
)
}
Box(Modifier.padding(vertical = 8.dp)) { Box(Modifier.padding(vertical = 8.dp)) {
val cs = composeState.value val cs = composeState.value
var progressByTimeout by rememberSaveable { mutableStateOf(false) } var progressByTimeout by rememberSaveable { mutableStateOf(false) }
@@ -195,11 +203,6 @@ fun SendMsgView(
DefaultDropdownMenu(showDropdown) { DefaultDropdownMenu(showDropdown) {
menuItems.forEach { composable -> composable() } menuItems.forEach { composable -> composable() }
} }
CustomDisappearingMessageDialog(
showCustomDisappearingMessageDialog,
sendMessage = sendMessage,
customDisappearingMessageTimePref = customDisappearingMessageTimePref
)
} else { } else {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage) SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage)
} }
@@ -217,43 +220,93 @@ expect fun VoiceButtonWithoutPermissionByPlatform()
@Composable @Composable
private fun CustomDisappearingMessageDialog( private fun CustomDisappearingMessageDialog(
showMenu: MutableState<Boolean>,
sendMessage: (Int?) -> Unit, sendMessage: (Int?) -> Unit,
setShowDialog: (Boolean) -> Unit,
customDisappearingMessageTimePref: SharedPreference<Int>? customDisappearingMessageTimePref: SharedPreference<Int>?
) { ) {
DefaultDropdownMenu(showMenu) { val showCustomTimePicker = remember { mutableStateOf(false) }
Text(
generalGetString(MR.strings.send_disappearing_message),
Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING * 1.5f),
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
ItemAction(generalGetString(MR.strings.send_disappearing_message_30_seconds)) { if (showCustomTimePicker.value) {
sendMessage(30) val selectedDisappearingMessageTime = remember {
showMenu.value = false mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
} }
ItemAction(generalGetString(MR.strings.send_disappearing_message_1_minute)) { CustomTimePickerDialog(
sendMessage(60) selectedDisappearingMessageTime,
showMenu.value = false title = generalGetString(MR.strings.delete_after),
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send),
confirmButtonAction = { ttl ->
sendMessage(ttl)
customDisappearingMessageTimePref?.set?.invoke(ttl)
setShowDialog(false)
},
cancel = { setShowDialog(false) }
)
} else {
@Composable
fun ChoiceButton(
text: String,
onClick: () -> Unit
) {
TextButton(onClick) {
Text(
text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
} }
ItemAction(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
sendMessage(300) DefaultDialog(onDismissRequest = { setShowDialog(false) }) {
showMenu.value = false Surface(
} shape = RoundedCornerShape(corner = CornerSize(25.dp)),
ItemAction(generalGetString(MR.strings.send_disappearing_message_custom_time)) { contentColor = LocalContentColor.current
showMenu.value = false ) {
val selectedDisappearingMessageTime = mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300) Box(
showCustomTimePickerDialog( contentAlignment = Alignment.Center
selectedDisappearingMessageTime, ) {
title = generalGetString(MR.strings.delete_after), Column(
confirmButtonText = generalGetString(MR.strings.send_disappearing_message_send), modifier = Modifier.padding(DEFAULT_PADDING),
confirmButtonAction = { ttl -> verticalArrangement = Arrangement.spacedBy(6.dp),
sendMessage(ttl) horizontalAlignment = Alignment.CenterHorizontally
customDisappearingMessageTimePref?.set?.invoke(ttl) ) {
}, Row(
cancel = { showMenu.value = false } modifier = Modifier.fillMaxWidth(),
) horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(" ") // centers title
Text(
generalGetString(MR.strings.send_disappearing_message),
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
Icon(
painterResource(MR.images.ic_close),
generalGetString(MR.strings.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(25.dp)
.clickable { setShowDialog(false) }
)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_30_seconds)) {
sendMessage(30)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_1_minute)) {
sendMessage(60)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_5_minutes)) {
sendMessage(300)
setShowDialog(false)
}
ChoiceButton(generalGetString(MR.strings.send_disappearing_message_custom_time)) {
showCustomTimePicker.value = true
}
}
}
}
} }
} }
} }

View File

@@ -54,7 +54,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
}, },
inviteMembers = { inviteMembers = {
allowModifyMembers = false allowModifyMembers = false
withLongRunningApi(slow = 120_000) { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
for (contactId in selectedContacts) { for (contactId in selectedContacts) {
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value) val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
if (member != null) { if (member != null) {
@@ -86,7 +86,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
.map { it.chatInfo } .map { it.chatInfo }
.filterIsInstance<ChatInfo.Direct>() .filterIsInstance<ChatInfo.Direct>()
.map { it.contact } .map { it.contact }
.filter { c -> c.ready && c.active && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) } .filter { it.contactId !in memberContactIds && it.chatViewName.lowercase().contains(s) }
.sortedBy { it.displayName.lowercase() } .sortedBy { it.displayName.lowercase() }
.toList() .toList()
} }

View File

@@ -152,7 +152,7 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved), text = generalGetString(MR.strings.you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved),
confirmText = generalGetString(MR.strings.leave_group_button), confirmText = generalGetString(MR.strings.leave_group_button),
onConfirm = { onConfirm = {
withLongRunningApi(60_000) { withBGApi {
chatModel.controller.leaveGroup(rhId, groupInfo.groupId) chatModel.controller.leaveGroup(rhId, groupInfo.groupId)
close?.invoke() close?.invoke()
} }
@@ -424,47 +424,69 @@ private fun MemberVerifiedShield() {
@Composable @Composable
private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) { private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState<Boolean>) {
if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) { // revert from this:
val canBlockForAll = member.canBlockForAll(groupInfo) DefaultDropdownMenu(showMenu) {
val canRemove = member.canBeRemoved(groupInfo) if (member.canBeRemoved(groupInfo)) {
if (canBlockForAll || canRemove) { ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
DefaultDropdownMenu(showMenu) { removeMemberAlert(rhId, groupInfo, member)
if (canBlockForAll) { showMenu.value = false
if (member.blockedByAdmin) { })
ItemAction(stringResource(MR.strings.unblock_for_all), painterResource(MR.images.ic_do_not_touch), onClick = {
unblockForAllAlert(rhId, groupInfo, member)
showMenu.value = false
})
} else {
ItemAction(stringResource(MR.strings.block_for_all), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
blockForAllAlert(rhId, groupInfo, member)
showMenu.value = false
})
}
}
if (canRemove) {
ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
removeMemberAlert(rhId, groupInfo, member)
showMenu.value = false
})
}
}
} }
} else if (!member.blockedByAdmin) { if (member.memberSettings.showMessages) {
DefaultDropdownMenu(showMenu) { ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
if (member.memberSettings.showMessages) { blockMemberAlert(rhId, groupInfo, member)
ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = { showMenu.value = false
blockMemberAlert(rhId, groupInfo, member) })
showMenu.value = false } else {
}) ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = {
} else { unblockMemberAlert(rhId, groupInfo, member)
ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = { showMenu.value = false
unblockMemberAlert(rhId, groupInfo, member) })
showMenu.value = false
})
}
} }
} }
// revert to this: vvv
// if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
// val canBlockForAll = member.canBlockForAll(groupInfo)
// val canRemove = member.canBeRemoved(groupInfo)
// if (canBlockForAll || canRemove) {
// DefaultDropdownMenu(showMenu) {
// if (canBlockForAll) {
// if (member.blockedByAdmin) {
// ItemAction(stringResource(MR.strings.unblock_for_all), painterResource(MR.images.ic_do_not_touch), onClick = {
// unblockForAllAlert(rhId, groupInfo, member)
// showMenu.value = false
// })
// } else {
// ItemAction(stringResource(MR.strings.block_for_all), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
// blockForAllAlert(rhId, groupInfo, member)
// showMenu.value = false
// })
// }
// }
// if (canRemove) {
// ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
// removeMemberAlert(rhId, groupInfo, member)
// showMenu.value = false
// })
// }
// }
// }
// } else if (!member.blockedByAdmin) {
// DefaultDropdownMenu(showMenu) {
// if (member.memberSettings.showMessages) {
// ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = {
// blockMemberAlert(rhId, groupInfo, member)
// showMenu.value = false
// })
// } else {
// ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = {
// unblockMemberAlert(rhId, groupInfo, member)
// showMenu.value = false
// })
// }
// }
// }
// ^^^
} }
@Composable @Composable

View File

@@ -3,9 +3,11 @@ package chat.simplex.common.views.chat.group
import InfoRow import InfoRow
import SectionBottomSpacer import SectionBottomSpacer
import SectionDividerSpaced import SectionDividerSpaced
import SectionItemView
import SectionSpacer import SectionSpacer
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import TextIconSpaced
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import java.net.URI import java.net.URI
import androidx.compose.foundation.* import androidx.compose.foundation.*
@@ -72,8 +74,9 @@ fun GroupMemberInfoView(
if (chatModel.getContactChat(it) == null) { if (chatModel.getContactChat(it) == null) {
chatModel.addChat(c) chatModel.addChat(c)
} }
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear() chatModel.chatItemStatuses.clear()
chatModel.chatItems.replaceAll(c.chatItems) chatModel.chatItems.addAll(c.chatItems)
chatModel.chatId.value = c.id chatModel.chatId.value = c.id
closeAll() closeAll()
} }
@@ -387,11 +390,25 @@ fun GroupMemberInfoLayout(
} }
} }
if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) { // revert from this:
AdminDestructiveSection() SectionDividerSpaced(maxBottomPadding = false)
} else { SectionView {
NonAdminBlockSection() if (member.memberSettings.showMessages) {
BlockMemberButton(blockMember)
} else {
UnblockMemberButton(unblockMember)
}
if (member.canBeRemoved(groupInfo)) {
RemoveMemberButton(removeMember)
}
} }
// revert to this: vvv
// if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) {
// AdminDestructiveSection()
// } else {
// NonAdminBlockSection()
// }
// ^^^
if (developerTools) { if (developerTools) {
SectionDividerSpaced() SectionDividerSpaced()

View File

@@ -3,7 +3,6 @@ package chat.simplex.common.views.chat.group
import SectionBottomSpacer import SectionBottomSpacer
import SectionDividerSpaced import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionTextFooter
import SectionView import SectionView
import TextIconSpaced import TextIconSpaced
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -15,7 +14,6 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@@ -29,13 +27,9 @@ import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.GroupInfo import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.chatJsonLength
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
private const val maxByteCount = 1200
@Composable @Composable
fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) { fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () -> Unit) {
var gInfo by remember { mutableStateOf(groupInfo) } var gInfo by remember { mutableStateOf(groupInfo) }
@@ -60,11 +54,8 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
ModalView( ModalView(
close = { close = {
when { if (welcomeText.value == gInfo.groupProfile.description || (welcomeText.value == "" && gInfo.groupProfile.description == null)) close()
welcomeTextUnchanged(welcomeText, gInfo) -> close() else showUnsavedChangesAlert({ save(close) }, close)
!welcomeTextFitsLimit(welcomeText) -> showUnsavedChangesTooLongAlert(close)
else -> showUnsavedChangesAlert({ save(close) }, close)
}
}, },
) { ) {
GroupWelcomeLayout( GroupWelcomeLayout(
@@ -76,14 +67,6 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
} }
} }
private fun welcomeTextUnchanged(welcomeText: MutableState<String>, groupInfo: GroupInfo): Boolean {
return welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)
}
private fun welcomeTextFitsLimit(welcomeText: MutableState<String>): Boolean {
return chatJsonLength(welcomeText.value) <= maxByteCount
}
@Composable @Composable
private fun GroupWelcomeLayout( private fun GroupWelcomeLayout(
welcomeText: MutableState<String>, welcomeText: MutableState<String>,
@@ -112,13 +95,6 @@ private fun GroupWelcomeLayout(
} else { } else {
TextPreview(wt.value, linkMode) TextPreview(wt.value, linkMode)
} }
SectionTextFooter(
if (!welcomeTextFitsLimit(wt)) { generalGetString(MR.strings.message_too_large) } else "",
color = if (welcomeTextFitsLimit(wt)) MaterialTheme.colors.secondary else Color.Red
)
Spacer(Modifier.size(8.dp))
ChangeModeButton( ChangeModeButton(
editMode.value, editMode.value,
click = { click = {
@@ -128,18 +104,10 @@ private fun GroupWelcomeLayout(
) )
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) } CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) }
SectionDividerSpaced(maxBottomPadding = false)
Divider(
Modifier.padding(
start = DEFAULT_PADDING_HALF,
top = 8.dp,
end = DEFAULT_PADDING_HALF,
bottom = 8.dp)
)
SaveButton( SaveButton(
save = save, save = save,
disabled = welcomeTextUnchanged(wt, groupInfo) || !welcomeTextFitsLimit(wt) disabled = wt.value == groupInfo.groupProfile.description || (wt.value == "" && groupInfo.groupProfile.description == null)
) )
} else { } else {
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
@@ -214,11 +182,3 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
onDismiss = revert, onDismiss = revert,
) )
} }
private fun showUnsavedChangesTooLongAlert(revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(MR.strings.welcome_message_is_too_long),
confirmText = generalGetString(MR.strings.exit_without_saving),
onConfirm = revert,
)
}

View File

@@ -94,7 +94,7 @@ fun CIFileView(
FileProtocol.LOCAL -> {} FileProtocol.LOCAL -> {}
} }
file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> { file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> {
withLongRunningApi(slow = 600_000) { withLongRunningApi(slow = 60_000, deadlock = 600_000) {
var filePath = getLoadedFilePath(file) var filePath = getLoadedFilePath(file)
if (chatModel.connectedToRemote() && filePath == null) { if (chatModel.connectedToRemote() && filePath == null) {
file.loadRemoteFile(true) file.loadRemoteFile(true)

View File

@@ -41,7 +41,7 @@ fun CIVideoView(
val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) } val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) }
if (chatModel.connectedToRemote()) { if (chatModel.connectedToRemote()) {
LaunchedEffect(file) { LaunchedEffect(file) {
withLongRunningApi(slow = 600_000) { withLongRunningApi(slow = 60_000, deadlock = 600_000) {
if (file != null && file.loaded && getLoadedFilePath(file) == null) { if (file != null && file.loaded && getLoadedFilePath(file) == null) {
file.loadRemoteFile(false) file.loadRemoteFile(false)
filePath.value = getLoadedFilePath(file) filePath.value = getLoadedFilePath(file)

View File

@@ -103,7 +103,7 @@ fun ChatItemView(
setReaction(cInfo, cItem, !r.userReacted, r.reaction) setReaction(cInfo, cItem, !r.userReacted, r.reaction)
} }
} }
Row(modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) { Row(modifier.padding(2.dp)) {
ReactionIcon(r.reaction.text, fontSize = 12.sp) ReactionIcon(r.reaction.text, fontSize = 12.sp)
if (r.totalReacted > 1) { if (r.totalReacted > 1) {
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
@@ -112,6 +112,7 @@ fun ChatItemView(
fontSize = 11.5.sp, fontSize = 11.5.sp,
fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal, fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal,
color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = if (appPlatform.isAndroid) Modifier else Modifier.padding(top = 4.dp)
) )
} }
} }
@@ -177,8 +178,7 @@ fun ChatItemView(
fun MsgContentItemDropdownMenu() { fun MsgContentItemDropdownMenu() {
val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file) val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file)
when { when {
// cItem.id check is a special case for live message chat item which has negative ID while not sent yet cItem.content.msgContent != null -> {
cItem.content.msgContent != null && cItem.id >= 0 -> {
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(showMenu) {
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) { if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
MsgReactionsMenu() MsgReactionsMenu()
@@ -213,7 +213,7 @@ fun ChatItemView(
showMenu.value = false showMenu.value = false
} }
if (chatModel.connectedToRemote() && fileSource == null) { if (chatModel.connectedToRemote() && fileSource == null) {
withLongRunningApi(slow = 600_000) { withLongRunningApi(slow = 60_000, deadlock = 600_000) {
cItem.file?.loadRemoteFile(true) cItem.file?.loadRemoteFile(true)
fileSource = getLoadedFileSource(cItem.file) fileSource = getLoadedFileSource(cItem.file)
shareIfExists() shareIfExists()
@@ -527,9 +527,8 @@ fun DeleteItemAction(
val range = chatViewItemsRange(currIndex, prevHidden) val range = chatViewItemsRange(currIndex, prevHidden)
if (range != null) { if (range != null) {
val itemIds: ArrayList<Long> = arrayListOf() val itemIds: ArrayList<Long> = arrayListOf()
val reversedChatItems = chatModel.chatItems.asReversed()
for (i in range) { for (i in range) {
itemIds.add(reversedChatItems[i].id) itemIds.add(chatModel.chatItems.asReversed()[i].id)
} }
deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages) deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages)
} else { } else {
@@ -652,23 +651,6 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo
} }
} }
@Composable
fun ItemAction(text: String, color: Color = Color.Unspecified, onClick: () -> Unit) {
val finalColor = if (color == Color.Unspecified) {
MenuTextColor
} else color
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
Text(
text,
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = finalColor
)
}
}
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) { fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
title = generalGetString(cancelAction.alert.titleId), title = generalGetString(cancelAction.alert.titleId),

View File

@@ -91,7 +91,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState<B
) )
} }
fun markedDeletedText(meta: CIMeta): String = private fun markedDeletedText(meta: CIMeta): String =
when (meta.itemDeleted) { when (meta.itemDeleted) {
is CIDeleted.Moderated -> is CIDeleted.Moderated ->
String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName) String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName)

View File

@@ -212,15 +212,18 @@ suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) {
} }
suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) { suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) {
Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId) val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId)
if (chat != null) { if (chat != null) {
openLoadedChat(chat, chatModel) openLoadedChat(chat, chatModel)
Log.d(TAG, "TODOCHAT: openChat: opened ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
} }
} }
fun openLoadedChat(chat: Chat, chatModel: ChatModel) { fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
chatModel.chatItems.clear()
chatModel.chatItemStatuses.clear() chatModel.chatItemStatuses.clear()
chatModel.chatItems.replaceAll(chat.chatItems) chatModel.chatItems.addAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id chatModel.chatId.value = chat.chatInfo.id
} }
@@ -236,7 +239,8 @@ suspend fun apiFindMessages(ch: Chat, chatModel: ChatModel, search: String) {
val chatInfo = ch.chatInfo val chatInfo = ch.chatInfo
val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return val chat = chatModel.controller.apiGetChat(ch.remoteHostId, chatInfo.chatType, chatInfo.apiId, search = search) ?: return
if (chatModel.chatId.value != chat.id) return if (chatModel.chatId.value != chat.id) return
chatModel.chatItems.replaceAll(chat.chatItems) chatModel.chatItems.clear()
chatModel.chatItems.addAll(0, chat.chatItems)
} }
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {

View File

@@ -29,7 +29,6 @@ import chat.simplex.common.views.onboarding.WhatsNewView
import chat.simplex.common.views.onboarding.shouldShowWhatsNew import chat.simplex.common.views.onboarding.shouldShowWhatsNew
import chat.simplex.common.views.usersettings.SettingsView import chat.simplex.common.views.usersettings.SettingsView
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.call.Call
import chat.simplex.common.views.newchat.* import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -122,12 +121,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
} }
} }
if (searchText.value.text.isEmpty()) { if (searchText.value.text.isEmpty()) {
if (appPlatform.isDesktop) { DesktopActiveCallOverlayLayout(newChatSheetState)
val call = remember { chatModel.activeCall }.value
if (call != null) {
ActiveCallInteractiveArea(call, newChatSheetState)
}
}
// TODO disable this button and sheet for the duration of the switch // TODO disable this button and sheet for the duration of the switch
tryOrShowError("NewChatSheet", error = {}) { tryOrShowError("NewChatSheet", error = {}) {
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
@@ -320,7 +314,7 @@ private fun ToggleFilterDisabledButton() {
} }
@Composable @Composable
expect fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow<AnimatedViewState>) expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>)
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) { fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
Log.d(TAG, "connectIfOpenedViaUri: opened via link") Log.d(TAG, "connectIfOpenedViaUri: opened via link")

View File

@@ -26,7 +26,6 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.GroupInfo import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.chatModel import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chat.item.markedDeletedText
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.ImageResource
@@ -171,7 +170,7 @@ fun ChatPreviewView(
val (text: CharSequence, inlineTextContent) = when { val (text: CharSequence, inlineTextContent) = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) } chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
ci.meta.itemDeleted == null -> ci.text to null ci.meta.itemDeleted == null -> ci.text to null
else -> markedDeletedText(ci.meta) to null else -> generalGetString(MR.strings.marked_deleted_description) to null
} }
val formattedText = when { val formattedText = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> null chatModelDraftChatId == chat.id && chatModelDraft != null -> null
@@ -287,7 +286,7 @@ fun ChatPreviewView(
Box( Box(
contentAlignment = Alignment.TopEnd contentAlignment = Alignment.TopEnd
) { ) {
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs) val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt)
Text( Text(
ts, ts,
color = MaterialTheme.colors.secondary, color = MaterialTheme.colors.secondary,

View File

@@ -85,7 +85,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
userPickerState.value = AnimatedViewState.VISIBLE userPickerState.value = AnimatedViewState.VISIBLE
} }
} }
else -> NavigationButtonBack(onButtonClicked = { chatModel.sharedContent.value = null }) else -> NavigationButtonBack { chatModel.sharedContent.value = null }
} }
} }
if (chatModel.chats.size >= 8) { if (chatModel.chats.size >= 8) {
@@ -143,7 +143,7 @@ private fun ShareList(chatModel: ChatModel, search: String) {
} }
val chats by remember(search) { val chats by remember(search) {
derivedStateOf { derivedStateOf {
if (search.isEmpty()) chatModel.chats.toList().filter { it.chatInfo.ready } else chatModel.chats.toList().filter { it.chatInfo.ready }.filter(filter) if (search.isEmpty()) chatModel.chats.filter { it.chatInfo.ready } else chatModel.chats.filter { it.chatInfo.ready }.filter(filter)
} }
} }
LazyColumn( LazyColumn(

View File

@@ -62,7 +62,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
initialRandomDBPassphrase, initialRandomDBPassphrase,
progressIndicator, progressIndicator,
onConfirmEncrypt = { onConfirmEncrypt = {
withLongRunningApi { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator) encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
} }
} }
@@ -233,13 +233,13 @@ fun resetFormAfterEncryption(
storedKey: MutableState<Boolean>, storedKey: MutableState<Boolean>,
stored: Boolean = false, stored: Boolean = false,
) { ) {
m.chatDbEncrypted.value = true
initialRandomDBPassphrase.value = false
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
currentKey.value = "" currentKey.value = ""
newKey.value = "" newKey.value = ""
confirmNewKey.value = "" confirmNewKey.value = ""
storedKey.value = stored storedKey.value = stored
m.chatDbEncrypted.value = true
initialRandomDBPassphrase.value = false
m.controller.appPrefs.initialRandomDBPassphrase.set(false)
} }
fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences) { fun setUseKeychain(value: Boolean, useKeychain: MutableState<Boolean>, prefs: AppPreferences) {
@@ -392,11 +392,12 @@ suspend fun encryptDatabase(
false false
} }
else -> { else -> {
val new = newKey.value prefs.initialRandomDBPassphrase.set(false)
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value) initialRandomDBPassphrase.value = false
if (useKeychain.value) { if (useKeychain.value) {
DatabaseUtils.ksDatabasePassword.set(new) DatabaseUtils.ksDatabasePassword.set(newKey.value)
} }
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
operationEnded(m, progressIndicator) { operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted)) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
} }

View File

@@ -368,7 +368,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
} }
fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>, progressIndicator: MutableState<Boolean>? = null) { fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>, progressIndicator: MutableState<Boolean>? = null) {
withLongRunningApi { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
try { try {
progressIndicator?.value = true progressIndicator?.value = true
if (chatDbChanged.value) { if (chatDbChanged.value) {
@@ -581,7 +581,7 @@ private fun importArchive(
progressIndicator.value = true progressIndicator.value = true
val archivePath = saveArchiveFromURI(importedArchiveURI) val archivePath = saveArchiveFromURI(importedArchiveURI)
if (archivePath != null) { if (archivePath != null) {
withLongRunningApi { withLongRunningApi(slow = 60_000, deadlock = 180_000) {
try { try {
m.controller.apiDeleteStorage() m.controller.apiDeleteStorage()
try { try {

View File

@@ -12,7 +12,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.* import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -23,8 +22,6 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
class AlertManager { class AlertManager {
@@ -131,8 +128,6 @@ class AlertManager {
) { ) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
delay(200)
focusRequester.requestFocus() focusRequester.requestFocus()
} }
TextButton(onClick = { TextButton(onClick = {
@@ -191,7 +186,6 @@ class AlertManager {
title: String, text: String? = null, title: String, text: String? = null,
confirmText: String = generalGetString(MR.strings.ok), confirmText: String = generalGetString(MR.strings.ok),
hostDevice: Pair<Long?, String>? = null, hostDevice: Pair<Long?, String>? = null,
shareText: Boolean? = null
) { ) {
showAlert { showAlert {
AlertDialog( AlertDialog(
@@ -201,23 +195,12 @@ class AlertManager {
AlertContent(text, hostDevice, extraPadding = true) { AlertContent(text, hostDevice, extraPadding = true) {
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
delay(200)
focusRequester.requestFocus() focusRequester.requestFocus()
} }
// Can pass shareText = false to prevent showing Share button if it's needed in a specific case
val showShareButton = text != null && (shareText == true || (shareText == null && text.length > 500))
Row( Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = if (showShareButton) Arrangement.SpaceBetween else Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
val clipboard = LocalClipboardManager.current
if (showShareButton && text != null) {
TextButton(onClick = {
clipboard.shareText(text)
hideAlert()
}) { Text(stringResource(MR.strings.share_verb)) }
}
TextButton( TextButton(
onClick = { onClick = {
hideAlert() hideAlert()

View File

@@ -27,8 +27,8 @@ fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme
val icon = val icon =
when (chatInfo) { when (chatInfo) {
is ChatInfo.Group -> MR.images.ic_supervised_user_circle_filled is ChatInfo.Group -> MR.images.ic_supervised_user_circle_filled
is ChatInfo.Local -> MR.images.ic_folder_filled is ChatInfo.Direct -> MR.images.ic_account_circle_filled
else -> MR.images.ic_account_circle_filled else -> MR.images.ic_folder_filled
} }
ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor) ProfileImage(size, chatInfo.image, icon, if (chatInfo is ChatInfo.Local) NoteFolderIconColor else iconColor)
} }

View File

@@ -18,7 +18,7 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
@Composable @Composable
fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, endButtons: @Composable RowScope.() -> Unit = {}) { fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) {
Column( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -35,7 +35,7 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (showClose) { if (showClose) {
NavigationButtonBack(tintColor = tintColor, onButtonClicked = close) NavigationButtonBack(onButtonClicked = close)
} else { } else {
Spacer(Modifier) Spacer(Modifier)
} }

View File

@@ -1,21 +1,116 @@
package chat.simplex.common.views.helpers package chat.simplex.common.views.helpers
import SectionItemView import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.model.CustomTimeUnit import chat.simplex.common.model.CustomTimeUnit
import chat.simplex.common.model.timeText import chat.simplex.common.model.timeText
import chat.simplex.res.MR import chat.simplex.res.MR
import com.sd.lib.compose.wheel_picker.*
@Composable @Composable
expect fun CustomTimePicker( fun CustomTimePicker(
selection: MutableState<Int>, selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits
) ) {
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
}
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
val selectedDuration = remember { mutableStateOf(duration) }
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
val isTriggered = remember { mutableStateOf(false) }
LaunchedEffect(selectedUnit.value) {
// on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue
// (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120),
// selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition
if (isTriggered.value) {
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
if (maxValue != null && selectedDuration.value > maxValue) {
selectedDuration.value = maxValue
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
} else {
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
} else {
isTriggered.value = true
}
}
LaunchedEffect(selectedDuration.value) {
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.spacedBy(0.dp)
) {
Column(Modifier.weight(1f)) {
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
FVerticalWheelPicker(
count = selectedUnitValues.value.count(),
state = durationPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
selectedUnitValues.value[index].toString(),
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(durationPickerState) {
snapshotFlow { durationPickerState.currentIndex }
.collect {
selectedDuration.value = selectedUnitValues.value[it]
}
}
}
Column(Modifier.weight(1f)) {
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
FVerticalWheelPicker(
count = timeUnitsLimits.count(),
state = unitPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
timeUnitsLimits[index].timeUnit.text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(unitPickerState) {
snapshotFlow { unitPickerState.currentIndex }
.collect {
selectedUnit.value = timeUnitsLimits[it].timeUnit
}
}
}
}
}
data class TimeUnitLimits( data class TimeUnitLimits(
val timeUnit: CustomTimeUnit, val timeUnit: CustomTimeUnit,
@@ -46,7 +141,8 @@ data class TimeUnitLimits(
} }
} }
fun showCustomTimePickerDialog( @Composable
fun CustomTimePickerDialog(
selection: MutableState<Int>, selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits, timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
title: String, title: String,
@@ -54,26 +150,53 @@ fun showCustomTimePickerDialog(
confirmButtonAction: (Int) -> Unit, confirmButtonAction: (Int) -> Unit,
cancel: () -> Unit cancel: () -> Unit
) { ) {
AlertManager.shared.showAlertDialogButtonsColumn( DefaultDialog(onDismissRequest = cancel) {
title = title, Surface(
onDismissRequest = cancel shape = RoundedCornerShape(corner = CornerSize(25.dp)),
) { contentColor = LocalContentColor.current
Column(horizontalAlignment = Alignment.CenterHorizontally) { ) {
CustomTimePicker( Box(
selection, contentAlignment = Alignment.Center
timeUnitsLimits
)
SectionItemView({
AlertManager.shared.hideAlert()
confirmButtonAction(selection.value)
}
) { ) {
Text( Column(
confirmButtonText, modifier = Modifier.padding(DEFAULT_PADDING),
Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(6.dp),
textAlign = TextAlign.Center, horizontalAlignment = Alignment.CenterHorizontally
color = MaterialTheme.colors.primary ) {
) Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(" ") // centers title
Text(
title,
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
Icon(
painterResource(MR.images.ic_close),
generalGetString(MR.strings.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(25.dp)
.clickable { cancel() }
)
}
CustomTimePicker(
selection,
timeUnitsLimits
)
TextButton(onClick = { confirmButtonAction(selection.value) }) {
Text(
confirmButtonText,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
}
} }
} }
} }
@@ -97,6 +220,7 @@ fun DropdownCustomTimePickerSettingRow(
val dropdownSelection: MutableState<DropdownSelection> = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) } val dropdownSelection: MutableState<DropdownSelection> = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) }
val values: MutableState<List<DropdownSelection>> = remember { mutableStateOf(getValues(selection.value)) } val values: MutableState<List<DropdownSelection>> = remember { mutableStateOf(getValues(selection.value)) }
val showCustomTimePicker = remember { mutableStateOf(false) }
fun updateValue(selectedValue: Int?) { fun updateValue(selectedValue: Int?) {
values.value = getValues(selectedValue) values.value = getValues(selectedValue)
@@ -123,22 +247,28 @@ fun DropdownCustomTimePickerSettingRow(
onSelected = { sel: DropdownSelection -> onSelected = { sel: DropdownSelection ->
when (sel) { when (sel) {
is DropdownSelection.DropdownValue -> updateValue(sel.value) is DropdownSelection.DropdownValue -> updateValue(sel.value)
DropdownSelection.Custom -> { DropdownSelection.Custom -> showCustomTimePicker.value = true
val selectedCustomTime = mutableStateOf(selection.value ?: 86400)
showCustomTimePickerDialog(
selectedCustomTime,
timeUnitsLimits = customPickerTimeUnitsLimits,
title = customPickerTitle,
confirmButtonText = customPickerConfirmButtonText,
confirmButtonAction = ::updateValue,
cancel = {
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
}
)
}
} }
} }
) )
if (showCustomTimePicker.value) {
val selectedCustomTime = remember { mutableStateOf(selection.value ?: 86400) }
CustomTimePickerDialog(
selectedCustomTime,
timeUnitsLimits = customPickerTimeUnitsLimits,
title = customPickerTitle,
confirmButtonText = customPickerConfirmButtonText,
confirmButtonAction = { time ->
updateValue(time)
showCustomTimePicker.value = false
},
cancel = {
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
showCustomTimePicker.value = false
}
)
}
} }
private sealed class DropdownSelection { private sealed class DropdownSelection {

View File

@@ -5,7 +5,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.ui.theme.DEFAULT_PADDING
@@ -21,7 +20,7 @@ fun DefaultProgressView(description: String?) {
strokeWidth = 2.5.dp strokeWidth = 2.5.dp
) )
if (description != null) { if (description != null) {
Text(description, textAlign = TextAlign.Center) Text(description)
} }
} }
} }

View File

@@ -44,10 +44,10 @@ fun DefaultTopAppBar(
} }
@Composable @Composable
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) { fun NavigationButtonBack(onButtonClicked: (() -> Unit)?) {
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) { IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
Icon( Icon(
painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), tint = tintColor painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), tint = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
) )
} }
} }

View File

@@ -29,7 +29,7 @@ fun ModalView(
} }
Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) { Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) {
Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) { Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) {
CloseSheetBar(close, showClose, endButtons = endButtons) CloseSheetBar(close, showClose, endButtons)
Box(modifier) { content() } Box(modifier) { content() }
} }
} }
@@ -61,10 +61,10 @@ class ModalManager(private val placement: ModalPlacement? = null) {
} }
} }
fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable ModalData.(close: () -> Unit) -> Unit) {
val data = ModalData() val data = ModalData()
showCustomModal { close -> showCustomModal { close ->
ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) }) ModalView(close, showClose = showClose, content = { data.content(close) })
} }
} }

View File

@@ -16,7 +16,7 @@ class ProcessedErrors <T: AgentErrorType>(val interval: Long) {
fun newError(error: T, offerRestart: Boolean) { fun newError(error: T, offerRestart: Boolean) {
timer.cancel() timer.cancel()
timer = withLongRunningApi(slow = 130_000) { timer = withLongRunningApi(slow = 70_000, deadlock = 130_000) {
val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis() val delayBeforeNext = (lastShownTimestamp + interval) - System.currentTimeMillis()
if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) { if ((lastShownOfferRestart || !offerRestart) && delayBeforeNext >= 0) {
delay(delayBeforeNext) delay(delayBeforeNext)

View File

@@ -22,7 +22,6 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.* import androidx.compose.ui.text.input.*
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.res.MR import chat.simplex.res.MR
@@ -96,7 +95,7 @@ fun SearchTextField(
value = searchText.value.text, value = searchText.value.text,
innerTextField = innerTextField, innerTextField = innerTextField,
placeholder = { placeholder = {
Text(placeholder, maxLines = 1, overflow = TextOverflow.Ellipsis) Text(placeholder)
}, },
trailingIcon = if (searchText.value.text.isNotEmpty()) {{ trailingIcon = if (searchText.value.text.isNotEmpty()) {{
IconButton({ IconButton({

View File

@@ -198,16 +198,16 @@ fun <T> SectionItemWithValue(
} }
@Composable @Composable
fun SectionTextFooter(text: String, color: Color = MaterialTheme.colors.secondary) { fun SectionTextFooter(text: String) {
SectionTextFooter(AnnotatedString(text), color = color) SectionTextFooter(AnnotatedString(text))
} }
@Composable @Composable
fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start, color: Color = MaterialTheme.colors.secondary) { fun SectionTextFooter(text: AnnotatedString, textAlign: TextAlign = TextAlign.Start) {
Text( Text(
text, text,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F), Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
color = color, color = MaterialTheme.colors.secondary,
lineHeight = 18.sp, lineHeight = 18.sp,
fontSize = 14.sp, fontSize = 14.sp,
textAlign = textAlign textAlign = textAlign

View File

@@ -37,22 +37,32 @@ fun withBGApi(action: suspend CoroutineScope.() -> Unit): Job =
CoroutineScope(singleThreadDispatcher).launch(block = { wrapWithLogging(action, it) }) CoroutineScope(singleThreadDispatcher).launch(block = { wrapWithLogging(action, it) })
} }
fun withLongRunningApi(slow: Long = Long.MAX_VALUE, action: suspend CoroutineScope.() -> Unit): Job = fun withLongRunningApi(slow: Long = Long.MAX_VALUE, deadlock: Long = Long.MAX_VALUE, action: suspend CoroutineScope.() -> Unit): Job =
Exception().let { Exception().let {
CoroutineScope(Dispatchers.Default).launch(block = { wrapWithLogging(action, it, slow = slow) }) CoroutineScope(Dispatchers.Default).launch(block = { wrapWithLogging(action, it, slow = slow, deadlock = deadlock) })
} }
private suspend fun wrapWithLogging(action: suspend CoroutineScope.() -> Unit, exception: java.lang.Exception, slow: Long = 20_000) = coroutineScope { suspend fun withSingleThreadContext(action: suspend CoroutineScope.() -> Unit) = withContext(singleThreadDispatcher, action)
private suspend fun wrapWithLogging(action: suspend CoroutineScope.() -> Unit, exception: java.lang.Exception, slow: Long = 10_000, deadlock: Long = 60_000) = coroutineScope {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val job = launch {
delay(deadlock)
Log.e(TAG, "Possible deadlock of the thread, not finished after ${deadlock / 1000}s:\n${exception.stackTraceToString()}")
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_deadlock_title),
text = generalGetString(MR.strings.possible_deadlock_desc).format(deadlock / 1000, exception.stackTraceToString()),
)
}
action() action()
val end = System.currentTimeMillis() job.cancel()
if (end - start > slow) { if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}") val end = System.currentTimeMillis()
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) { if (end - start > slow) {
Log.e(TAG, "Possible problem with execution of the thread, took ${(end - start) / 1000}s:\n${exception.stackTraceToString()}")
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_slow_function_title), title = generalGetString(MR.strings.possible_slow_function_title),
text = generalGetString(MR.strings.possible_slow_function_desc).format((end - start) / 1000, exception.stackTraceToString()), text = generalGetString(MR.strings.possible_slow_function_desc).format((end - start) / 1000, exception.stackTraceToString()),
shareText = true
) )
} }
} }
@@ -411,7 +421,7 @@ expect fun ByteArray.toBase64StringForPassphrase(): String
// Android's default implementation that was used before multiplatform, adds non-needed characters at the end of string // Android's default implementation that was used before multiplatform, adds non-needed characters at the end of string
// which can be bypassed by: // which can be bypassed by:
// fun String.toByteArrayFromBase64(): ByteArray = Base64.getMimeDecoder().decode(this.trimEnd { it == '\n' || it == ' ' }) // fun String.toByteArrayFromBase64(): ByteArray = Base64.getDecoder().decode(this.trimEnd { it == '\n' || it == ' ' })
expect fun String.toByteArrayFromBase64ForPassphrase(): ByteArray expect fun String.toByteArrayFromBase64ForPassphrase(): ByteArray
val LongRange.Companion.saver val LongRange.Companion.saver

View File

@@ -49,7 +49,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
} }
private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) { private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) {
withLongRunningApi { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
try { try {
/** Waiting until [initChatController] finishes */ /** Waiting until [initChatController] finishes */
while (m.ctrlInitInProgress.value) { while (m.ctrlInitInProgress.value) {

View File

@@ -50,7 +50,7 @@ fun SetupDatabasePassphrase(m: ChatModel) {
confirmNewKey, confirmNewKey,
progressIndicator, progressIndicator,
onConfirmEncrypt = { onConfirmEncrypt = {
withLongRunningApi { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
if (m.chatRunning.value == true) { if (m.chatRunning.value == true) {
// Stop chat if it's started before doing anything // Stop chat if it's started before doing anything
stopChatAsync(m) stopChatAsync(m)

View File

@@ -36,7 +36,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
Icon( Icon(
painterResource(MR.images.ic_open_in_new), stringResource(titleId), tint = MaterialTheme.colors.primary, painterResource(MR.images.ic_open_in_new), stringResource(titleId), tint = MaterialTheme.colors.primary,
modifier = Modifier modifier = Modifier
.clickable { if (link.startsWith("simplex:")) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openUriCatching(link) } .clickable { uriHandler.openUriCatching(link) }
) )
} }

View File

@@ -47,6 +47,10 @@ fun NetworkAndServersView(
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) } val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
LaunchedEffect(Unit) {
chatModel.userSMPServersUnsaved.value = null
}
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } } val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
NetworkAndServersLayout( NetworkAndServersLayout(
currentRemoteHost = currentRemoteHost, currentRemoteHost = currentRemoteHost,

View File

@@ -96,7 +96,7 @@ fun PrivacySettingsView(
val currentUser = chatModel.currentUser.value val currentUser = chatModel.currentUser.value
if (currentUser != null) { if (currentUser != null) {
fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) { fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) {
withLongRunningApi(slow = 60_000) { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
val mrs = UserMsgReceiptSettings(enable, clearOverrides) val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserContactReceipts(currentUser, mrs) chatModel.controller.apiSetUserContactReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
@@ -119,7 +119,7 @@ fun PrivacySettingsView(
} }
fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) { fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) {
withLongRunningApi(slow = 60_000) { withLongRunningApi(slow = 30_000, deadlock = 60_000) {
val mrs = UserMsgReceiptSettings(enable, clearOverrides) val mrs = UserMsgReceiptSettings(enable, clearOverrides)
chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs) chatModel.controller.apiSetUserGroupReceipts(currentUser, mrs)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)

View File

@@ -28,18 +28,19 @@ import chat.simplex.res.MR
@Composable @Composable
fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) {
var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) } var presetServers by remember(rhId) { mutableStateOf(emptyList<String>()) }
var servers by remember { stateGetOrPut("servers") { emptyList<ServerCfg>() } } var servers by remember(rhId) {
var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } } mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
}
val currServers = remember(rhId) { mutableStateOf(servers) } val currServers = remember(rhId) { mutableStateOf(servers) }
val testing = rememberSaveable(rhId) { mutableStateOf(false) } val testing = rememberSaveable(rhId) { mutableStateOf(false) }
val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } } val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } } val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
val saveDisabled = remember(servers) { val saveDisabled = remember {
derivedStateOf { derivedStateOf {
servers.isEmpty() || servers.isEmpty() ||
servers == currServers.value || servers == currServers.value ||
testing.value || testing.value ||
servers.none { srv -> !servers.all { srv ->
val address = parseServerAddress(srv.server) val address = parseServerAddress(srv.server)
address != null && uniqueAddress(srv, address, servers) address != null && uniqueAddress(srv, address, servers)
} || } ||
@@ -48,8 +49,8 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
} }
KeyChangeEffect(rhId) { KeyChangeEffect(rhId) {
m.userSMPServersUnsaved.value = null
servers = emptyList() servers = emptyList()
serversAlreadyLoaded = false
} }
LaunchedEffect(rhId) { LaunchedEffect(rhId) {
@@ -58,9 +59,8 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
if (res != null) { if (res != null) {
currServers.value = res.protoServers currServers.value = res.protoServers
presetServers = res.presetServers presetServers = res.presetServers
if (servers.isEmpty() && !serversAlreadyLoaded) { if (servers.isEmpty()) {
servers = currServers.value servers = currServers.value
serversAlreadyLoaded = true
} }
} }
} }
@@ -80,11 +80,13 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
newServers.add(index, updated) newServers.add(index, updated)
old = updated old = updated
servers = newServers servers = newServers
m.userSMPServersUnsaved.value = servers
}, },
onDelete = { onDelete = {
val newServers = ArrayList(servers) val newServers = ArrayList(servers)
newServers.removeAt(index) newServers.removeAt(index)
servers = newServers servers = newServers
m.userSMPServersUnsaved.value = servers
close() close()
}) })
} }
@@ -123,6 +125,7 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
ScanProtocolServer(rhId) { ScanProtocolServer(rhId) {
close() close()
servers = servers + it servers = servers + it
m.userSMPServersUnsaved.value = servers
} }
} }
} }
@@ -147,11 +150,13 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser
testServersJob.value = withLongRunningApi { testServersJob.value = withLongRunningApi {
testServers(testing, servers, m) { testServers(testing, servers, m) {
servers = it servers = it
m.userSMPServersUnsaved.value = servers
} }
} }
}, },
resetServers = { resetServers = {
servers = currServers.value servers = currServers.value ?: emptyList()
m.userSMPServersUnsaved.value = null
}, },
saveSMPServers = { saveSMPServers = {
saveServers(rhId, serverProtocol, currServers, servers, m) saveServers(rhId, serverProtocol, currServers, servers, m)
@@ -350,6 +355,7 @@ private fun saveServers(rhId: Long?, protocol: ServerProtocol, currServers: Muta
withBGApi { withBGApi {
if (m.controller.setUserProtoServers(rhId, protocol, servers)) { if (m.controller.setUserProtoServers(rhId, protocol, servers)) {
currServers.value = servers currServers.value = servers
m.userSMPServersUnsaved.value = null
} }
afterSave() afterSave()
} }

View File

@@ -22,10 +22,10 @@
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">منصة الرسائل والتطبيقات تحمي خصوصيتك وأمنك.</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">منصة الرسائل والتطبيقات تحمي خصوصيتك وأمنك.</string>
<string name="profile_is_only_shared_with_your_contacts">يتم مشاركة ملف التعريف مع جهات اتصالك فقط.</string> <string name="profile_is_only_shared_with_your_contacts">يتم مشاركة ملف التعريف مع جهات اتصالك فقط.</string>
<string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى \"%s\". سيتم إبلاغ كل فرد في المجموعة.</string> <string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى \"%s\". سيتم إبلاغ كل فرد في المجموعة.</string>
<string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى \"%s\". سيستلم العضو دعوة جديدة.</string> <string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى \"%s\". سيتلقى العضو دعوة جديدة.</string>
<string name="smp_servers_per_user">خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك</string> <string name="smp_servers_per_user">خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك</string>
<string name="switch_receiving_address_desc">سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت.</string> <string name="switch_receiving_address_desc">سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت.</string>
<string name="this_link_is_not_a_valid_connection_link">هذا الرابط ليس رابط اتصال صالح!</string> <string name="this_link_is_not_a_valid_connection_link">هذا الارتباط ليس ارتباط اتصال صالح!</string>
<string name="allow_verb">يسمح</string> <string name="allow_verb">يسمح</string>
<string name="smp_servers_preset_add">أضِف خوادم محدّدة مسبقًا</string> <string name="smp_servers_preset_add">أضِف خوادم محدّدة مسبقًا</string>
<string name="smp_servers_add_to_another_device">أضِف إلى جهاز آخر</string> <string name="smp_servers_add_to_another_device">أضِف إلى جهاز آخر</string>
@@ -36,13 +36,13 @@
<string name="all_group_members_will_remain_connected">سيبقى جميع أعضاء المجموعة على اتصال.</string> <string name="all_group_members_will_remain_connected">سيبقى جميع أعضاء المجموعة على اتصال.</string>
<string name="allow_disappearing_messages_only_if">السماح باختفاء الرسائل فقط إذا سمحت جهة اتصالك بذلك.</string> <string name="allow_disappearing_messages_only_if">السماح باختفاء الرسائل فقط إذا سمحت جهة اتصالك بذلك.</string>
<string name="allow_irreversible_message_deletion_only_if">السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك. (24 ساعة)</string> <string name="allow_irreversible_message_deletion_only_if">السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك. (24 ساعة)</string>
<string name="group_member_role_admin">المشرف</string> <string name="group_member_role_admin">مسؤل</string>
<string name="users_add">أضِف ملف التعريف</string> <string name="users_add">أضِف ملف التعريف</string>
<string name="allow_direct_messages">السماح بإرسال رسائل مباشرة إلى الأعضاء.</string> <string name="allow_direct_messages">السماح بإرسال رسائل مباشرة إلى الأعضاء.</string>
<string name="accept_contact_incognito_button">قبول التخفي</string> <string name="accept_contact_incognito_button">قبول التخفي</string>
<string name="button_add_welcome_message">أضِف رسالة ترحيب</string> <string name="button_add_welcome_message">أضِف رسالة ترحيب</string>
<string name="v4_3_improved_server_configuration_desc">أضف الخوادم عن طريق مسح رموز QR.</string> <string name="v4_3_improved_server_configuration_desc">أضف الخوادم عن طريق مسح رموز QR.</string>
<string name="v4_2_group_links_desc">يمكّن للمشرفين إنشاء روابط للانضمام إلى المجموعات.</string> <string name="v4_2_group_links_desc">يمكن للمسؤولين إنشاء روابط للانضمام إلى المجموعات.</string>
<string name="accept_connection_request__question">قبول طلب الاتصال؟</string> <string name="accept_connection_request__question">قبول طلب الاتصال؟</string>
<string name="clear_chat_warning">سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك.</string> <string name="clear_chat_warning">سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك.</string>
<string name="callstatus_accepted">مكالمة مقبولة</string> <string name="callstatus_accepted">مكالمة مقبولة</string>
@@ -65,7 +65,7 @@
<string name="allow_voice_messages_only_if">اسمح بالرسائل الصوتية فقط إذا سمحت جهة اتصالك بذلك.</string> <string name="allow_voice_messages_only_if">اسمح بالرسائل الصوتية فقط إذا سمحت جهة اتصالك بذلك.</string>
<string name="v5_0_app_passcode">رمز مرور التطبيق</string> <string name="v5_0_app_passcode">رمز مرور التطبيق</string>
<string name="notifications_mode_service">دائِماً مُتاح</string> <string name="notifications_mode_service">دائِماً مُتاح</string>
<string name="notifications_mode_off_desc">يمكن للتطبيق استلام الإشعارات فقط عند تشغيله، ولن يتم بدء تشغيل أي خدمة في الخلفية</string> <string name="notifications_mode_off_desc">يمكن للتطبيق تلقي الإشعارات فقط عند تشغيله ، ولن يتم بدء تشغيل أي خدمة في الخلفية</string>
<string name="allow_voice_messages_question">السماح بالرسائل الصوتية؟</string> <string name="allow_voice_messages_question">السماح بالرسائل الصوتية؟</string>
<string name="all_your_contacts_will_remain_connected">ستبقى جميع جهات الاتصال الخاصة بك متصلة.</string> <string name="all_your_contacts_will_remain_connected">ستبقى جميع جهات الاتصال الخاصة بك متصلة.</string>
<string name="always_use_relay">استخدم التتابع دائمًا</string> <string name="always_use_relay">استخدم التتابع دائمًا</string>
@@ -86,7 +86,7 @@
<string name="invite_prohibited">لا يمكن دعوة جهة اتصال!</string> <string name="invite_prohibited">لا يمكن دعوة جهة اتصال!</string>
<string name="icon_descr_cancel_image_preview">إلغاء معاينة الصورة</string> <string name="icon_descr_cancel_image_preview">إلغاء معاينة الصورة</string>
<string name="use_camera_button">الكاميرا</string> <string name="use_camera_button">الكاميرا</string>
<string name="icon_descr_cancel_link_preview">إلغاء معاينة الروابط</string> <string name="icon_descr_cancel_link_preview">إلغاء معاينة الارتباط</string>
<string name="network_session_mode_user_description"><![CDATA[سيتم استخدام اتصال TCP منفصل (وبيانات اعتماد SOCKS) <b> لكل ملف تعريف دردشة لديك في التطبيق </b>.]]></string> <string name="network_session_mode_user_description"><![CDATA[سيتم استخدام اتصال TCP منفصل (وبيانات اعتماد SOCKS) <b> لكل ملف تعريف دردشة لديك في التطبيق </b>.]]></string>
<string name="feature_cancelled_item">ألغيت %s</string> <string name="feature_cancelled_item">ألغيت %s</string>
<string name="one_time_link_short">رابط لمرة واحدة</string> <string name="one_time_link_short">رابط لمرة واحدة</string>
@@ -96,11 +96,11 @@
<string name="both_you_and_your_contact_can_add_message_reactions">يمكنك أنت وجهة اتصالك إضافة ردود فعل الرسائل.</string> <string name="both_you_and_your_contact_can_add_message_reactions">يمكنك أنت وجهة اتصالك إضافة ردود فعل الرسائل.</string>
<string name="both_you_and_your_contact_can_send_disappearing">يمكنك أنت وجهة اتصالك إرسال رسائل تختفي.</string> <string name="both_you_and_your_contact_can_send_disappearing">يمكنك أنت وجهة اتصالك إرسال رسائل تختفي.</string>
<string name="icon_descr_call_progress">مكالمتك تحت الإجراء</string> <string name="icon_descr_call_progress">مكالمتك تحت الإجراء</string>
<string name="cannot_receive_file">لا يمكّن استلام الملف</string> <string name="cannot_receive_file">لا يمكن استقبال الملف</string>
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> جيد للبطارية </b>. خدمة الخلفية تتحقق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]></string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> جيد للبطارية </b>. خدمة الخلفية تتحقق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]></string>
<string name="bold_text">عريض</string> <string name="bold_text">عريض</string>
<string name="audio_call_no_encryption">مكالمات الصوت (ليست مُعمّاة بين الطرفين)</string> <string name="audio_call_no_encryption">مكالمات الصوت (ليست مُعمّاة بين الطرفين)</string>
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b> الأفضل للبطارية </b>. ستستلم إشعارات فقط عندما يكون التطبيق قيد التشغيل (لا توجد خدمة في الخلفية).]]></string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b> الأفضل للبطارية </b>. ستتلقى إشعارات فقط عندما يكون التطبيق قيد التشغيل (لا توجد خدمة في الخلفية).]]></string>
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b> تستهلك المزيد من البطارية </b>! تعمل خدمة الخلفية دائمًا - تظهر الإشعارات بمجرد توفر الرسائل.]]></string> <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b> تستهلك المزيد من البطارية </b>! تعمل خدمة الخلفية دائمًا - تظهر الإشعارات بمجرد توفر الرسائل.]]></string>
<string name="call_already_ended">انتهت المكالمة بالفعل!</string> <string name="call_already_ended">انتهت المكالمة بالفعل!</string>
<string name="alert_title_msg_bad_hash">تجزئة رسالة سيئة</string> <string name="alert_title_msg_bad_hash">تجزئة رسالة سيئة</string>
@@ -244,7 +244,7 @@
<string name="chat_archive_header">أرشيف الدردشة</string> <string name="chat_archive_header">أرشيف الدردشة</string>
<string name="group_member_status_intro_invitation">الاتصال (دعوة مقدمة)</string> <string name="group_member_status_intro_invitation">الاتصال (دعوة مقدمة)</string>
<string name="clear_contacts_selection_button">مسح</string> <string name="clear_contacts_selection_button">مسح</string>
<string name="error_creating_link_for_group">خطأ في إنشاء رابط المجموعة</string> <string name="error_creating_link_for_group">خطأ في إنشاء ارتباط المجموعة</string>
<string name="item_info_current">(حاضِر)</string> <string name="item_info_current">(حاضِر)</string>
<string name="network_option_enable_tcp_keep_alive">تفعيل أبقِ TCP على قيد الحياة</string> <string name="network_option_enable_tcp_keep_alive">تفعيل أبقِ TCP على قيد الحياة</string>
<string name="contact_connection_pending">جار الاتصال…</string> <string name="contact_connection_pending">جار الاتصال…</string>
@@ -332,7 +332,7 @@
<string name="snd_conn_event_ratchet_sync_required">مطلوب إعادة التفاوض على التعمية ل%s</string> <string name="snd_conn_event_ratchet_sync_required">مطلوب إعادة التفاوض على التعمية ل%s</string>
<string name="error_changing_message_deletion">خطأ في تغيير الإعداد</string> <string name="error_changing_message_deletion">خطأ في تغيير الإعداد</string>
<string name="error_changing_role">خطأ في تغيير الدور</string> <string name="error_changing_role">خطأ في تغيير الدور</string>
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d فشل فك تعمية الرسائل.</string> <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d فشل فك تشفير الرسائل.</string>
<string name="dark_theme">سمة داكنة</string> <string name="dark_theme">سمة داكنة</string>
<string name="deleted_description">حُذِفت</string> <string name="deleted_description">حُذِفت</string>
<string name="database_passphrase_and_export">عبارة مرور قاعدة البيانات وتصديرها</string> <string name="database_passphrase_and_export">عبارة مرور قاعدة البيانات وتصديرها</string>
@@ -385,7 +385,7 @@
<string name="chat_preferences_default">الافتراضي %s</string> <string name="chat_preferences_default">الافتراضي %s</string>
<string name="delete_pending_connection__question">حذف الاتصال قيد الانتظار؟</string> <string name="delete_pending_connection__question">حذف الاتصال قيد الانتظار؟</string>
<string name="delete_chat_profile">حذف ملف تعريف الدردشة</string> <string name="delete_chat_profile">حذف ملف تعريف الدردشة</string>
<string name="decryption_error">خطأ في فك التعمية</string> <string name="decryption_error">خطأ في فك التشفير</string>
<string name="delete_message__question">حذف الرسالة؟</string> <string name="delete_message__question">حذف الرسالة؟</string>
<string name="developer_options">معرفات قاعدة البيانات وخيار عزل النقل.</string> <string name="developer_options">معرفات قاعدة البيانات وخيار عزل النقل.</string>
<string name="delete_address__question">حذف العنوان؟</string> <string name="delete_address__question">حذف العنوان؟</string>
@@ -410,7 +410,7 @@
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 سطح المكتب: امسح رمز الاستجابة السريعة (QR) المعروض من التطبيق، عبر <b>مسح رمز QR</b>.]]></string> <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 سطح المكتب: امسح رمز الاستجابة السريعة (QR) المعروض من التطبيق، عبر <b>مسح رمز QR</b>.]]></string>
<string name="delete_profile">حذف ملف التعريف</string> <string name="delete_profile">حذف ملف التعريف</string>
<string name="smp_servers_delete_server">حذف الخادم</string> <string name="smp_servers_delete_server">حذف الخادم</string>
<string name="error_updating_link_for_group">خطأ في تحديث رابط المجموعة</string> <string name="error_updating_link_for_group">خطأ في تحديث ارتباط المجموعة</string>
<string name="simplex_link_mode_description">الوصف</string> <string name="simplex_link_mode_description">الوصف</string>
<string name="icon_descr_expand_role">توسيع اختيار الدور</string> <string name="icon_descr_expand_role">توسيع اختيار الدور</string>
<string name="group_invitation_expired">انتهت صلاحية دعوة المجموعة</string> <string name="group_invitation_expired">انتهت صلاحية دعوة المجموعة</string>
@@ -507,7 +507,7 @@
<string name="conn_level_desc_indirect">غير مباشر (%1$s)</string> <string name="conn_level_desc_indirect">غير مباشر (%1$s)</string>
<string name="how_it_works">آلية العمل</string> <string name="how_it_works">آلية العمل</string>
<string name="incoming_video_call">مكالمة فيديو واردة</string> <string name="incoming_video_call">مكالمة فيديو واردة</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">إذا استلمت رابط دعوة SimpleX Chat، فيمكنك فتحه في متصفحك:</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">إذا تلقيت رابط دعوة SimpleX Chat، فيمكنك فتحه في متصفحك:</string>
<string name="incognito_info_protects">يحمي وضع التخفي خصوصيتك بأستخدام ملف تعريف عشوائي جديد لكل جهة اتصال جديدة.</string> <string name="incognito_info_protects">يحمي وضع التخفي خصوصيتك بأستخدام ملف تعريف عشوائي جديد لكل جهة اتصال جديدة.</string>
<string name="info_menu">معلومات</string> <string name="info_menu">معلومات</string>
<string name="v4_3_improved_privacy_and_security_desc">إخفاء شاشة التطبيق في التطبيقات الحديثة.</string> <string name="v4_3_improved_privacy_and_security_desc">إخفاء شاشة التطبيق في التطبيقات الحديثة.</string>
@@ -549,7 +549,7 @@
<string name="icon_descr_add_members">دعوة الأعضاء</string> <string name="icon_descr_add_members">دعوة الأعضاء</string>
<string name="alert_text_skipped_messages_it_can_happen_when">يمكن أن يحدث عندما: <string name="alert_text_skipped_messages_it_can_happen_when">يمكن أن يحدث عندما:
\n1. انتهت صلاحية الرسائل في العميل المرسل بعد يومين أو على الخادم بعد 30 يومًا. \n1. انتهت صلاحية الرسائل في العميل المرسل بعد يومين أو على الخادم بعد 30 يومًا.
\n2. فشل فك تعمية الرسالة، لأنك أو جهة اتصالك استخدمت نسخة احتياطية قديمة من قاعدة البيانات. \n2. فشل فك تشفير الرسالة، لأنك أو جهة اتصالك استخدمت نسخة احتياطية قديمة من قاعدة البيانات.
\n3. اُخترق الاتصال.</string> \n3. اُخترق الاتصال.</string>
<string name="v5_1_japanese_portuguese_interface">واجهة أستخدام يابانية وبرتغالية</string> <string name="v5_1_japanese_portuguese_interface">واجهة أستخدام يابانية وبرتغالية</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">يمكن أن يحدث ذلك عندما تستخدم أنت أو اتصالك النُسخة الاحتياطية القديمة لقاعدة البيانات.</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">يمكن أن يحدث ذلك عندما تستخدم أنت أو اتصالك النُسخة الاحتياطية القديمة لقاعدة البيانات.</string>
@@ -756,7 +756,7 @@
<string name="feature_offered_item_with_param">متوفرة %s: %2s</string> <string name="feature_offered_item_with_param">متوفرة %s: %2s</string>
<string name="notifications_will_be_hidden">سيتم تسليم الإشعارات فقط حتى يتوقف التطبيق!</string> <string name="notifications_will_be_hidden">سيتم تسليم الإشعارات فقط حتى يتوقف التطبيق!</string>
<string name="no_filtered_chats">لا توجد محادثات مُصفاة</string> <string name="no_filtered_chats">لا توجد محادثات مُصفاة</string>
<string name="no_received_app_files">لا توجد ملفات مُستلمة أو مُرسلة</string> <string name="no_received_app_files">لا توجد ملفات مستلمة أو مرسلة</string>
<string name="shutdown_alert_desc">ستتوقف الإشعارات عن العمل حتى تعيد تشغيل التطبيق</string> <string name="shutdown_alert_desc">ستتوقف الإشعارات عن العمل حتى تعيد تشغيل التطبيق</string>
<string name="new_passcode">رمز مرور جديد</string> <string name="new_passcode">رمز مرور جديد</string>
<string name="new_database_archive">أرشيف قاعدة بيانات جديدة</string> <string name="new_database_archive">أرشيف قاعدة بيانات جديدة</string>
@@ -764,7 +764,7 @@
<string name="network_use_onion_hosts_prefer_desc">سيتم استخدام مضيفات البصل عند توفرها.</string> <string name="network_use_onion_hosts_prefer_desc">سيتم استخدام مضيفات البصل عند توفرها.</string>
<string name="network_use_onion_hosts_no_desc">لن يتم استخدام مضيفات البصل.</string> <string name="network_use_onion_hosts_no_desc">لن يتم استخدام مضيفات البصل.</string>
<string name="no_contacts_selected">لم تٌحدد جهات اتصال</string> <string name="no_contacts_selected">لم تٌحدد جهات اتصال</string>
<string name="v4_6_group_moderation_descr">يمكّن للمشرف الآن: <string name="v4_6_group_moderation_descr">يمكن للمسؤولين الآن:
\n- حذف رسائل الأعضاء. \n- حذف رسائل الأعضاء.
\n- تعطيل الأعضاء (دور \"المراقب\")</string> \n- تعطيل الأعضاء (دور \"المراقب\")</string>
<string name="settings_notifications_mode_title">خدمة الإشعار</string> <string name="settings_notifications_mode_title">خدمة الإشعار</string>
@@ -795,7 +795,7 @@
<string name="new_passphrase">عبارة مرور جديدة…</string> <string name="new_passphrase">عبارة مرور جديدة…</string>
<string name="icon_descr_server_status_pending">يرجى الانتظار</string> <string name="icon_descr_server_status_pending">يرجى الانتظار</string>
<string name="enter_passphrase_notification_title">كلمة المرور مطلوبة</string> <string name="enter_passphrase_notification_title">كلمة المرور مطلوبة</string>
<string name="paste_the_link_you_received">ألصِق الرابط الذي استلمته</string> <string name="paste_the_link_you_received">ألصِق الرابط الذي تلقيته</string>
<string name="only_owners_can_enable_files_and_media">فقط مالكي المجموعة يمكنهم تفعيل الملفات والوسائط.</string> <string name="only_owners_can_enable_files_and_media">فقط مالكي المجموعة يمكنهم تفعيل الملفات والوسائط.</string>
<string name="only_group_owners_can_enable_voice">فقط مالكي المجموعة يمكنهم تفعيل الرسائل الصوتية.</string> <string name="only_group_owners_can_enable_voice">فقط مالكي المجموعة يمكنهم تفعيل الرسائل الصوتية.</string>
<string name="only_stored_on_members_devices">(يخزن فقط بواسطة أعضاء المجموعة)</string> <string name="only_stored_on_members_devices">(يخزن فقط بواسطة أعضاء المجموعة)</string>
@@ -930,7 +930,7 @@
<string name="group_welcome_preview">معاينة</string> <string name="group_welcome_preview">معاينة</string>
<string name="error_smp_test_certificate">من المحتمل أن الملف المرجعي للشهادة في عنوان الخادم غير صحيح</string> <string name="error_smp_test_certificate">من المحتمل أن الملف المرجعي للشهادة في عنوان الخادم غير صحيح</string>
<string name="simplex_service_notification_text">يتم استلام الرسائل…</string> <string name="simplex_service_notification_text">يتم استلام الرسائل…</string>
<string name="observer_cant_send_message_desc">يُرجى الاتصال بمشرف المجموعة.</string> <string name="observer_cant_send_message_desc">يرجى الاتصال بمسؤول المجموعة.</string>
<string name="sync_connection_force_confirm">أعد التفاوض</string> <string name="sync_connection_force_confirm">أعد التفاوض</string>
<string name="sync_connection_force_question">إعادة تفاوض التعمية</string> <string name="sync_connection_force_question">إعادة تفاوض التعمية</string>
<string name="revoke_file__action">سحب وصول الملف</string> <string name="revoke_file__action">سحب وصول الملف</string>
@@ -1039,7 +1039,7 @@
<string name="show_QR_code">عرض رمز QR</string> <string name="show_QR_code">عرض رمز QR</string>
<string name="is_verified">تم التحقق %s</string> <string name="is_verified">تم التحقق %s</string>
<string name="smp_servers_test_some_failed">فشلت بعض الخوادم في الاختبار:</string> <string name="smp_servers_test_some_failed">فشلت بعض الخوادم في الاختبار:</string>
<string name="send_link_previews">إرسال معاينات الرابط</string> <string name="send_link_previews">إرسال معاينات الارتباط</string>
<string name="skip_inviting_button">تخطي دعوة الأعضاء</string> <string name="skip_inviting_button">تخطي دعوة الأعضاء</string>
<string name="stop_chat_question">إيقاف الدردشة؟</string> <string name="stop_chat_question">إيقاف الدردشة؟</string>
<string name="show_call_on_lock_screen">عرض</string> <string name="show_call_on_lock_screen">عرض</string>
@@ -1062,7 +1062,7 @@
<string name="star_on_github">اضع نجمة على GitHub</string> <string name="star_on_github">اضع نجمة على GitHub</string>
<string name="stop_sharing_address">إيقاف مشاركة العنوان؟</string> <string name="stop_sharing_address">إيقاف مشاركة العنوان؟</string>
<string name="stop_sharing">إيقاف المشاركة</string> <string name="stop_sharing">إيقاف المشاركة</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">أوقف الدردشة لتصدير أو استيراد أو حذف قاعدة بيانات الدردشة. لن تتمكّن من استلام الرسائل وإرسالها أثناء إيقاف الدردشة.</string> <string name="stop_chat_to_export_import_or_delete_chat_database">أوقف الدردشة لتصدير أو استيراد أو حذف قاعدة بيانات الدردشة. لن تتمكن من تلقي الرسائل وإرسالها أثناء إيقاف الدردشة.</string>
<string name="stop_chat_to_enable_database_actions">أوقف الدردشة لتمكين إجراءات قاعدة البيانات.</string> <string name="stop_chat_to_enable_database_actions">أوقف الدردشة لتمكين إجراءات قاعدة البيانات.</string>
<string name="chat_item_ttl_seconds">%s ثانية/ثواني</string> <string name="chat_item_ttl_seconds">%s ثانية/ثواني</string>
<string name="callstate_starting">يبدأ…</string> <string name="callstate_starting">يبدأ…</string>
@@ -1135,7 +1135,7 @@
<string name="v5_2_message_delivery_receipts_descr">فقدنا القراد الثاني! ✅</string> <string name="v5_2_message_delivery_receipts_descr">فقدنا القراد الثاني! ✅</string>
<string name="whats_new_thanks_to_users_contribute_weblate">بفضل المستخدمين - المساهمة عبر Weblate!</string> <string name="whats_new_thanks_to_users_contribute_weblate">بفضل المستخدمين - المساهمة عبر Weblate!</string>
<string name="database_backup_can_be_restored">لم تكتمل محاولة تغيير عبارة مرور قاعدة البيانات.</string> <string name="database_backup_can_be_restored">لم تكتمل محاولة تغيير عبارة مرور قاعدة البيانات.</string>
<string name="enter_passphrase_notification_desc">لاستلام الإشعارات، يُرجى إدخال عبارة مرور قاعدة البيانات</string> <string name="enter_passphrase_notification_desc">لتلقي الإشعارات، يرجى إدخال عبارة مرور قاعدة البيانات</string>
<string name="la_lock_mode_system">مصادقة النظام</string> <string name="la_lock_mode_system">مصادقة النظام</string>
<string name="sync_connection_force_desc">يعمل التعمية واتفاقية التعمية الجديدة غير مطلوبة. قد ينتج عن ذلك أخطاء في الاتصال!</string> <string name="sync_connection_force_desc">يعمل التعمية واتفاقية التعمية الجديدة غير مطلوبة. قد ينتج عن ذلك أخطاء في الاتصال!</string>
<string name="image_decoding_exception_desc">لا يمكن فك ترميز الصورة. من فضلك، جرب صورة مختلفة أو تواصل مع المطورين.</string> <string name="image_decoding_exception_desc">لا يمكن فك ترميز الصورة. من فضلك، جرب صورة مختلفة أو تواصل مع المطورين.</string>
@@ -1149,19 +1149,19 @@
<string name="alert_text_msg_bad_id">معرف الرسالة التالية غير صحيح (أقل أو يساوي السابق). <string name="alert_text_msg_bad_id">معرف الرسالة التالية غير صحيح (أقل أو يساوي السابق).
\nيمكن أن يحدث ذلك بسبب بعض العلل أو عندما يُخترق الاتصال.</string> \nيمكن أن يحدث ذلك بسبب بعض العلل أو عندما يُخترق الاتصال.</string>
<string name="unfavorite_chat">إزالة من المفضلة</string> <string name="unfavorite_chat">إزالة من المفضلة</string>
<string name="trying_to_connect_to_server_to_receive_messages">محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه.</string> <string name="trying_to_connect_to_server_to_receive_messages">محاولة الاتصال بالخادم المستخدم لتلقي الرسائل من جهة الاتصال هذه.</string>
<string name="choose_file_title">اختيار ملف</string> <string name="choose_file_title">اختيار ملف</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">إرسال غير مصرح به</string> <string name="icon_descr_sent_msg_status_unauthorized_send">إرسال غير مصرح به</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">محاولة الاتصال بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه (خطأ: %1$s).</string> <string name="trying_to_connect_to_server_to_receive_messages_with_error">محاولة الاتصال بالخادم المستخدم لتلقي الرسائل من جهة الاتصال هذه (خطأ: %1$s).</string>
<string name="la_notice_turn_on">تشغيل</string> <string name="la_notice_turn_on">تشغيل</string>
<string name="webrtc_ice_servers">خوادم WebRTC ICE</string> <string name="webrtc_ice_servers">خوادم WebRTC ICE</string>
<string name="alert_title_cant_invite_contacts_descr">أنت تستخدم ملفًا شخصيًا متخفيًا لهذه المجموعة - لمنع مشاركة ملفك الشخصي الرئيسي الذي يدعو جهات الاتصال غير مسموح به</string> <string name="alert_title_cant_invite_contacts_descr">أنت تستخدم ملفًا شخصيًا متخفيًا لهذه المجموعة - لمنع مشاركة ملفك الشخصي الرئيسي الذي يدعو جهات الاتصال غير مسموح به</string>
<string name="snd_group_event_changed_member_role">غيّرتَ دور %s إلى %s</string> <string name="snd_group_event_changed_member_role">غيّرتَ دور %s إلى %s</string>
<string name="chat_preferences_yes">نعم</string> <string name="chat_preferences_yes">نعم</string>
<string name="connected_to_server_to_receive_messages_from_contact">أنت متصل بالخادم المستخدم لاستلام الرسائل من جهة الاتصال هذه.</string> <string name="connected_to_server_to_receive_messages_from_contact">أنت متصل بالخادم المستخدم لتلقي الرسائل من جهة الاتصال هذه.</string>
<string name="sender_you_pronoun">أنت</string> <string name="sender_you_pronoun">أنت</string>
<string name="description_you_shared_one_time_link">لقد شاركت رابط لمرة واحدة</string> <string name="description_you_shared_one_time_link">لقد شاركت رابط لمرة واحدة</string>
<string name="profile_will_be_sent_to_contact_sending_link">سيتم إرسال ملف التعريفك إلى جهة الاتصال التي استلمت منها هذا الرابط.</string> <string name="profile_will_be_sent_to_contact_sending_link">سيتم إرسال ملف التعريفك إلى جهة الاتصال التي تلقيت منها هذا الارتباط.</string>
<string name="you_will_join_group">سوف تتصل بجميع أعضاء المجموعة.</string> <string name="you_will_join_group">سوف تتصل بجميع أعضاء المجموعة.</string>
<string name="your_chat_profiles">ملفات تعريف الدردشة الخاصة بك</string> <string name="your_chat_profiles">ملفات تعريف الدردشة الخاصة بك</string>
<string name="your_simplex_contact_address">عنوان SimpleX الخاص بك</string> <string name="your_simplex_contact_address">عنوان SimpleX الخاص بك</string>
@@ -1226,7 +1226,7 @@
<string name="snd_group_event_user_left">غادرت</string> <string name="snd_group_event_user_left">غادرت</string>
<string name="you_must_use_the_most_recent_version_of_database">يجب عليك استخدام أحدث إصدار من قاعدة بيانات الدردشة الخاصة بك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال.</string> <string name="you_must_use_the_most_recent_version_of_database">يجب عليك استخدام أحدث إصدار من قاعدة بيانات الدردشة الخاصة بك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال.</string>
<string name="video_will_be_received_when_contact_is_online">سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> <string name="video_will_be_received_when_contact_is_online">سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string>
<string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[يمكنك التحكم من خلال الخادم (الخوادم) <b>لاستلام</b> الرسائل وجهات اتصالك - الخوادم التي تستخدمها لمراسلتهم.]]></string> <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[يمكنك التحكم من خلال الخادم (الخوادم) <b>لتلقي</b> الرسائل وجهات اتصالك - الخوادم التي تستخدمها لمراسلتهم.]]></string>
<string name="you_can_share_this_address_with_your_contacts">يمكنك مشاركة هذا العنوان مع جهات اتصالك للسماح لهم بالاتصال بـ%s.</string> <string name="you_can_share_this_address_with_your_contacts">يمكنك مشاركة هذا العنوان مع جهات اتصالك للسماح لهم بالاتصال بـ%s.</string>
<string name="snd_group_event_member_deleted">أُزيلت %1$s</string> <string name="snd_group_event_member_deleted">أُزيلت %1$s</string>
<string name="update_database">تحديث</string> <string name="update_database">تحديث</string>
@@ -1299,13 +1299,13 @@
<string name="upgrade_and_open_chat">قم بالترقية وفتح الدردشة</string> <string name="upgrade_and_open_chat">قم بالترقية وفتح الدردشة</string>
<string name="button_welcome_message">رسالة الترحيب</string> <string name="button_welcome_message">رسالة الترحيب</string>
<string name="description_via_contact_address_link">عبر رابط عنوان الاتصال</string> <string name="description_via_contact_address_link">عبر رابط عنوان الاتصال</string>
<string name="connection_error_auth_desc">ما لم يحذف جهة الاتصال الاتصال أو استُخدم هذا الرابط بالفعل، فقد يكون خطأ - الرجاء الإبلاغ عنه. <string name="connection_error_auth_desc">ما لم يحذف جهة الاتصال الاتصال أو تم استخدام هذا الرابط بالفعل، فقد يكون خطأ - الرجاء الإبلاغ عنه.
\nللاتصال، يُرجى مطالبة جهة اتصالك بإنشاء رابط اتصال آخر والتحقق من أن لديك اتصال شبكة ثابت.</string> \nللاتصال، يُرجى مطالبة جهة اتصالك بإنشاء ارتباط اتصال آخر والتحقق من أن لديك اتصال شبكة ثابت.</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">سيتم إرسال ملف تعريف الدردشة الخاص بك <string name="your_chat_profile_will_be_sent_to_your_contact">سيتم إرسال ملف تعريف الدردشة الخاص بك
\nإلى جهة اتصالك</string> \nإلى جهة اتصالك</string>
<string name="user_unhide">إلغاء الإخفاء</string> <string name="user_unhide">إلغاء الإخفاء</string>
<string name="incognito_random_profile">ملفك الشخصي العشوائي</string> <string name="incognito_random_profile">ملفك الشخصي العشوائي</string>
<string name="you_will_still_receive_calls_and_ntfs">ستستمر في استلام المكالمات والإشعارات من الملفات الشخصية المكتومة عندما تكون نشطة.</string> <string name="you_will_still_receive_calls_and_ntfs">ستستمر في تلقي المكالمات والإشعارات من الملفات الشخصية المكتومة عندما تكون نشطة.</string>
<string name="chat_preferences_you_allow">انت تسمح بها</string> <string name="chat_preferences_you_allow">انت تسمح بها</string>
<string name="icon_descr_video_call">مكالمة فيديو</string> <string name="icon_descr_video_call">مكالمة فيديو</string>
<string name="voice_messages_are_prohibited">الرسائل الصوتية ممنوعة في هذه الدردشة.</string> <string name="voice_messages_are_prohibited">الرسائل الصوتية ممنوعة في هذه الدردشة.</string>
@@ -1348,7 +1348,7 @@
<string name="connect_use_current_profile">استخدم ملف التعريف الحالي</string> <string name="connect_use_current_profile">استخدم ملف التعريف الحالي</string>
<string name="disable_notifications_button">تعطيل الإشعارات</string> <string name="disable_notifications_button">تعطيل الإشعارات</string>
<string name="turn_off_system_restriction_button">افتح إعدادات التطبيق</string> <string name="turn_off_system_restriction_button">افتح إعدادات التطبيق</string>
<string name="system_restricted_background_desc">لا يمكن تشغيل SimpleX في الخلفية. ستستلم الإشعارات فقط عندما يكون التطبيق قيد التشغيل.</string> <string name="system_restricted_background_desc">لا يمكن تشغيل SimpleX في الخلفية. ستتلقى الإشعارات فقط عندما يكون التطبيق قيد التشغيل.</string>
<string name="connect__a_new_random_profile_will_be_shared">سيتم مشاركة ملف تعريف عشوائي جديد.</string> <string name="connect__a_new_random_profile_will_be_shared">سيتم مشاركة ملف تعريف عشوائي جديد.</string>
<string name="paste_the_link_you_received_to_connect_with_your_contact">ألصق الرابط المُستلَم للتواصل مع جهة اتصالك…</string> <string name="paste_the_link_you_received_to_connect_with_your_contact">ألصق الرابط المُستلَم للتواصل مع جهة اتصالك…</string>
<string name="connect__your_profile_will_be_shared">ستتم مشاركة ملفك الشخصي %1$s.</string> <string name="connect__your_profile_will_be_shared">ستتم مشاركة ملفك الشخصي %1$s.</string>
@@ -1376,7 +1376,7 @@
<string name="open_database_folder">افتح مجلد قاعدة البيانات</string> <string name="open_database_folder">افتح مجلد قاعدة البيانات</string>
<string name="passphrase_will_be_saved_in_settings">سيتم تخزين عبارة المرور في الإعدادات كنص عادي بعد تغييرها أو إعادة تشغيل التطبيق.</string> <string name="passphrase_will_be_saved_in_settings">سيتم تخزين عبارة المرور في الإعدادات كنص عادي بعد تغييرها أو إعادة تشغيل التطبيق.</string>
<string name="settings_is_storing_in_clear_text">يُخزين عبارة المرور في الإعدادات كنص عادي.</string> <string name="settings_is_storing_in_clear_text">يُخزين عبارة المرور في الإعدادات كنص عادي.</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>يُرجى الملاحظة</b>: يتم توصيل مرحلات الرسائل والملفات عبر وكيل SOCKS. تستخدم المكالمات وإرسال معاينات الروابط الاتصال المباشر.]]></string> <string name="socks_proxy_setting_limitations"><![CDATA[<b>يُرجى الملاحظة</b>: يتم توصيل مرحلات الرسائل والملفات عبر وكيل SOCKS. تستخدم المكالمات وإرسال معاينات الارتباط الاتصال المباشر.]]></string>
<string name="encrypt_local_files">عَمِّ الملفات المحلية</string> <string name="encrypt_local_files">عَمِّ الملفات المحلية</string>
<string name="v5_3_encrypt_local_files">عَمِّ الملفات والوسائط المخزنة</string> <string name="v5_3_encrypt_local_files">عَمِّ الملفات والوسائط المخزنة</string>
<string name="v5_3_new_desktop_app">تطبيق سطح المكتب الجديد!</string> <string name="v5_3_new_desktop_app">تطبيق سطح المكتب الجديد!</string>
@@ -1529,7 +1529,7 @@
<string name="retry_verb">حاول مجددًا</string> <string name="retry_verb">حاول مجددًا</string>
<string name="camera_not_available">الكاميرا غير متوفرة</string> <string name="camera_not_available">الكاميرا غير متوفرة</string>
<string name="enable_sending_recent_history">أرسل ما يصل إلى 100 رسالة أخيرة للأعضاء الجدد.</string> <string name="enable_sending_recent_history">أرسل ما يصل إلى 100 رسالة أخيرة للأعضاء الجدد.</string>
<string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>إضافة جهة اتصال</b>: لإنشاء رابط دعوة جديد، أو الاتصال عبر رابط استلمته.]]></string> <string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>إضافة جهة اتصال</b>: لإنشاء رابط دعوة جديد، أو الاتصال عبر رابط تلقيته.]]></string>
<string name="disable_sending_recent_history">لا ترسل التاريخ للأعضاء الجدد.</string> <string name="disable_sending_recent_history">لا ترسل التاريخ للأعضاء الجدد.</string>
<string name="or_show_this_qr_code">أو أظهر هذا الرمز</string> <string name="or_show_this_qr_code">أو أظهر هذا الرمز</string>
<string name="recent_history_is_sent_to_new_members">يتم إرسال ما يصل إلى 100 رسالة أخيرة إلى الأعضاء الجدد.</string> <string name="recent_history_is_sent_to_new_members">يتم إرسال ما يصل إلى 100 رسالة أخيرة إلى الأعضاء الجدد.</string>
@@ -1588,6 +1588,8 @@
<string name="remote_ctrl_error_busy">سطح المكتب مشغول</string> <string name="remote_ctrl_error_busy">سطح المكتب مشغول</string>
<string name="remote_ctrl_error_bad_version">يحتوي سطح المكتب على إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين</string> <string name="remote_ctrl_error_bad_version">يحتوي سطح المكتب على إصدار غير مدعوم. يُرجى التأكد من استخدام نفس الإصدار على كلا الجهازين</string>
<string name="past_member_vName">العضو السابق %1$s</string> <string name="past_member_vName">العضو السابق %1$s</string>
<string name="possible_deadlock_title">مأزق</string>
<string name="possible_deadlock_desc">يستغرق تنفيذ التعليمات البرمجية وقتًا طويلاً جدًا: %1$d ثانية. من المحتمل أن التطبيق مجمّد: %2$s</string>
<string name="possible_slow_function_title">وظيفة بطيئة</string> <string name="possible_slow_function_title">وظيفة بطيئة</string>
<string name="developer_options_section">خيارات المطور</string> <string name="developer_options_section">خيارات المطور</string>
<string name="profile_update_event_member_name_changed">تغيّر العضو %1$s إلى %2$s</string> <string name="profile_update_event_member_name_changed">تغيّر العضو %1$s إلى %2$s</string>
@@ -1615,17 +1617,4 @@
<string name="clear_note_folder_question">مسح الملاحظات الخاصة؟</string> <string name="clear_note_folder_question">مسح الملاحظات الخاصة؟</string>
<string name="share_text_created_at">أُنشئ في: %s</string> <string name="share_text_created_at">أُنشئ في: %s</string>
<string name="saved_message_title">رسالة محفوظة</string> <string name="saved_message_title">رسالة محفوظة</string>
<string name="unblock_for_all_question">إلغاء حظر العضو للجميع؟</string>
<string name="unblock_for_all">إلغاء الحظر للجميع</string>
<string name="error_blocking_member_for_all">حدث خطأ أثناء حظر العضو للجميع</string>
<string name="blocked_by_admin_items_description">حُظر %d رسالة من قبل المشرف</string>
<string name="rcv_group_event_member_blocked">محظور %s</string>
<string name="rcv_group_event_member_unblocked">أُلغيت حظر %s</string>
<string name="snd_group_event_member_blocked">حظرت %s</string>
<string name="snd_group_event_member_unblocked">أُلغيت حظر %s</string>
<string name="member_info_member_blocked">محظور</string>
<string name="block_for_all">حظر للجميع</string>
<string name="block_for_all_question">حظر العضو للجميع؟</string>
<string name="blocked_by_admin_item_description">محظور من قبل المشرف</string>
<string name="member_blocked_by_admin">محظور من قبل المشرف</string>
</resources> </resources>

View File

@@ -16,7 +16,6 @@
<!-- MainActivity.kt --> <!-- MainActivity.kt -->
<string name="opening_database">Opening database…</string> <string name="opening_database">Opening database…</string>
<string name="database_migration_in_progress">Database migration is in progress.\nIt may take a few minutes.</string>
<string name="non_content_uri_alert_title">Invalid file path</string> <string name="non_content_uri_alert_title">Invalid file path</string>
<string name="non_content_uri_alert_text">You shared an invalid file path. Report the issue to the app developers.</string> <string name="non_content_uri_alert_text">You shared an invalid file path. Report the issue to the app developers.</string>
<string name="app_was_crashed">View crashed</string> <string name="app_was_crashed">View crashed</string>
@@ -147,6 +146,8 @@
<string name="smp_server_test_delete_file">Delete file</string> <string name="smp_server_test_delete_file">Delete file</string>
<string name="error_deleting_user">Error deleting user profile</string> <string name="error_deleting_user">Error deleting user profile</string>
<string name="error_updating_user_privacy">Error updating user privacy</string> <string name="error_updating_user_privacy">Error updating user privacy</string>
<string name="possible_deadlock_title">Deadlock</string>
<string name="possible_deadlock_desc">Execution of code takes too long time: %1$d seconds. Probably, the app is frozen: %2$s</string>
<string name="possible_slow_function_title">Slow function</string> <string name="possible_slow_function_title">Slow function</string>
<string name="possible_slow_function_desc">Execution of function takes too long time: %1$d seconds: %2$s</string> <string name="possible_slow_function_desc">Execution of function takes too long time: %1$d seconds: %2$s</string>
@@ -177,9 +178,6 @@
<!-- SimpleX Chat foreground Service --> <!-- SimpleX Chat foreground Service -->
<string name="simplex_service_notification_title">SimpleX Chat service</string> <string name="simplex_service_notification_title">SimpleX Chat service</string>
<string name="simplex_service_notification_text">Receiving messages…</string> <string name="simplex_service_notification_text">Receiving messages…</string>
<string name="call_service_notification_audio_call">Audio call</string>
<string name="call_service_notification_video_call">Video call</string>
<string name="call_service_notification_end_call">End call</string>
<string name="hide_notification">Hide</string> <string name="hide_notification">Hide</string>
<!-- Notification channels --> <!-- Notification channels -->
@@ -804,10 +802,6 @@
<string name="callstate_connected">connected</string> <string name="callstate_connected">connected</string>
<string name="callstate_ended">ended</string> <string name="callstate_ended">ended</string>
<!-- 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>
<!-- SimpleXInfo --> <!-- SimpleXInfo -->
<string name="next_generation_of_private_messaging">The next generation of private messaging</string> <string name="next_generation_of_private_messaging">The next generation of private messaging</string>
<string name="privacy_redefined">Privacy redefined</string> <string name="privacy_redefined">Privacy redefined</string>
@@ -1383,11 +1377,9 @@
<!-- GroupWelcomeView.kt --> <!-- GroupWelcomeView.kt -->
<string name="group_welcome_title">Welcome message</string> <string name="group_welcome_title">Welcome message</string>
<string name="save_welcome_message_question">Save welcome message?</string> <string name="save_welcome_message_question">Save welcome message?</string>
<string name="welcome_message_is_too_long">Welcome message is too long</string>
<string name="save_and_update_group_profile">Save and update group profile</string> <string name="save_and_update_group_profile">Save and update group profile</string>
<string name="group_welcome_preview">Preview</string> <string name="group_welcome_preview">Preview</string>
<string name="enter_welcome_message">Enter welcome message…</string> <string name="enter_welcome_message">Enter welcome message…</string>
<string name="message_too_large">Message too large</string>
<!-- ConnectionStats --> <!-- ConnectionStats -->
<string name="conn_stats_section_title_servers">SERVERS</string> <string name="conn_stats_section_title_servers">SERVERS</string>

View File

@@ -1555,6 +1555,7 @@
<string name="chat_is_stopped_you_should_transfer_database">Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново.</string> <string name="chat_is_stopped_you_should_transfer_database">Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново.</string>
<string name="remote_ctrl_error_bad_invitation">Настолното устройство има грешен код за връзка</string> <string name="remote_ctrl_error_bad_invitation">Настолното устройство има грешен код за връзка</string>
<string name="remote_ctrl_error_bad_version">Настолното устройство е с неподдържана версия. Моля, уверете се, че използвате една и съща версия и на двете устройства</string> <string name="remote_ctrl_error_bad_version">Настолното устройство е с неподдържана версия. Моля, уверете се, че използвате една и съща версия и на двете устройства</string>
<string name="possible_deadlock_desc">Изпълнението на кода отнема твърде много време: %1$d секунди. Вероятно приложението е замразено: %2$s</string>
<string name="possible_slow_function_title">Бавна функция</string> <string name="possible_slow_function_title">Бавна функция</string>
<string name="possible_slow_function_desc">Изпълнението на функцията отнема твърде много време: %1$d секунди: %2$s</string> <string name="possible_slow_function_desc">Изпълнението на функцията отнема твърде много време: %1$d секунди: %2$s</string>
<string name="show_internal_errors">Покажи вътрешните грешки</string> <string name="show_internal_errors">Покажи вътрешните грешки</string>
@@ -1590,4 +1591,5 @@
\nПрепоръчително е да рестартирате приложението.</string> \nПрепоръчително е да рестартирате приложението.</string>
<string name="developer_options_section">Опции за разработчици</string> <string name="developer_options_section">Опции за разработчици</string>
<string name="show_slow_api_calls">Показване на бавни API заявки</string> <string name="show_slow_api_calls">Показване на бавни API заявки</string>
<string name="possible_deadlock_title">Грешка в заключено положение</string>
</resources> </resources>

View File

@@ -3,8 +3,8 @@
<string name="app_name">SimpleX</string> <string name="app_name">SimpleX</string>
<string name="thousand_abbreviation">k</string> <string name="thousand_abbreviation">k</string>
<!-- Connect via Link - MainActivity.kt --> <!-- Connect via Link - MainActivity.kt -->
<string name="connect_via_contact_link">Über den Kontaktadressen-Link verbinden?</string> <string name="connect_via_contact_link">Über die Kontakt-Adresse verbinden?</string>
<string name="connect_via_invitation_link">Über den Einmal-Einladungslink verbinden?</string> <string name="connect_via_invitation_link">Über den Einmal-Link verbinden?</string>
<string name="connect_via_group_link">Der Gruppe beitreten?</string> <string name="connect_via_group_link">Der Gruppe beitreten?</string>
<string name="profile_will_be_sent_to_contact_sending_link">Ihr Profil wird an den Kontakt gesendet, von dem Sie diesen Link erhalten haben.</string> <string name="profile_will_be_sent_to_contact_sending_link">Ihr Profil wird an den Kontakt gesendet, von dem Sie diesen Link erhalten haben.</string>
<string name="you_will_join_group">Sie werden mit allen Gruppenmitgliedern verbunden.</string> <string name="you_will_join_group">Sie werden mit allen Gruppenmitgliedern verbunden.</string>
@@ -39,7 +39,7 @@
<string name="description_via_one_time_link_incognito">Inkognito über einen Einmal-Link</string> <string name="description_via_one_time_link_incognito">Inkognito über einen Einmal-Link</string>
<!-- FormattedText, SimpleX links - ChatModel.kt --> <!-- FormattedText, SimpleX links - ChatModel.kt -->
<string name="simplex_link_contact">SimpleX-Kontaktadressen-Link</string> <string name="simplex_link_contact">SimpleX-Kontaktadressen-Link</string>
<string name="simplex_link_invitation">SimpleX-Einmal-Einladungslink</string> <string name="simplex_link_invitation">SimpleX-Einmal-Einladung</string>
<string name="simplex_link_group">SimpleX-Gruppen-Link</string> <string name="simplex_link_group">SimpleX-Gruppen-Link</string>
<string name="simplex_link_connection">über %1$s</string> <string name="simplex_link_connection">über %1$s</string>
<string name="simplex_link_mode">SimpleX-Links</string> <string name="simplex_link_mode">SimpleX-Links</string>
@@ -331,7 +331,7 @@
<string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird <string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird
\nan Ihren Kontakt gesendet</string> \nan Ihren Kontakt gesendet</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs scannen</b> oder Ihr Kontakt kann einen Einladungslink über einen anderen Kanal mit Ihnen teilen.]]></string> <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs scannen</b> oder Ihr Kontakt kann einen Einladungslink über einen anderen Kanal mit Ihnen teilen.]]></string>
<string name="share_invitation_link">Einmal-Einladungslink teilen</string> <string name="share_invitation_link">Einmal-Link teilen</string>
<!-- PasteToConnect.kt --> <!-- PasteToConnect.kt -->
<string name="connect_via_link">Über einen Link verbinden</string> <string name="connect_via_link">Über einen Link verbinden</string>
<string name="connect_button">Verbinden</string> <string name="connect_button">Verbinden</string>
@@ -1512,7 +1512,7 @@
<string name="remove_member_button">Mitglied entfernen</string> <string name="remove_member_button">Mitglied entfernen</string>
<string name="block_member_confirmation">Blockieren</string> <string name="block_member_confirmation">Blockieren</string>
<string name="unblock_member_question">Mitglied freigeben?</string> <string name="unblock_member_question">Mitglied freigeben?</string>
<string name="blocked_items_description">%d Nachrichten wurden blockiert</string> <string name="blocked_items_description">%d Nachrichten blockiert</string>
<string name="block_member_button">Mitglied blockieren</string> <string name="block_member_button">Mitglied blockieren</string>
<string name="connect_plan_repeat_join_request">Verbindungsanfrage wiederholen?</string> <string name="connect_plan_repeat_join_request">Verbindungsanfrage wiederholen?</string>
<string name="button_remove_member_question">Mitglied entfernen?</string> <string name="button_remove_member_question">Mitglied entfernen?</string>
@@ -1530,8 +1530,8 @@
<string name="non_content_uri_alert_title">Ungültiger Datei-Pfad</string> <string name="non_content_uri_alert_title">Ungültiger Datei-Pfad</string>
<string name="connect_plan_you_have_already_requested_connection_via_this_address">Sie haben über diese Adresse bereits eine Verbindung beantragt!</string> <string name="connect_plan_you_have_already_requested_connection_via_this_address">Sie haben über diese Adresse bereits eine Verbindung beantragt!</string>
<string name="terminal_always_visible">Die Konsole in einem neuen Fenster anzeigen</string> <string name="terminal_always_visible">Die Konsole in einem neuen Fenster anzeigen</string>
<string name="block_member_desc">Von %s werden alle neuen Nachrichten ausgeblendet!</string> <string name="block_member_desc">Alle neuen Nachrichten von %s werden ausgeblendet!</string>
<string name="blocked_item_description">Blockiert</string> <string name="blocked_item_description">blockiert</string>
<string name="encryption_renegotiation_error">Fehler bei der Neuverhandlung der Verschlüsselung</string> <string name="encryption_renegotiation_error">Fehler bei der Neuverhandlung der Verschlüsselung</string>
<string name="alert_text_encryption_renegotiation_failed">Neuverhandlung der Verschlüsselung fehlgeschlagen</string> <string name="alert_text_encryption_renegotiation_failed">Neuverhandlung der Verschlüsselung fehlgeschlagen</string>
<string name="v5_4_block_group_members">Gruppenmitglieder blockieren</string> <string name="v5_4_block_group_members">Gruppenmitglieder blockieren</string>
@@ -1672,43 +1672,8 @@
<string name="possible_slow_function_title">Langsame Funktion</string> <string name="possible_slow_function_title">Langsame Funktion</string>
<string name="show_slow_api_calls">Zeige langsame API-Aufrufe an</string> <string name="show_slow_api_calls">Zeige langsame API-Aufrufe an</string>
<string name="group_member_status_unknown_short">unbekannt</string> <string name="group_member_status_unknown_short">unbekannt</string>
<string name="possible_deadlock_title">Blockade</string>
<string name="developer_options_section">Optionen für Entwickler</string> <string name="developer_options_section">Optionen für Entwickler</string>
<string name="possible_deadlock_desc">Die Code-Ausführung dauert zu lange: %1$d Sekunden. Wahrscheinlich ist die App eingefroren: %2$s</string>
<string name="group_member_status_unknown">unbekannter Gruppenmitglieds-Status</string> <string name="group_member_status_unknown">unbekannter Gruppenmitglieds-Status</string>
<string name="v5_5_private_notes_descr">Mit verschlüsselten Dateien und Medien.</string>
<string name="v5_5_private_notes">Private Notizen</string>
<string name="clear_note_folder_warning">Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!</string>
<string name="clear_note_folder_question">Private Notizen löschen?</string>
<string name="rcv_group_event_member_blocked">%s wurde blockiert</string>
<string name="rcv_group_event_member_unblocked">%s wurde freigegeben</string>
<string name="snd_group_event_member_blocked">Sie haben %s blockiert</string>
<string name="snd_group_event_member_unblocked">Sie haben %s freigegeben</string>
<string name="block_for_all_question">Mitglied für Alle blockieren?</string>
<string name="saved_message_title">Gespeicherte Nachricht</string>
<string name="v5_5_simpler_connect_ui">Zum Verbinden den Link einfügen!</string>
<string name="v5_5_message_delivery_descr">Mit reduziertem Akkuverbrauch.</string>
<string name="v5_5_join_group_conversation_descr">Aktueller Nachrichtenverlauf und verbesserter Gruppenverzeichnis-Bot.</string>
<string name="v5_5_simpler_connect_ui_descr">Von der Suchleiste werden Einladungslinks akzeptiert.</string>
<string name="unblock_for_all">Für Alle freigeben</string>
<string name="unblock_for_all_question">Mitglied für Alle freigeben?</string>
<string name="member_info_member_blocked">wurde blockiert</string>
<string name="blocked_by_admin_item_description">ist vom Administrator blockiert worden</string>
<string name="member_blocked_by_admin">wurde vom Administrator blockiert</string>
<string name="block_for_all">Für Alle blockiert</string>
<string name="info_row_created_at">Erstellt um</string>
<string name="share_text_created_at">Erstellt um: %s</string>
<string name="blocked_by_admin_items_description">%d Nachrichten wurden vom Administrator blockiert</string>
<string name="error_blocking_member_for_all">Fehler beim Blockieren des Mitglieds für Alle</string>
<string name="error_creating_message">Fehler beim Erstellen der Nachricht</string>
<string name="error_deleting_note_folder">Fehler beim Löschen der privaten Notizen</string>
<string name="v5_5_message_delivery">Verbesserte Zustellung von Nachrichten</string>
<string name="v5_5_join_group_conversation">Gruppenunterhaltungen beitreten</string>
<string name="v5_5_new_interface_languages">Ungarische und türkische Bedienoberfläche</string>
<string name="profile_update_event_contact_name_changed">Kontaktname %1$s wurde auf %2$s geändert</string>
<string name="profile_update_event_member_name_changed">Mitgliedsname %1$s wurde auf %2$s geändert</string>
<string name="profile_update_event_set_new_address">Neue Kontaktadresse wurde festgelegt</string>
<string name="profile_update_event_set_new_picture">Neues Profil-Bild wurde festgelegt</string>
<string name="profile_update_event_updated_profile">Das Profil wurde aktualisiert</string>
<string name="profile_update_event_removed_picture">Profil-Bild wurde entfernt</string>
<string name="profile_update_event_removed_address">Kontaktadresse wurde entfernt</string>
<string name="note_folder_local_display_name">Private Notizen</string>
</resources> </resources>

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