Compare commits
1 Commits
f/chat-nex
...
av/multipl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa9f5b3250 |
41
Dockerfile
41
Dockerfile
@@ -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 /
|
||||||
|
|||||||
@@ -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.).
|
||||||
|
|||||||
@@ -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") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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&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&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&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&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&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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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&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&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&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&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&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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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]?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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!";
|
||||||
|
|
||||||
|
|||||||
@@ -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!";
|
||||||
|
|
||||||
|
|||||||
@@ -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!";
|
||||||
|
|
||||||
|
|||||||
@@ -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!";
|
||||||
|
|
||||||
|
|||||||
@@ -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!" = "Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!";
|
||||||
|
|
||||||
|
|||||||
@@ -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. */
|
||||||
" (" = " (";
|
" (" = " (";
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
Reference in New Issue
Block a user