Compare commits

..

2 Commits

Author SHA1 Message Date
Evgeny Poberezkin
ff57bef1e9 Merge branch 'master' into av/rtl-animation 2023-09-07 22:48:01 +01:00
Avently
cf4e2acd0a multiplatform: animation for RTL layout 2023-09-07 04:50:33 +08:00
132 changed files with 910 additions and 10530 deletions

View File

@@ -2,7 +2,7 @@
[![GitHub downloads](https://img.shields.io/github/downloads/simplex-chat/simplex-chat/total)](https://github.com/simplex-chat/simplex-chat/releases)
[![GitHub release](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases)
[![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat)
<a rel="me" href="https://mastodon.social/@simplex">![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)</a>
[![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@simplex)
| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) |
@@ -15,7 +15,7 @@
## Welcome to SimpleX Chat!
1. 📲 [Install the app](#install-the-app).
2. ↔️ [Connect to the team](#connect-to-the-team), [join user groups](#join-user-groups) and [follow our updates](#follow-our-updates).
2. ↔️ [Connect to the team](#connect-to-the-team-via-the-app) and [join user groups](#join-user-groups).
3. 🤝 [Make a private connection](#make-a-private-connection) with a friend.
4. 🔤 [Help translating SimpleX Chat](#help-translating-simplex-chat).
5. ⚡️ [Contribute](#contribute) and [help us with donations](#help-us-with-donations).
@@ -40,22 +40,14 @@
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
- 🖥 Available as a terminal (console) [app / CLI](#zap-quick-installation-of-a-terminal-app) on Linux, MacOS, Windows.
## Connect to the team
You can connect to the team via the app using "chat with the developers button" available when you have no conversations in the profile, "Send questions and ideas" in the app settings or via our [SimpleX address](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). Please connect to:
## Connect to the team via the app
- to ask any questions
- to suggest any improvements
- to share anything relevant
We are replying the questions manually, so it is not instant it can take up to 24 hours.
If you are interested in helping us to integrate open-source language models, and in [joining our team](./docs/JOIN_TEAM.md), please get in touch.
## Join user groups
You can join the groups created by other users via the new [directory service](https://simplex.chat/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). We are not responsible for the content shared in these groups.
**Please note**: The groups below are created for the users to be able to ask questions, make suggestions and ask questions about SimpleX Chat only.
You also can:
@@ -87,14 +79,7 @@ There are groups in other languages, that we have the apps interface translated
You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
## Follow our updates
We publish our updates and releases via:
- [Reddit](https://www.reddit.com/r/SimpleXChat/), [Twitter](https://twitter.com/SimpleXChat), [Lemmy](https://lemmy.ml/c/simplex), [Mastodon](https://mastodon.social/@simplex) and [Nostr](https://snort.social/p/npub1exv22uulqnmlluszc4yk92jhs2e5ajcs6mu3t00a6avzjcalj9csm7d828).
- SimpleX Chat [team profile](#connect-to-the-team).
- [blog](https://simplex.chat/blog/) and [RSS feed](https://simplex.chat/feed.rss).
- [mailing list](https://simplex.chat/#join-simplex), very rarely.
You can also join the group created by other users by searching for them via the [directory service](https://simplex.chat/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). We are not responsible for the content shared in these groups.
## Make a private connection

View File

@@ -563,10 +563,6 @@ final class ChatModel: ObservableObject {
}
}
func getGroupMember(_ groupMemberId: Int64) -> GroupMember? {
groupMembers.first(where: { $0.groupMemberId == groupMemberId })
}
func upsertGroupMember(_ groupInfo: GroupInfo, _ member: GroupMember) -> Bool {
// user member was updated
if groupInfo.membership.groupMemberId == member.groupMemberId {

View File

@@ -315,18 +315,34 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
throw r
}
func apiSendMessage(sendRef: SendRef, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
let chatModel = ChatModel.shared
let cmd: ChatCommand = .apiSendMessage(sendRef: sendRef, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
switch sendRef {
case .direct:
let cItem = await sendMessageDirect(cmd)
return cItem
case .group(_, .some(_)):
let cItem = await sendMessageDirect(cmd)
return cItem
case .group(_, .none):
let r = await chatSendCmd(cmd, bgDelay: msgDelay)
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
let r: ChatResponse
if type == .direct {
var cItem: ChatItem? = nil
let endTask = beginBGTask({
if let cItem = cItem {
DispatchQueue.main.async {
chatModel.messageDelivery.removeValue(forKey: cItem.id)
}
}
})
r = await chatSendCmd(cmd, bgTask: false)
if case let .newChatItem(_, aChatItem) = r {
cItem = aChatItem.chatItem
chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
return cItem
}
if let networkErrorAlert = networkErrorAlert(r) {
AlertManager.shared.showAlert(networkErrorAlert)
} else {
sendMessageErrorAlert(r)
}
endTask()
return nil
} else {
r = await chatSendCmd(cmd, bgDelay: msgDelay)
if case let .newChatItem(_, aChatItem) = r {
return aChatItem.chatItem
}
@@ -335,31 +351,6 @@ func apiSendMessage(sendRef: SendRef, file: CryptoFile?, quotedItemId: Int64?, m
}
}
private func sendMessageDirect(_ cmd: ChatCommand) async -> ChatItem? {
let chatModel = ChatModel.shared
var cItem: ChatItem? = nil
let endTask = beginBGTask({
if let cItem = cItem {
DispatchQueue.main.async {
chatModel.messageDelivery.removeValue(forKey: cItem.id)
}
}
})
let r = await chatSendCmd(cmd, bgTask: false)
if case let .newChatItem(_, aChatItem) = r {
cItem = aChatItem.chatItem
chatModel.messageDelivery[aChatItem.chatItem.id] = endTask
return cItem
}
if let networkErrorAlert = networkErrorAlert(r) {
AlertManager.shared.showAlert(networkErrorAlert)
} else {
sendMessageErrorAlert(r)
}
endTask()
return nil
}
private func sendMessageErrorAlert(_ r: ChatResponse) {
logger.error("apiSendMessage error: \(String(describing: r))")
AlertManager.shared.showAlertMsg(

View File

@@ -41,7 +41,7 @@ struct CIRcvDecryptionError: View {
.onAppear {
// for direct chat ConnectionStats are populated on opening chat, see ChatView onAppear
if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember, _) = chatItem.chatDir {
case let .groupRcv(groupMember) = chatItem.chatDir {
do {
let (member, stats) = try apiGroupMemberInfo(groupInfo.apiId, groupMember.groupMemberId)
if let s = stats {
@@ -78,7 +78,7 @@ struct CIRcvDecryptionError: View {
basicDecryptionErrorItem()
}
} else if case let .group(groupInfo) = chat.chatInfo,
case let .groupRcv(groupMember, _) = chatItem.chatDir,
case let .groupRcv(groupMember) = chatItem.chatDir,
let modelMember = ChatModel.shared.groupMembers.first(where: { $0.id == groupMember.id }),
let memberStats = modelMember.activeConn?.connectionStats {
if memberStats.ratchetSyncAllowed {

View File

@@ -33,7 +33,7 @@ struct DeletedItemView_Previews: PreviewProvider {
static var previews: some View {
Group {
DeletedItemView(chatItem: ChatItem.getDeletedContentSample())
DeletedItemView(chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData, messageScope: .msGroup)))
DeletedItemView(chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)))
}
.previewLayout(.fixed(width: 360, height: 200))
}

File diff suppressed because one or more lines are too long

View File

@@ -290,7 +290,7 @@ struct ChatItemInfoView: View {
private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] {
memberDeliveryStatuses.compactMap({ mds in
if let mem = ChatModel.shared.getGroupMember(mds.groupMemberId) {
if let mem = ChatModel.shared.groupMembers.first(where: { $0.groupMemberId == mds.groupMemberId }) {
return (mem, mds.memberDeliveryStatus)
} else {
return nil

View File

@@ -141,9 +141,6 @@ struct ChatView: View {
.appSheet(isPresented: $showChatInfoSheet) {
GroupChatInfoView(chat: chat, groupInfo: groupInfo)
}
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
}
}
}
ToolbarItem(placement: .navigationBarTrailing) {
@@ -210,13 +207,6 @@ struct ChatView: View {
logger.error("apiContactInfo error: \(responseError(error))")
}
}
} else if case let .group(groupInfo) = cInfo {
Task {
let groupMembers = await apiListMembers(groupInfo.groupId)
await MainActor.run {
ChatModel.shared.groupMembers = groupMembers
}
}
}
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
composeState = draft
@@ -438,36 +428,29 @@ struct ChatView: View {
}
@ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
if case let .groupRcv(member, msgScope) = ci.chatDir,
if case let .groupRcv(member) = ci.chatDir,
case let .group(groupInfo) = chat.chatInfo {
let (prevItem, nextItem) = chatModel.getChatItemNeighbors(ci)
if ci.memberConnected != nil && nextItem?.memberConnected != nil {
// memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView
ZStack {} // scroll doesn't work if it's EmptyView()
} else {
if prevItem == nil || showMemberImage(member, msgScope, prevItem) {
if prevItem == nil || showMemberImage(member, prevItem) {
VStack(alignment: .leading, spacing: 4) {
if ci.content.showMemberName {
Group {
if msgScope == .msDirect {
Text("\(member.displayName) **only to you**")
} else {
Text(member.displayName)
}
}
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, memberImageSize + 14)
.padding(.top, 7)
Text(member.displayName)
.font(.caption)
.foregroundStyle(.secondary)
.padding(.leading, memberImageSize + 14)
.padding(.top, 7)
}
HStack(alignment: .top, spacing: 8) {
RcvMemberImageWithMenu(
member: member,
groupInfo: groupInfo,
composeState: $composeState,
selectedMember: $selectedMember
)
.environmentObject(chat)
ProfileImage(imageStr: member.memberProfile.image)
.frame(width: memberImageSize, height: memberImageSize)
.onTapGesture { selectedMember = member }
.appSheet(item: $selectedMember) { member in
GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true)
}
chatItemWithMenu(ci, maxWidth)
}
}
@@ -481,34 +464,6 @@ struct ChatView: View {
.padding(.leading, memberImageSize + 8 + 12)
}
}
} else if case let .groupSnd(directMember) = ci.chatDir {
let (prevItem, _) = chatModel.getChatItemNeighbors(ci)
if prevItem == nil || showSndScope(directMember, prevItem) {
VStack(alignment: .trailing, spacing: 4) {
if ci.content.showMemberName {
HStack {
if let directMember = directMember {
Text("**only to** \(directMember.displayName)")
ProfileImage(imageStr: directMember.image)
.frame(width: 20, height: 20)
.onTapGesture { selectedMember = directMember }
} else {
Text("to group")
}
}
.font(.caption)
.foregroundStyle(.secondary)
.padding(.top, 7)
}
chatItemWithMenu(ci, maxWidth)
}
.padding(.horizontal)
.padding(.top, 5)
} else {
chatItemWithMenu(ci, maxWidth)
.padding(.horizontal)
.padding(.top, 5)
}
} else {
chatItemWithMenu(ci, maxWidth)
.padding(.horizontal)
@@ -516,69 +471,6 @@ struct ChatView: View {
}
}
private struct RcvMemberImageWithMenu: View {
var member: GroupMember
var groupInfo: GroupInfo
@Binding var composeState: ComposeState
@Binding var selectedMember: GroupMember?
@State private var allowMenu: Bool = true
var body: some View {
let menuItems = memberMenu(member, groupInfo)
if menuItems.isEmpty {
profileImage()
} else {
let uiMenu: Binding<UIMenu> = Binding(
get: { UIMenu(title: "", children: menuItems) },
set: { _ in }
)
profileImage()
.uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu)
}
}
private func profileImage() -> some View {
ProfileImage(imageStr: member.memberProfile.image)
.frame(width: memberImageSize, height: memberImageSize)
.onTapGesture { selectedMember = member }
}
private func memberMenu(_ member: GroupMember, _ groupInfo: GroupInfo) -> [UIMenuElement] {
var menu: [UIMenuElement] = []
if let memberWithConn = ChatModel.shared.getGroupMember(member.groupMemberId),
memberWithConn.allowedToSendDirectlyTo(groupInfo: groupInfo) {
menu.append(memberMenuSendDirectlyUIAction(memberWithConn, groupInfo))
}
return menu
}
private func memberMenuSendDirectlyUIAction(_ toDirectMember: GroupMember, _ groupInfo: GroupInfo) -> UIAction {
UIAction(
title: NSLocalizedString("Send directly", comment: "chat item action"),
image: UIImage(systemName: "arrow.left.arrow.right")
) { _ in
let canSend = toDirectMember.canSendDirectlyTo(groupInfo: groupInfo)
switch canSend {
case .notConnected:
AlertManager.shared.showAlert(cantSendDirectlyNotConnectedAlert())
case .notSupported:
AlertManager.shared.showAlert(cantSendDirectlyNotSupportedAlert())
case .canSend:
withAnimation {
if composeState.editing {
composeState = ComposeState(directMember: .directMember(groupMember: toDirectMember))
} else {
composeState = composeState.copy(
directMember: .directMember(groupMember: toDirectMember),
contextItem: .noContextItem
)
}
}
}
}
}
}
private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View {
ChatItemWithMenu(
ci: ci,
@@ -705,15 +597,7 @@ struct ChatView: View {
menu.append(rm)
}
if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live {
if ci.directMember == nil {
menu.append(replyUIAction())
}
if case let .group(groupInfo) = chat.chatInfo,
let toDirectMember = ci.memberToReplyDirectlyTo,
let toDirectMemberWithConn = ChatModel.shared.getGroupMember(toDirectMember.groupMemberId),
toDirectMemberWithConn.allowedToSendDirectlyTo(groupInfo: groupInfo) {
menu.append(replyDirectlyUIAction(toDirectMemberWithConn, groupInfo))
}
menu.append(replyUIAction())
}
menu.append(shareUIAction())
menu.append(copyUIAction())
@@ -762,45 +646,13 @@ struct ChatView: View {
private func replyUIAction() -> UIAction {
UIAction(
title: NSLocalizedString("Reply", comment: "chat item action"),
image: UIImage(systemName: "arrowshape.turn.up.left.2")
image: UIImage(systemName: "arrowshape.turn.up.left")
) { _ in
withAnimation {
if composeState.editing {
composeState = ComposeState(contextItem: .quotedItem(chatItem: ci))
} else {
composeState = composeState.copy(
directMember: .noDirectMember,
contextItem: .quotedItem(chatItem: ci)
)
}
}
}
}
private func replyDirectlyUIAction(_ toDirectMember: GroupMember, _ groupInfo: GroupInfo) -> UIAction {
UIAction(
title: NSLocalizedString("Reply directly", comment: "chat item action"),
image: UIImage(systemName: "arrowshape.turn.up.left")
) { _ in
let canSend = toDirectMember.canSendDirectlyTo(groupInfo: groupInfo)
switch canSend {
case .notConnected:
AlertManager.shared.showAlert(cantSendDirectlyNotConnectedAlert())
case .notSupported:
AlertManager.shared.showAlert(cantSendDirectlyNotSupportedAlert())
case .canSend:
withAnimation {
if composeState.editing {
composeState = ComposeState(
directMember: .directMember(groupMember: toDirectMember),
contextItem: .quotedItem(chatItem: ci)
)
} else {
composeState = composeState.copy(
directMember: .directMember(groupMember: toDirectMember),
contextItem: .quotedItem(chatItem: ci)
)
}
composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci))
}
}
}
@@ -1022,33 +874,10 @@ struct ChatView: View {
}
}
private static func cantSendDirectlyNotConnectedAlert() -> Alert {
Alert(
title: Text("Can't send directly!"),
message: Text("Member is not connected.")
)
}
private static func cantSendDirectlyNotSupportedAlert() -> Alert {
Alert(
title: Text("Can't send directly!"),
message: Text("Member doesn't support this feature, ask them to update.")
)
}
private func showMemberImage(_ member: GroupMember, _ msgScope: MessageScope, _ prevItem: ChatItem?) -> Bool {
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
switch (prevItem?.chatDir) {
case .groupSnd: return true
case let .groupRcv(prevMember, prevMsgScope): return prevMember.groupMemberId != member.groupMemberId || prevMsgScope != msgScope
default: return false
}
}
private func showSndScope(_ directMember: GroupMember?, _ prevItem: ChatItem?) -> Bool {
switch (prevItem?.chatDir) {
case .groupRcv: return directMember != nil
case let .groupSnd(prevDirectMember):
return prevDirectMember?.groupMemberId != directMember?.groupMemberId
case let .groupRcv(prevMember): return prevMember.groupMemberId != member.groupMemberId
default: return false
}
}

View File

@@ -11,12 +11,6 @@ import SimpleXChat
import SwiftyGif
import PhotosUI
enum ComposeDirectMember {
case noDirectMember
case directMember(groupMember: GroupMember)
case directMemberCancelled
}
enum ComposePreview {
case noPreview
case linkPreview(linkPreview: LinkPreview?)
@@ -46,7 +40,6 @@ struct LiveMessage {
struct ComposeState {
var message: String
var liveMessage: LiveMessage? = nil
var directMember: ComposeDirectMember
var preview: ComposePreview
var contextItem: ComposeContextItem
var voiceMessageRecordingState: VoiceMessageRecordingState
@@ -56,14 +49,12 @@ struct ComposeState {
init(
message: String = "",
liveMessage: LiveMessage? = nil,
directMember: ComposeDirectMember = .noDirectMember,
preview: ComposePreview = .noPreview,
contextItem: ComposeContextItem = .noContextItem,
voiceMessageRecordingState: VoiceMessageRecordingState = .noRecording
) {
self.message = message
self.liveMessage = liveMessage
self.directMember = directMember
self.preview = preview
self.contextItem = contextItem
self.voiceMessageRecordingState = voiceMessageRecordingState
@@ -71,11 +62,6 @@ struct ComposeState {
init(editingItem: ChatItem) {
self.message = editingItem.content.text
if let directMember = editingItem.directMember {
self.directMember = .directMember(groupMember: directMember)
} else {
self.directMember = .noDirectMember
}
self.preview = chatItemPreview(chatItem: editingItem)
self.contextItem = .editingItem(chatItem: editingItem)
if let emc = editingItem.content.msgContent,
@@ -89,7 +75,6 @@ struct ComposeState {
func copy(
message: String? = nil,
liveMessage: LiveMessage? = nil,
directMember: ComposeDirectMember? = nil,
preview: ComposePreview? = nil,
contextItem: ComposeContextItem? = nil,
voiceMessageRecordingState: VoiceMessageRecordingState? = nil
@@ -97,7 +82,6 @@ struct ComposeState {
ComposeState(
message: message ?? self.message,
liveMessage: liveMessage ?? self.liveMessage,
directMember: directMember ?? self.directMember,
preview: preview ?? self.preview,
contextItem: contextItem ?? self.contextItem,
voiceMessageRecordingState: voiceMessageRecordingState ?? self.voiceMessageRecordingState
@@ -273,7 +257,6 @@ struct ComposeView: View {
var body: some View {
VStack(spacing: 0) {
contextDirectMemberView()
contextItemView()
switch (composeState.editing, composeState.preview) {
case (true, .filePreview): EmptyView()
@@ -599,26 +582,6 @@ struct ComposeView: View {
}
}
@ViewBuilder private func contextDirectMemberView() -> some View {
switch composeState.directMember {
case .noDirectMember:
EmptyView()
case let .directMember(groupMember):
ContextDirectMemberView(
directMember: groupMember,
cancelDirectMemberContext: { composeState = composeState.copy(
directMember: .directMemberCancelled,
contextItem: .noContextItem
)}
)
case .directMemberCancelled:
ContextDirectMemberView(
directMember: nil,
cancelDirectMemberContext: { composeState = composeState.copy(directMember: .noDirectMember) }
)
}
}
@ViewBuilder private func contextItemView() -> some View {
switch composeState.contextItem {
case .noContextItem:
@@ -784,43 +747,25 @@ struct ComposeView: View {
}
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
var sendRef: SendRef?
let chatId = chat.chatInfo.apiId
switch chat.chatInfo.chatType {
case .direct:
sendRef = .direct(contactId: chatId)
case .group:
switch composeState.directMember {
case let .directMember(groupMember):
sendRef = .group(groupId: chatId, directMemberId: groupMember.groupMemberId)
default:
sendRef = .group(groupId: chatId, directMemberId: nil)
if let chatItem = await apiSendMessage(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
file: file,
quotedItemId: quoted,
msg: mc,
live: live,
ttl: ttl
) {
await MainActor.run {
chatModel.removeLiveDummy(animated: false)
chatModel.addChatItem(chat.chatInfo, chatItem)
}
default: sendRef = nil
return chatItem
}
if let sendRef = sendRef {
if let chatItem = await apiSendMessage(
sendRef: sendRef,
file: file,
quotedItemId: quoted,
msg: mc,
live: live,
ttl: ttl
) {
await MainActor.run {
chatModel.removeLiveDummy(animated: false)
chatModel.addChatItem(chat.chatInfo, chatItem)
}
return chatItem
}
if let file = file {
removeFile(file.filePath)
}
return nil
} else {
logger.error("ComposeView send: sendRef is nil")
return nil
if let file = file {
removeFile(file.filePath)
}
return nil
}
func checkLinkPreview() -> MsgContent {

View File

@@ -1,59 +0,0 @@
//
// ContextDirectMemberView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 11.09.2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ContextDirectMemberView: View {
@Environment(\.colorScheme) var colorScheme
let directMember: GroupMember?
let cancelDirectMemberContext: () -> Void
var body: some View {
HStack {
if let directMember = directMember {
Text("**only to** \(directMember.chatViewName)")
.lineLimit(1)
if let image = directMember.image {
ProfileImage(imageStr: image)
.frame(width: 24, height: 24)
.padding(.trailing, 2)
} else {
Image(systemName: "arrow.left.arrow.right")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
.foregroundColor(.secondary)
.padding(.trailing, 2)
}
} else {
// maybe make it disappear after some time?
Text("send to all group members")
}
Spacer()
Button {
withAnimation {
cancelDirectMemberContext()
}
} label: {
Image(systemName: "multiply")
}
}
.padding(12)
.frame(minHeight: 50)
.frame(maxWidth: .infinity)
.background(colorScheme == .light ? sentColorLight : sentColorDark)
.padding(.top, 8)
}
}
struct ContextDirectMemberView_Previews: PreviewProvider {
static var previews: some View {
ContextDirectMemberView(directMember: GroupMember.sampleData, cancelDirectMemberContext: {})
}
}

View File

@@ -239,7 +239,7 @@ struct GroupMemberInfoView: View {
chatModel.chatId = chat.id
}
} label: {
Label("Open direct chat", systemImage: "message")
Label("Send direct message", systemImage: "message")
}
}
@@ -256,7 +256,7 @@ struct GroupMemberInfoView: View {
logger.error("openDirectChatButton apiGetChat error: \(responseError(error))")
}
} label: {
Label("Open direct chat", systemImage: "message")
Label("Send direct message", systemImage: "message")
}
}

View File

@@ -1819,10 +1819,6 @@
<target>Šifrovat databázi?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>Zašifrovaná databáze</target>
@@ -1953,10 +1949,6 @@
<target>Chyba při vytváření profilu!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Chyba při mazání databáze chatu</target>

View File

@@ -1819,10 +1819,6 @@
<target>Datenbank verschlüsseln?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>Verschlüsselte Datenbank</target>
@@ -1953,10 +1949,6 @@
<target>Fehler beim Erstellen des Profils!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Fehler beim Löschen der Chat-Datenbank</target>

View File

@@ -1819,11 +1819,6 @@
<target>Encrypt database?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<target>Encrypt local files</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>Encrypted database</target>
@@ -1954,11 +1949,6 @@
<target>Error creating profile!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<target>Error decrypting file</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Error deleting chat database</target>

View File

@@ -1819,10 +1819,6 @@
<target>¿Cifrar base de datos?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>Base de datos cifrada</target>
@@ -1953,10 +1949,6 @@
<target>¡Error al crear perfil!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Error al eliminar base de datos</target>

View File

@@ -1,15 +0,0 @@
{
"colors" : [
{
"idiom" : "universal",
"locale" : "fi"
}
],
"properties" : {
"localizable" : true
},
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,23 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.000",
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.533"
}
},
"idiom" : "universal"
}
],
"properties" : {
"localizable" : true
},
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,6 +0,0 @@
/* Bundle display name */
"CFBundleDisplayName" = "SimpleX NSE";
/* Bundle name */
"CFBundleName" = "SimpleX NSE";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved.";

View File

@@ -1,30 +0,0 @@
/* No comment provided by engineer. */
"_italic_" = "\\_italic_";
/* No comment provided by engineer. */
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
/* No comment provided by engineer. */
"*bold*" = "\\*bold*";
/* No comment provided by engineer. */
"`a + b`" = "\\`a + b`";
/* No comment provided by engineer. */
"~strike~" = "\\~strike~";
/* call status */
"connecting call" = "connecting call…";
/* No comment provided by engineer. */
"Connecting server…" = "Connecting to server…";
/* No comment provided by engineer. */
"Connecting server… (error: %@)" = "Connecting to server… (error: %@)";
/* rcv group event chat item */
"member connected" = "connected";
/* No comment provided by engineer. */
"No group!" = "Group not found!";

View File

@@ -1,10 +0,0 @@
/* Bundle name */
"CFBundleName" = "SimpleX";
/* Privacy - Camera Usage Description */
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media";

View File

@@ -1,12 +0,0 @@
{
"developmentRegion" : "en",
"project" : "SimpleX.xcodeproj",
"targetLocale" : "fi",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
},
"version" : "1.0"
}

View File

@@ -1819,10 +1819,6 @@
<target>Chiffrer la base de données ?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>Base de données chiffrée</target>
@@ -1953,10 +1949,6 @@
<target>Erreur lors de la création du profil !</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Erreur lors de la suppression de la base de données du chat</target>

View File

@@ -1819,10 +1819,6 @@
<target>Crittografare il database?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>Database crittografato</target>
@@ -1953,10 +1949,6 @@
<target>Errore nella creazione del profilo!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Errore nell'eliminazione del database della chat</target>

View File

@@ -1818,10 +1818,6 @@
<target>データベースを暗号化しますか?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>暗号化済みデータベース</target>
@@ -1952,10 +1948,6 @@
<target>プロフィール作成にエラー発生!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>チャットデータベース削除にエラー発生</target>

View File

@@ -1819,10 +1819,6 @@
<target>Database versleutelen?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>Versleutelde database</target>
@@ -1953,10 +1949,6 @@
<target>Fout bij aanmaken van profiel!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Fout bij het verwijderen van de chat database</target>

View File

@@ -1819,10 +1819,6 @@
<target>Zaszyfrować bazę danych?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>Zaszyfrowana baza danych</target>
@@ -1953,10 +1949,6 @@
<target>Błąd tworzenia profilu!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Błąd usuwania bazy danych czatu</target>

View File

@@ -1819,10 +1819,6 @@
<target>Зашифровать базу данных?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>База данных зашифрована</target>
@@ -1953,10 +1949,6 @@
<target>Ошибка создания профиля!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>Ошибка при удалении данных чата</target>

View File

@@ -1807,10 +1807,6 @@
<target>Encrypt ฐานข้อมูล?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>Encrypt ฐานข้อมูลเรียบร้อยแล้ว</target>
@@ -1941,10 +1937,6 @@
<target>เกิดข้อผิดพลาดในการสร้างโปรไฟล์!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท</target>

View File

@@ -1,15 +0,0 @@
{
"colors" : [
{
"idiom" : "universal",
"locale" : "uk"
}
],
"properties" : {
"localizable" : true
},
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1891,7 +1891,7 @@
</trans-unit>
<trans-unit id="Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve" approved="no">
<source>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</source>
<target state="translated">Встановіть [SimpleX Chat для терміналу](https://github.com/simplex-chat/simplex-chat)</target>
<target state="translated">Встановіть [SimpleX Chat для терміналу] (https://github.com/simplex-chat/simplex-chat)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Instant push notifications will be hidden!&#10;" xml:space="preserve" approved="no">
@@ -2586,7 +2586,7 @@ We will be adding server redundancy to prevent lost messages.</source>
</trans-unit>
<trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve" approved="no">
<source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source>
<target state="translated">Читайте більше в нашому [GitHub репозиторії](https://github.com/simplex-chat/simplex-chat#readme).</target>
<target state="translated">Читайте більше в нашому [GitHub репозиторії] (https://github.com/simplex-chat/simplex-chat#readme).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Received file event" xml:space="preserve" approved="no">
@@ -3892,17 +3892,17 @@ SimpleX servers cannot see your profile.</source>
</trans-unit>
<trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve" approved="no">
<source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source>
<target state="translated">[Внесок](https://github.com/simplex-chat/simplex-chat#contribute)</target>
<target state="translated">[Внесок] (https://github.com/simplex-chat/simplex-chat#contribute)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve" approved="no">
<source>[Send us email](mailto:chat@simplex.chat)</source>
<target state="translated">[Напишіть нам електронною поштою](mailto:chat@simplex.chat)</target>
<target state="translated">[Напишіть нам електронною поштою] (mailto:chat@simplex.chat)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve" approved="no">
<source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source>
<target state="translated">[Зірка на GitHub](https://github.com/simplex-chat/simplex-chat)</target>
<target state="translated">[Зірка на GitHub] (https://github.com/simplex-chat/simplex-chat)</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="_italic_" xml:space="preserve" approved="no">
@@ -5369,7 +5369,7 @@ SimpleX servers cannot see your profile.</source>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve" approved="no">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
<target state="translated">Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target>
<target state="translated">Читайте більше в [Посібнику користувача] (https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Receiving file will be stopped." xml:space="preserve" approved="no">
@@ -5419,7 +5419,7 @@ SimpleX servers cannot see your profile.</source>
</trans-unit>
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve" approved="no">
<source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source>
<target state="translated">Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target>
<target state="translated">Читайте більше в [Посібнику користувача] (https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Moderated at" xml:space="preserve" approved="no">

View File

@@ -1,23 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.000",
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.533"
}
},
"idiom" : "universal"
}
],
"properties" : {
"localizable" : true
},
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,6 +0,0 @@
/* Bundle display name */
"CFBundleDisplayName" = "SimpleX NSE";
/* Bundle name */
"CFBundleName" = "SimpleX NSE";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved.";

View File

@@ -1,30 +0,0 @@
/* No comment provided by engineer. */
"_italic_" = "\\_italic_";
/* No comment provided by engineer. */
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
/* No comment provided by engineer. */
"*bold*" = "\\*bold*";
/* No comment provided by engineer. */
"`a + b`" = "\\`a + b`";
/* No comment provided by engineer. */
"~strike~" = "\\~strike~";
/* call status */
"connecting call" = "connecting call…";
/* No comment provided by engineer. */
"Connecting server…" = "Connecting to server…";
/* No comment provided by engineer. */
"Connecting server… (error: %@)" = "Connecting to server… (error: %@)";
/* rcv group event chat item */
"member connected" = "connected";
/* No comment provided by engineer. */
"No group!" = "Group not found!";

View File

@@ -1,10 +0,0 @@
/* Bundle name */
"CFBundleName" = "SimpleX";
/* Privacy - Camera Usage Description */
"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages.";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media";

View File

@@ -1,12 +0,0 @@
{
"developmentRegion" : "en",
"project" : "SimpleX.xcodeproj",
"targetLocale" : "uk",
"toolInfo" : {
"toolBuildNumber" : "15A5219j",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.0"
},
"version" : "1.0"
}

View File

@@ -1808,10 +1808,6 @@
<target>加密数据库?</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypt local files" xml:space="preserve">
<source>Encrypt local files</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Encrypted database" xml:space="preserve">
<source>Encrypted database</source>
<target>加密数据库</target>
@@ -1942,10 +1938,6 @@
<target>创建资料错误!</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error decrypting file" xml:space="preserve">
<source>Error decrypting file</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="Error deleting chat database" xml:space="preserve">
<source>Error deleting chat database</source>
<target>删除聊天数据库错误</target>

View File

@@ -1,9 +0,0 @@
/* Bundle display name */
"CFBundleDisplayName" = "SimpleX NSE";
/* Bundle name */
"CFBundleName" = "SimpleX NSE";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. Kaikki oikeudet pidätetään.";

View File

@@ -1,9 +0,0 @@
/* Bundle display name */
"CFBundleDisplayName" = "SimpleX NSE";
/* Bundle name */
"CFBundleName" = "SimpleX NSE";
/* Copyright (human-readable) */
"NSHumanReadableCopyright" = "Авторське право © 2022 SimpleX Chat. Всі права захищені.";

View File

@@ -78,11 +78,11 @@
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; };
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; };
5C9E127E2AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9E12792AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a */; };
5C9E127F2AAE62A500C9D8FF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9E127A2AAE62A500C9D8FF /* libgmpxx.a */; };
5C9E12802AAE62A500C9D8FF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9E127B2AAE62A500C9D8FF /* libgmp.a */; };
5C9E12812AAE62A500C9D8FF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9E127C2AAE62A500C9D8FF /* libffi.a */; };
5C9E12822AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9E127D2AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a */; };
5C9F83F42A9A7D98009AD0AA /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83EF2A9A7D98009AD0AA /* libffi.a */; };
5C9F83F52A9A7D98009AD0AA /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83F02A9A7D98009AD0AA /* libgmp.a */; };
5C9F83F62A9A7D98009AD0AA /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83F12A9A7D98009AD0AA /* libgmpxx.a */; };
5C9F83F72A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83F22A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a */; };
5C9F83F82A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F83F32A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a */; };
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; };
@@ -172,7 +172,6 @@
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; };
649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; };
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
64A2723E2AAF16CD00BE1136 /* ContextDirectMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A2723D2AAF16CD00BE1136 /* ContextDirectMemberView.swift */; };
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
@@ -269,8 +268,6 @@
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = "<group>"; };
5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMediaView.swift; sourceTree = "<group>"; };
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = "<group>"; };
5C136D8E2AAB3D14006DE2FC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = "fi.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5C136D8F2AAB3D14006DE2FC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = "<group>"; };
5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; };
5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; };
@@ -299,8 +296,6 @@
5C5E5D3C282447AB00B0488A /* CallTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallTypes.swift; sourceTree = "<group>"; };
5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = "<group>"; };
5C636F662AAB3D2400751C84 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = "uk.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5C636F672AAB3D2400751C84 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
5C65DAE429C77136003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
5C65DAE629C771B9003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5C65DAE729C771B9003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@@ -338,11 +333,11 @@
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; };
5C9E12792AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a"; sourceTree = "<group>"; };
5C9E127A2AAE62A500C9D8FF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C9E127B2AAE62A500C9D8FF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C9E127C2AAE62A500C9D8FF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C9E127D2AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a"; sourceTree = "<group>"; };
5C9F83EF2A9A7D98009AD0AA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C9F83F02A9A7D98009AD0AA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C9F83F12A9A7D98009AD0AA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C9F83F22A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a"; sourceTree = "<group>"; };
5C9F83F32A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a"; sourceTree = "<group>"; };
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; };
@@ -420,8 +415,6 @@
5CE2BA96284537A800EC33A6 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = "<group>"; };
5CE4407127ADB1D0007B033A /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; };
5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = "<group>"; };
5CE6C7B32AAB1515007F345C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
5CE6C7B42AAB1527007F345C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
@@ -450,7 +443,6 @@
6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
649BCD9F280460FD00C3A862 /* ComposeImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeImageView.swift; sourceTree = "<group>"; };
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
64A2723D2AAF16CD00BE1136 /* ContextDirectMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextDirectMemberView.swift; sourceTree = "<group>"; };
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
@@ -502,13 +494,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
5C9E127F2AAE62A500C9D8FF /* libgmpxx.a in Frameworks */,
5C9E12812AAE62A500C9D8FF /* libffi.a in Frameworks */,
5C9E12802AAE62A500C9D8FF /* libgmp.a in Frameworks */,
5C9F83F82A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C9F83F72A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a in Frameworks */,
5C9F83F62A9A7D98009AD0AA /* libgmpxx.a in Frameworks */,
5C9F83F42A9A7D98009AD0AA /* libffi.a in Frameworks */,
5C9F83F52A9A7D98009AD0AA /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
5C9E127E2AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a in Frameworks */,
5C9E12822AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -569,11 +561,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
5C9E127C2AAE62A500C9D8FF /* libffi.a */,
5C9E127B2AAE62A500C9D8FF /* libgmp.a */,
5C9E127A2AAE62A500C9D8FF /* libgmpxx.a */,
5C9E12792AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a */,
5C9E127D2AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a */,
5C9F83EF2A9A7D98009AD0AA /* libffi.a */,
5C9F83F02A9A7D98009AD0AA /* libgmp.a */,
5C9F83F12A9A7D98009AD0AA /* libgmpxx.a */,
5C9F83F22A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h-ghc8.10.7.a */,
5C9F83F32A9A7D98009AD0AA /* libHSsimplex-chat-5.3.0.6-5utBXdHr6QFBiN3wb3H70h.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -839,7 +831,6 @@
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */,
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */,
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */,
64A2723D2AAF16CD00BE1136 /* ContextDirectMemberView.swift */,
);
path = ComposeMessage;
sourceTree = "<group>";
@@ -1015,8 +1006,6 @@
pl,
ja,
th,
fi,
uk,
);
mainGroup = 5CA059BD279559F40002BEB4;
packageReferences = (
@@ -1122,7 +1111,6 @@
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */,
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */,
5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */,
64A2723E2AAF16CD00BE1136 /* ContextDirectMemberView.swift in Sources */,
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */,
5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */,
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */,
@@ -1299,8 +1287,6 @@
5C6D183329E93FBA00D430B3 /* pl */,
5CAC411B2A192DE800C331A2 /* ja */,
5CA3ED502A9422D1005D71E2 /* th */,
5C136D8F2AAB3D14006DE2FC /* fi */,
5C636F672AAB3D2400751C84 /* uk */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1320,8 +1306,6 @@
5CAB912529E93F9400F34A95 /* pl */,
5CAC41182A192D8400C331A2 /* ja */,
5CA3ED4D2A942170005D71E2 /* th */,
5CE6C7B32AAB1515007F345C /* fi */,
5CE6C7B42AAB1527007F345C /* uk */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1340,8 +1324,6 @@
5C6D183229E93FBA00D430B3 /* pl */,
5CAC411A2A192DE800C331A2 /* ja */,
5CA3ED4F2A9422D1005D71E2 /* th */,
5C136D8E2AAB3D14006DE2FC /* fi */,
5C636F662AAB3D2400751C84 /* uk */,
);
name = "SimpleX--iOS--InfoPlist.strings";
sourceTree = "<group>";
@@ -1475,7 +1457,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 170;
CURRENT_PROJECT_VERSION = 169;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1517,7 +1499,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 170;
CURRENT_PROJECT_VERSION = 169;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@@ -1597,7 +1579,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 170;
CURRENT_PROJECT_VERSION = 169;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1629,7 +1611,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 170;
CURRENT_PROJECT_VERSION = 169;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GENERATE_INFOPLIST_FILE = YES;
@@ -1661,7 +1643,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 170;
CURRENT_PROJECT_VERSION = 169;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1707,7 +1689,7 @@
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 170;
CURRENT_PROJECT_VERSION = 169;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;

View File

@@ -39,7 +39,7 @@ public enum ChatCommand {
case apiGetChats(userId: Int64)
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64)
case apiSendMessage(sendRef: SendRef, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?)
case apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?)
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool)
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64)
@@ -156,10 +156,10 @@ public enum ChatCommand {
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
(search == "" ? "" : " search=\(search)")
case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)"
case let .apiSendMessage(sendRef, file, quotedItemId, mc, live, ttl):
case let .apiSendMessage(type, id, file, quotedItemId, mc, live, ttl):
let msg = encodeJSON(ComposedMessage(fileSource: file, quotedItemId: quotedItemId, msgContent: mc))
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
return "/_send \(sendRefStr(sendRef)) live=\(onOff(live)) ttl=\(ttlStr) json \(msg)"
return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msg)"
case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)"
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)"
@@ -365,14 +365,6 @@ public enum ChatCommand {
"\(type.rawValue)\(id)"
}
func sendRefStr(_ sendRef: SendRef) -> String {
switch sendRef {
case let .direct(contactId): return "@\(contactId)"
case let .group(groupId, .none): return "#\(groupId)"
case let .group(groupId, .some(directMemberId)): return "#\(groupId) @\(directMemberId)"
}
}
func protoServersStr(_ servers: [ServerCfg]) -> String {
encodeJSON(ProtoServersConfig(servers: servers))
}
@@ -833,11 +825,6 @@ public enum ChatResponse: Decodable, Error {
}
}
public enum SendRef {
case direct(contactId: Int64)
case group(groupId: Int64, directMemberId: Int64?)
}
public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? {
switch chatResponse {
case let .chatCmdError(_, .error(error)): return error
@@ -1467,7 +1454,6 @@ public enum ChatErrorType: Decodable {
case agentCommandError(message: String)
case invalidFileDescription(message: String)
case connectionIncognitoChangeProhibited
case peerChatVRangeIncompatible
case internalError(message: String)
case exception(message: String)
}
@@ -1482,7 +1468,6 @@ public enum StoreError: Decodable {
case userNotFoundByContactRequestId(contactRequestId: Int64)
case contactNotFound(contactId: Int64)
case contactNotFoundByName(contactName: ContactName)
case contactNotFoundByMemberId(groupMemberId: Int64)
case contactNotReady(contactName: ContactName)
case duplicateContactLink
case userContactLinkNotFound
@@ -1510,7 +1495,6 @@ public enum StoreError: Decodable {
case rcvFileNotFoundXFTP(agentRcvFileId: String)
case connectionNotFound(agentConnId: String)
case connectionNotFoundById(connId: Int64)
case connectionNotFoundByMemberId(groupMemberId: Int64)
case pendingConnectionNotFound(connId: Int64)
case introNotFound
case uniqueID

View File

@@ -1449,7 +1449,6 @@ public struct ContactSubStatus: Decodable {
public struct Connection: Decodable {
public var connId: Int64
public var agentConnId: String
// public var peerChatVRange: VersionRange
var connStatus: ConnStatus
public var connLevel: Int
public var viaGroupLink: Bool
@@ -1460,7 +1459,6 @@ public struct Connection: Decodable {
private enum CodingKeys: String, CodingKey {
case connId, agentConnId, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode
// case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode
}
public var id: ChatId { get { ":\(connId)" } }
@@ -1468,22 +1466,12 @@ public struct Connection: Decodable {
static let sampleData = Connection(
connId: 1,
agentConnId: "abc",
// peerChatVRange: VersionRange(minVersion: 1, maxVersion: 1),
connStatus: .ready,
connLevel: 0,
viaGroupLink: false
)
}
public struct VersionRange: Decodable {
public var minVersion: Int
public var maxVersion: Int
public func isCompatibleRange(_ range: VersionRange) -> Bool {
minVersion <= range.maxVersion && range.minVersion <= maxVersion
}
}
public struct SecurityCode: Decodable, Equatable {
public init(securityCode: String, verifiedAt: Date) {
self.securityCode = securityCode
@@ -1819,30 +1807,6 @@ public struct GroupMember: Identifiable, Decodable {
return GroupMemberRole.allCases.filter { $0 <= userRole }
}
public func allowedToSendDirectlyTo(groupInfo: GroupInfo) -> Bool {
let userRole = groupInfo.membership.memberRole
return (
(groupInfo.fullGroupPreferences.directMessages.on && userRole >= .author)
|| userRole >= .admin
|| memberRole >= .admin
)
}
public enum CanSendDirectlyTo {
case canSend
case notConnected
case notSupported
}
public func canSendDirectlyTo(groupInfo: GroupInfo) -> CanSendDirectlyTo {
// TODO group-direct: check version
if let activeConn = activeConn {
return .canSend
} else {
return .notConnected
}
}
public var memberIncognito: Bool {
memberProfile.profileId != memberContactProfileId
}
@@ -1870,7 +1834,6 @@ public struct GroupMemberRef: Decodable {
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
case observer = "observer"
case author = "author"
case member = "member"
case admin = "admin"
case owner = "owner"
@@ -1880,7 +1843,6 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec
public var text: String {
switch self {
case .observer: return NSLocalizedString("observer", comment: "member role")
case .author: return NSLocalizedString("author", comment: "member role")
case .member: return NSLocalizedString("member", comment: "member role")
case .admin: return NSLocalizedString("admin", comment: "member role")
case .owner: return NSLocalizedString("owner", comment: "member role")
@@ -1890,10 +1852,9 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Dec
private var comparisonValue: Int {
switch self {
case .observer: return 0
case .author: return 1
case .member: return 2
case .admin: return 3
case .owner: return 4
case .member: return 1
case .admin: return 2
case .owner: return 3
}
}
@@ -2007,7 +1968,7 @@ public struct AChatItem: Decodable {
public var chatItem: ChatItem
public var chatId: String {
if case let .groupRcv(groupMember, _) = chatItem.chatDir {
if case let .groupRcv(groupMember) = chatItem.chatDir {
return groupMember.id
}
return chatInfo.id
@@ -2080,7 +2041,7 @@ public struct ChatItem: Identifiable, Decodable {
public var memberConnected: GroupMember? {
switch chatDir {
case let .groupRcv(groupMember, _):
case .groupRcv(let groupMember):
switch content {
case .rcvGroupEvent(rcvGroupEvent: .memberConnected): return groupMember
default: return nil
@@ -2164,7 +2125,7 @@ public struct ChatItem: Identifiable, Decodable {
public var memberDisplayName: String? {
get {
if case let .groupRcv(groupMember, _) = chatDir {
if case let .groupRcv(groupMember) = chatDir {
return groupMember.displayName
} else {
return nil
@@ -2172,25 +2133,9 @@ public struct ChatItem: Identifiable, Decodable {
}
}
public var directMember: GroupMember? {
switch chatDir {
case let .groupSnd(directMember): return directMember
case let .groupRcv(groupMember, .msDirect): return groupMember
default: return nil
}
}
public var memberToReplyDirectlyTo: GroupMember? {
switch chatDir {
case let .groupSnd(directMember): return directMember
case let .groupRcv(groupMember, _): return groupMember
default: return nil
}
}
public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember)? {
switch (chatInfo, chatDir) {
case let (.group(groupInfo), .groupRcv(groupMember, .msGroup)):
case let (.group(groupInfo), .groupRcv(groupMember)):
let m = groupInfo.membership
return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil
? (groupInfo, groupMember)
@@ -2303,7 +2248,7 @@ public struct ChatItem: Identifiable, Decodable {
public static func liveDummy(_ chatType: ChatType) -> ChatItem {
var item = ChatItem(
chatDir: chatType == ChatType.direct ? CIDirection.directSnd : CIDirection.groupSnd(directMember: nil),
chatDir: chatType == ChatType.direct ? CIDirection.directSnd : CIDirection.groupSnd,
meta: CIMeta(
itemId: -2,
itemTs: .now,
@@ -2335,34 +2280,11 @@ public struct ChatItem: Identifiable, Decodable {
}
}
public enum MessageScope: String, Decodable {
case msGroup = "group"
case msDirect = "direct"
}
public enum CIDirection: Decodable {
case directSnd
case directRcv
case groupSnd(directMember: GroupMember?)
case groupRcv(groupMember: GroupMember, messageScope: MessageScope)
public var sent: Bool {
get {
switch self {
case .directSnd: return true
case .directRcv: return false
case .groupSnd: return true
case .groupRcv: return false
}
}
}
}
public enum CIQDirection: Decodable {
case directSnd
case directRcv
case groupSnd(messageScope: MessageScope)
case groupRcv(groupMember: GroupMember, messageScope: MessageScope)
case groupSnd
case groupRcv(groupMember: GroupMember)
public var sent: Bool {
get {
@@ -2641,17 +2563,12 @@ public enum CIContent: Decodable, ItemContent {
public var showMemberName: Bool {
switch self {
case .rcvMsgContent: return true
case .sndMsgContent: return true
case .rcvDeleted: return true
case .sndDeleted: return true
case .rcvCall: return true
case .sndCall: return true
case .rcvIntegrityError: return true
case .rcvDecryptionError: return true
case .rcvGroupInvitation: return true
case .sndGroupInvitation: return true
case .rcvModerated: return true
case .sndModerated: return true
case .invalidJSON: return true
default: return false
}
@@ -2675,7 +2592,7 @@ public enum MsgDecryptError: String, Decodable {
}
public struct CIQuote: Decodable, ItemContent {
public var chatDir: CIQDirection?
public var chatDir: CIDirection?
public var itemId: Int64?
var sharedMsgId: String? = nil
public var sentAt: Date
@@ -2690,35 +2607,16 @@ public struct CIQuote: Decodable, ItemContent {
}
public func getSender(_ membership: GroupMember?) -> String? {
switch chatDir {
case .directSnd:
return NSLocalizedString("you", comment: "quote sender")
case .directRcv:
return nil
case .groupSnd(.msGroup):
if let membershipName = membership?.displayName {
return membershipName
} else {
return NSLocalizedString("you", comment: "quote sender")
}
case .groupSnd(.msDirect):
// TODO don't quite understand this condition - this is always user's message, why we show the name, and when "you" will be shown?
// Maybe better to repeat "only to <member>" as it was on the original message?
// Or "<user name> only to <member>"?
if let membershipName = membership?.displayName {
return String.localizedStringWithFormat(NSLocalizedString("%@, directly", comment: "quote sender"), membershipName)
} else {
return NSLocalizedString("you, directly", comment: "quote sender")
}
case let .groupRcv(member, .msGroup):
return member.displayName
case let .groupRcv(member, .msDirect):
return String.localizedStringWithFormat(NSLocalizedString("%@ only to you", comment: "quote sender"), member.displayName)
switch (chatDir) {
case .directSnd: return "you"
case .directRcv: return nil
case .groupSnd: return membership?.displayName ?? "you"
case let .groupRcv(member): return member.displayName
case nil: return nil
}
}
public static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIQDirection?, image: String? = nil) -> CIQuote {
public static func getSample(_ itemId: Int64?, _ sentAt: Date, _ text: String, chatDir: CIDirection?, image: String? = nil) -> CIQuote {
let mc: MsgContent
if let image = image {
mc = .image(text: text, image: image)

View File

@@ -59,7 +59,7 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact)
public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent {
let previewMode = ntfPreviewModeGroupDefault.get()
var title: String
if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember, _) = cItem.chatDir {
if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir {
title = groupMsgNtfTitle(groupInfo, groupMember, hideContent: previewMode == .hidden)
} else {
title = previewMode == .hidden ? contactHidden : "\(cInfo.chatViewName):"

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
/* Bundle name */
"CFBundleName" = "SimpleX";
/* Privacy - Camera Usage Description */
"NSCameraUsageDescription" = "SimpleX tarvitsee pääsyn kameraan, jotta se voi skannata QR-koodeja muodostaakseen yhteyden muihin käyttäjiin ja videopuheluita varten.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX käyttää Face ID:tä paikalliseen todennukseen";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX tarvitsee mikrofonia ääni- ja videopuheluita ja ääniviestien tallentamista varten.";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "SimpleX tarvitsee pääsyn valokuvakirjastoon kuvattujen ja vastaanotettujen medioiden tallentamista varten";

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
/* Bundle name */
"CFBundleName" = "SimpleX";
/* Privacy - Camera Usage Description */
"NSCameraUsageDescription" = "SimpleX потребує доступу до камери, щоб сканувати QR-коди для з'єднання з іншими користувачами та для відеодзвінків.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "SimpleX використовує Face ID для локальної автентифікації";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "SimpleX потребує доступу до мікрофона для аудіо та відео дзвінків, а також для запису голосових повідомлень.";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "SimpleX потребує доступу до фототеки для збереження захоплених та отриманих медіафайлів";

View File

@@ -1,5 +1,6 @@
package chat.simplex.common.platform
import android.app.Application
import android.content.Context
import android.media.*
import android.media.AudioManager.AudioPlaybackCallback
@@ -7,10 +8,10 @@ import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED
import android.os.Build
import androidx.compose.runtime.*
import chat.simplex.common.model.*
import chat.simplex.res.MR
import chat.simplex.common.model.ChatItem
import chat.simplex.common.platform.AudioPlayer.duration
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
import java.io.*
@@ -133,25 +134,20 @@ actual object AudioPlayer: AudioPlayerInterface {
}
// Returns real duration of the track
private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? {
val absoluteFilePath = getAppFilePath(fileSource.filePath)
if (!File(absoluteFilePath).exists()) {
Log.e(TAG, "No such file: ${fileSource.filePath}")
private fun start(filePath: String, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? {
if (!File(filePath).exists()) {
Log.e(TAG, "No such file: $filePath")
return null
}
VideoPlayer.stopAll()
RecorderInterface.stopRecording?.invoke()
val current = currentlyPlaying.value
if (current == null || current.first != fileSource.filePath) {
if (current == null || current.first != filePath) {
stopListener()
player.reset()
runCatching {
if (fileSource.cryptoArgs != null) {
player.setDataSource(CryptoMediaSource(readCryptoFile(absoluteFilePath, fileSource.cryptoArgs)))
} else {
player.setDataSource(absoluteFilePath)
}
player.setDataSource(filePath)
}.onFailure {
Log.e(TAG, it.stackTraceToString())
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message)
@@ -166,7 +162,7 @@ actual object AudioPlayer: AudioPlayerInterface {
}
if (seek != null) player.seekTo(seek)
player.start()
currentlyPlaying.value = fileSource.filePath to onProgressUpdate
currentlyPlaying.value = filePath to onProgressUpdate
progressJob = CoroutineScope(Dispatchers.Default).launch {
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
while(isActive && player.isPlaying) {
@@ -233,7 +229,7 @@ actual object AudioPlayer: AudioPlayerInterface {
}
override fun play(
fileSource: CryptoFile,
filePath: String?,
audioPlaying: MutableState<Boolean>,
progress: MutableState<Int>,
duration: MutableState<Int>,
@@ -242,7 +238,7 @@ actual object AudioPlayer: AudioPlayerInterface {
if (progress.value == duration.value) {
progress.value = 0
}
val realDuration = start(fileSource, progress.value) { pro, state ->
val realDuration = start(filePath ?: return, progress.value) { pro, state ->
if (pro != null) {
progress.value = pro
}
@@ -287,21 +283,3 @@ actual object AudioPlayer: AudioPlayerInterface {
}
actual typealias SoundPlayer = chat.simplex.common.helpers.SoundPlayer
class CryptoMediaSource(val data: ByteArray) : MediaDataSource() {
override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int {
if (position >= data.size) return -1
val endPosition: Int = (position + size).toInt()
var sizeLeft: Int = size
if (endPosition > data.size) {
sizeLeft -= endPosition - data.size
}
System.arraycopy(data, position.toInt(), buffer, offset, sizeLeft)
return sizeLeft
}
override fun getSize(): Long = data.size.toLong()
override fun close() {}
}

View File

@@ -8,15 +8,13 @@ import android.provider.MediaStore
import android.webkit.MimeTypeMap
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.UriHandler
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import chat.simplex.common.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.helpers.toUri
import chat.simplex.common.model.CIFile
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.views.helpers.getAppFileUri
import java.io.BufferedOutputStream
import java.io.File
import chat.simplex.res.MR
import java.io.ByteArrayOutputStream
actual fun ClipboardManager.shareText(text: String) {
val sendIntent: Intent = Intent().apply {
@@ -30,17 +28,9 @@ actual fun ClipboardManager.shareText(text: String) {
androidAppContext.startActivity(shareIntent)
}
actual fun shareFile(text: String, fileSource: CryptoFile) {
val uri = if (fileSource.cryptoArgs != null) {
val tmpFile = File(tmpDir, fileSource.filePath)
tmpFile.deleteOnExit()
ChatModel.filesToDelete.add(tmpFile)
decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, tmpFile.absolutePath)
FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", File(tmpFile.absolutePath)).toURI()
} else {
getAppFileUri(fileSource.filePath)
}
val ext = fileSource.filePath.substringAfterLast(".")
actual fun shareFile(text: String, filePath: String) {
val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
val ext = filePath.substringAfterLast(".")
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
@@ -94,16 +84,8 @@ fun saveImage(ciFile: CIFile?) {
uri?.let {
androidAppContext.contentResolver.openOutputStream(uri)?.let { stream ->
val outputStream = BufferedOutputStream(stream)
if (ciFile.fileSource?.cryptoArgs != null) {
createTmpFileAndDelete { tmpFile ->
decryptCryptoFile(filePath, ciFile.fileSource.cryptoArgs, tmpFile.absolutePath)
tmpFile.inputStream().use { it.copyTo(outputStream) }
}
outputStream.close()
} else {
File(filePath).inputStream().use { it.copyTo(outputStream) }
outputStream.close()
}
File(filePath).inputStream().use { it.copyTo(outputStream) }
outputStream.close()
showToast(generalGetString(MR.strings.image_saved))
}
}

View File

@@ -19,7 +19,7 @@ import java.net.URI
@Composable
actual fun SimpleAndAnimatedImageView(
data: ByteArray,
uri: URI,
imageBitmap: ImageBitmap,
file: CIFile?,
imageProvider: () -> ImageGalleryProvider,
@@ -27,7 +27,7 @@ actual fun SimpleAndAnimatedImageView(
) {
val context = LocalContext.current
val imagePainter = rememberAsyncImagePainter(
ImageRequest.Builder(context).data(data = data).size(coil.size.Size.ORIGINAL).build(),
ImageRequest.Builder(context).data(data = uri.toUri()).size(coil.size.Size.ORIGINAL).build(),
placeholder = BitmapPainter(imageBitmap), // show original image while it's still loading by coil
imageLoader = imageLoader
)

View File

@@ -26,7 +26,7 @@ actual fun ReactionIcon(text: String, fontSize: TextUnit) {
@Composable
actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) {
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = {
ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = {
when (cItem.content.msgContent) {
is MsgContent.MCImage -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {

View File

@@ -26,7 +26,7 @@ import dev.icerock.moko.resources.compose.stringResource
import java.net.URI
@Composable
actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) {
actual fun FullScreenImageView(modifier: Modifier, uri: URI, imageBitmap: ImageBitmap) {
// I'm making a new instance of imageLoader here because if I use one instance in multiple places
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want
val imageLoader = ImageLoader.Builder(LocalContext.current)
@@ -40,7 +40,7 @@ actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap:
.build()
Image(
rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = data).size(Size.ORIGINAL).build(),
ImageRequest.Builder(LocalContext.current).data(data = uri.toUri()).size(Size.ORIGINAL).build(),
placeholder = BitmapPainter(imageBitmap), // show original image while it's still loading by coil
imageLoader = imageLoader
),

View File

@@ -1,5 +1,6 @@
package chat.simplex.common.views.helpers
import android.app.Application
import android.content.res.Resources
import android.graphics.*
import android.graphics.Typeface
@@ -11,8 +12,11 @@ import android.text.Spanned
import android.text.SpannedString
import android.text.style.*
import android.util.Base64
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.BaselineShift
@@ -155,18 +159,17 @@ actual fun getAppFileUri(fileName: String): URI =
FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", File(getAppFilePath(fileName))).toURI()
// https://developer.android.com/training/data-storage/shared/documents-files#bitmap
actual fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? {
actual fun getLoadedImage(file: CIFile?): ImageBitmap? {
val filePath = getLoadedFilePath(file)
return if (filePath != null && file != null) {
return if (filePath != null) {
try {
val data = if (file.fileSource?.cryptoArgs != null) {
readCryptoFile(getAppFilePath(file.fileSource.filePath), file.fileSource.cryptoArgs)
} else {
File(getAppFilePath(file.fileName)).readBytes()
}
decodeSampledBitmapFromByteArray(data, 1000, 1000).asImageBitmap() to data
val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
val parcelFileDescriptor = androidAppContext.contentResolver.openFileDescriptor(uri.toUri(), "r")
val fileDescriptor = parcelFileDescriptor?.fileDescriptor
val image = decodeSampledBitmapFromFileDescriptor(fileDescriptor, 1000, 1000)
parcelFileDescriptor?.close()
image.asImageBitmap()
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
null
}
} else {
@@ -175,17 +178,17 @@ actual fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? {
}
// https://developer.android.com/topic/performance/graphics/load-bitmap#load-bitmap
private fun decodeSampledBitmapFromByteArray(data: ByteArray, reqWidth: Int, reqHeight: Int): Bitmap {
private fun decodeSampledBitmapFromFileDescriptor(fileDescriptor: FileDescriptor?, reqWidth: Int, reqHeight: Int): Bitmap {
// First decode with inJustDecodeBounds=true to check dimensions
return BitmapFactory.Options().run {
inJustDecodeBounds = true
BitmapFactory.decodeByteArray(data, 0, data.size)
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, this)
// Calculate inSampleSize
inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
// Decode bitmap with inSampleSize set
inJustDecodeBounds = false
BitmapFactory.decodeByteArray(data, 0, data.size)
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, this)
}
}
@@ -251,26 +254,6 @@ actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitma
}?.asImageBitmap()
}
actual fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap? {
return if (Build.VERSION.SDK_INT >= 31) {
val source = ImageDecoder.createSource(data)
try {
ImageDecoder.decodeBitmap(source)
} catch (e: android.graphics.ImageDecoder.DecodeException) {
Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}")
if (withAlertOnException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.image_decoding_exception_title),
text = generalGetString(MR.strings.image_decoding_exception_desc)
)
}
null
}
} else {
BitmapFactory.decodeByteArray(data, 0, data.size)
}?.asImageBitmap()
}
actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? {
return if (Build.VERSION.SDK_INT >= 28) {
val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri())

View File

@@ -1,6 +1,5 @@
#include <jni.h>
#include <string.h>
#include <stdint.h>
//#include <string.h>
//#include <stdlib.h>
//#include <android/log.h>
@@ -46,10 +45,6 @@ extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
extern char *chat_parse_markdown(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_write_file(const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(const char *from_path, const char *to_path);
extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path);
JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
@@ -120,76 +115,3 @@ Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, __unused
(*env)->ReleaseStringUTFChars(env, salt, _salt);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) {
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer);
jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer);
jstring res = (*env)->NewStringUTF(env, chat_write_file(_path, buff, capacity));
(*env)->ReleaseStringUTFChars(env, path, _path);
return res;
}
JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) {
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
const char *_key = (*env)->GetStringUTFChars(env, key, JNI_FALSE);
const char *_nonce = (*env)->GetStringUTFChars(env, nonce, JNI_FALSE);
jbyte *res = chat_read_file(_path, _key, _nonce);
(*env)->ReleaseStringUTFChars(env, path, _path);
(*env)->ReleaseStringUTFChars(env, key, _key);
(*env)->ReleaseStringUTFChars(env, nonce, _nonce);
jint status = (jint)res[0];
jbyteArray arr;
if (status == 0) {
union {
uint32_t w;
uint8_t b[4];
} len;
len.b[0] = (uint8_t)res[1];
len.b[1] = (uint8_t)res[2];
len.b[2] = (uint8_t)res[3];
len.b[3] = (uint8_t)res[4];
arr = (*env)->NewByteArray(env, len.w);
(*env)->SetByteArrayRegion(env, arr, 0, len.w, res + 5);
} else {
int len = strlen(res + 1); // + 1 offset here is to not include status byte
arr = (*env)->NewByteArray(env, len);
(*env)->SetByteArrayRegion(env, arr, 0, len, res + 1);
}
jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL);
jobject statusObj = (*env)->NewObject(env, (*env)->FindClass(env, "java/lang/Integer"),
(*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Integer"), "<init>", "(I)V"),
status);
(*env)->SetObjectArrayElement(env, ret, 0, statusObj);
(*env)->SetObjectArrayElement(env, ret, 1, arr);
return ret;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatEncryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring to_path) {
const char *_from_path = (*env)->GetStringUTFChars(env, from_path, JNI_FALSE);
const char *_to_path = (*env)->GetStringUTFChars(env, to_path, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_encrypt_file(_from_path, _to_path));
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
(*env)->ReleaseStringUTFChars(env, to_path, _to_path);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatDecryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring key, jstring nonce, jstring to_path) {
const char *_from_path = (*env)->GetStringUTFChars(env, from_path, JNI_FALSE);
const char *_key = (*env)->GetStringUTFChars(env, key, JNI_FALSE);
const char *_nonce = (*env)->GetStringUTFChars(env, nonce, JNI_FALSE);
const char *_to_path = (*env)->GetStringUTFChars(env, to_path, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_decrypt_file(_from_path, _key, _nonce, _to_path));
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
(*env)->ReleaseStringUTFChars(env, key, _key);
(*env)->ReleaseStringUTFChars(env, nonce, _nonce);
(*env)->ReleaseStringUTFChars(env, to_path, _to_path);
return res;
}

View File

@@ -1,7 +1,6 @@
#include <jni.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
// from the RTS
void hs_init(int * argc, char **argv[]);
@@ -21,10 +20,7 @@ extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
extern char *chat_parse_markdown(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_write_file(const char *path, char *ptr, int length);
extern char *chat_read_file(const char *path, const char *key, const char *nonce);
extern char *chat_encrypt_file(const char *from_path, const char *to_path);
extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path);
// As a reference: https://stackoverflow.com/a/60002045
jstring decode_to_utf8_string(JNIEnv *env, char *string) {
@@ -132,76 +128,3 @@ Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass cl
(*env)->ReleaseStringUTFChars(env, salt, _salt);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) {
const char *_path = encode_to_utf8_chars(env, path);
jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer);
jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer);
jstring res = decode_to_utf8_string(env, chat_write_file(_path, buff, capacity));
(*env)->ReleaseStringUTFChars(env, path, _path);
return res;
}
JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) {
const char *_path = encode_to_utf8_chars(env, path);
const char *_key = encode_to_utf8_chars(env, key);
const char *_nonce = encode_to_utf8_chars(env, nonce);
jbyte *res = chat_read_file(_path, _key, _nonce);
(*env)->ReleaseStringUTFChars(env, path, _path);
(*env)->ReleaseStringUTFChars(env, key, _key);
(*env)->ReleaseStringUTFChars(env, nonce, _nonce);
jint status = (jint)res[0];
jbyteArray arr;
if (status == 0) {
union {
uint32_t w;
uint8_t b[4];
} len;
len.b[0] = (uint8_t)res[1];
len.b[1] = (uint8_t)res[2];
len.b[2] = (uint8_t)res[3];
len.b[3] = (uint8_t)res[4];
arr = (*env)->NewByteArray(env, len.w);
(*env)->SetByteArrayRegion(env, arr, 0, len.w, res + 5);
} else {
int len = strlen(res + 1); // + 1 offset here is to not include status byte
arr = (*env)->NewByteArray(env, len);
(*env)->SetByteArrayRegion(env, arr, 0, len, res + 1);
}
jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL);
jobject statusObj = (*env)->NewObject(env, (*env)->FindClass(env, "java/lang/Integer"),
(*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Integer"), "<init>", "(I)V"),
status);
(*env)->SetObjectArrayElement(env, ret, 0, statusObj);
(*env)->SetObjectArrayElement(env, ret, 1, arr);
return ret;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatEncryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring to_path) {
const char *_from_path = encode_to_utf8_chars(env, from_path);
const char *_to_path = encode_to_utf8_chars(env, to_path);
jstring res = decode_to_utf8_string(env, chat_encrypt_file(_from_path, _to_path));
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
(*env)->ReleaseStringUTFChars(env, to_path, _to_path);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatDecryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring key, jstring nonce, jstring to_path) {
const char *_from_path = encode_to_utf8_chars(env, from_path);
const char *_key = encode_to_utf8_chars(env, key);
const char *_nonce = encode_to_utf8_chars(env, nonce);
const char *_to_path = encode_to_utf8_chars(env, to_path);
jstring res = decode_to_utf8_string(env, chat_decrypt_file(_from_path, _key, _nonce, _to_path));
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
(*env)->ReleaseStringUTFChars(env, key, _key);
(*env)->ReleaseStringUTFChars(env, nonce, _nonce);
(*env)->ReleaseStringUTFChars(env, to_path, _to_path);
return res;
}

View File

@@ -13,7 +13,6 @@ import chat.simplex.common.views.chat.ComposeState
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.platform.AudioPlayer
import chat.simplex.common.platform.chatController
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
@@ -1395,13 +1394,6 @@ data class ChatItem (
private val isLiveDummy: Boolean get() = meta.itemId == TEMP_LIVE_CHAT_ITEM_ID
val encryptedFile: Boolean? = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null
val encryptLocalFile: Boolean
get() = file?.fileProtocol == FileProtocol.XFTP &&
content.msgContent !is MsgContent.MCVideo &&
chatController.appPrefs.privacyEncryptLocalFiles.get()
val memberDisplayName: String? get() =
if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.displayName
else null
@@ -1596,9 +1588,8 @@ data class ChatItem (
file = null
)
// TODO group-direct: possibly this has to take sendRef as parameter instead (for directMember)
fun liveDummy(direct: Boolean): ChatItem = ChatItem(
chatDir = if (direct) CIDirection.DirectSnd() else CIDirection.GroupSnd(directMember = null),
chatDir = if (direct) CIDirection.DirectSnd() else CIDirection.GroupSnd(),
meta = CIMeta(
itemId = TEMP_LIVE_CHAT_ITEM_ID,
itemTs = Clock.System.now(),
@@ -1630,33 +1621,12 @@ data class ChatItem (
}
}
@Serializable
enum class MessageScope(val messageScope: String) {
@SerialName("group") MSGroup("group"),
@SerialName("direct") MSDirect("direct");
}
@Serializable
sealed class CIDirection {
@Serializable @SerialName("directSnd") class DirectSnd: CIDirection()
@Serializable @SerialName("directRcv") class DirectRcv: CIDirection()
@Serializable @SerialName("groupSnd") class GroupSnd(val directMember: GroupMember? = null): CIDirection()
@Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember, val messageScope: MessageScope): CIDirection()
val sent: Boolean get() = when(this) {
is DirectSnd -> true
is DirectRcv -> false
is GroupSnd -> true
is GroupRcv -> false
}
}
@Serializable
sealed class CIQDirection {
@Serializable @SerialName("directSnd") class DirectSnd: CIQDirection()
@Serializable @SerialName("directRcv") class DirectRcv: CIQDirection()
@Serializable @SerialName("groupSnd") class GroupSnd(val messageScope: MessageScope): CIQDirection()
@Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember, val messageScope: MessageScope): CIQDirection()
@Serializable @SerialName("groupSnd") class GroupSnd: CIDirection()
@Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember): CIDirection()
val sent: Boolean get() = when(this) {
is DirectSnd -> true
@@ -1951,7 +1921,7 @@ enum class MsgDecryptError {
@Serializable
class CIQuote (
val chatDir: CIQDirection? = null,
val chatDir: CIDirection? = null,
val itemId: Long? = null,
val sharedMsgId: String? = null,
val sentAt: Instant,
@@ -1967,15 +1937,15 @@ class CIQuote (
fun sender(membership: GroupMember?): String? = when (chatDir) {
is CIQDirection.DirectSnd -> generalGetString(MR.strings.sender_you_pronoun)
is CIQDirection.DirectRcv -> null
is CIQDirection.GroupSnd -> membership?.displayName ?: generalGetString(MR.strings.sender_you_pronoun)
is CIQDirection.GroupRcv -> chatDir.groupMember.displayName
is CIDirection.DirectSnd -> generalGetString(MR.strings.sender_you_pronoun)
is CIDirection.DirectRcv -> null
is CIDirection.GroupSnd -> membership?.displayName ?: generalGetString(MR.strings.sender_you_pronoun)
is CIDirection.GroupRcv -> chatDir.groupMember.displayName
null -> null
}
companion object {
fun getSample(itemId: Long?, sentAt: Instant, text: String, chatDir: CIQDirection?): CIQuote =
fun getSample(itemId: Long?, sentAt: Instant, text: String, chatDir: CIDirection?): CIQuote =
CIQuote(chatDir = chatDir, itemId = itemId, sentAt = sentAt, content = MsgContent.MCText(text))
}
}
@@ -2107,7 +2077,7 @@ class CIFile(
}
@Serializable
data class CryptoFile(
class CryptoFile(
val filePath: String,
val cryptoArgs: CryptoFileArgs?
) {
@@ -2117,7 +2087,7 @@ data class CryptoFile(
}
@Serializable
data class CryptoFileArgs(val fileKey: String, val fileNonce: String)
class CryptoFileArgs(val fileKey: String, val fileNonce: String)
class CancelAction(
val uiActionId: StringResource,

View File

@@ -1,59 +0,0 @@
package chat.simplex.common.model
import chat.simplex.common.platform.*
import kotlinx.serialization.*
import java.nio.ByteBuffer
@Serializable
sealed class WriteFileResult {
@Serializable @SerialName("result") data class Result(val cryptoArgs: CryptoFileArgs): WriteFileResult()
@Serializable @SerialName("error") data class Error(val writeError: String): WriteFileResult()
}
/*
fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs {
val str = chatWriteFile(path, data)
return when (val d = json.decodeFromString(WriteFileResult.serializer(), str)) {
is WriteFileResult.Result -> d.cryptoArgs
is WriteFileResult.Error -> throw Exception(d.writeError)
}
}
* */
fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs {
val buffer = ByteBuffer.allocateDirect(data.size)
buffer.put(data)
buffer.rewind()
val str = chatWriteFile(path, buffer)
return when (val d = json.decodeFromString(WriteFileResult.serializer(), str)) {
is WriteFileResult.Result -> d.cryptoArgs
is WriteFileResult.Error -> throw Exception(d.writeError)
}
}
fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray {
val res: Array<Any> = chatReadFile(path, cryptoArgs.fileKey, cryptoArgs.fileNonce)
val status = (res[0] as Integer).toInt()
val arr = res[1] as ByteArray
if (status == 0) {
return arr
} else {
throw Exception(String(arr))
}
}
fun encryptCryptoFile(fromPath: String, toPath: String): CryptoFileArgs {
val str = chatEncryptFile(fromPath, toPath)
val d = json.decodeFromString(WriteFileResult.serializer(), str)
return when (d) {
is WriteFileResult.Result -> d.cryptoArgs
is WriteFileResult.Error -> throw Exception(d.writeError)
}
}
fun decryptCryptoFile(fromPath: String, cryptoArgs: CryptoFileArgs, toPath: String) {
val err = chatDecryptFile(fromPath, cryptoArgs.fileKey, cryptoArgs.fileNonce, toPath)
if (err != "") {
throw Exception(err)
}
}

View File

@@ -5,6 +5,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
@@ -93,7 +94,6 @@ class AppPreferences {
val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true)
val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true)
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
@@ -249,7 +249,6 @@ class AppPreferences {
private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews"
private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft"
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
@@ -587,8 +586,8 @@ object ChatController {
return null
}
suspend fun apiSendMessage(sendRef: SendRef, file: CryptoFile? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? {
val cmd = CC.ApiSendMessage(sendRef, file, quotedItemId, mc, live, ttl)
suspend fun apiSendMessage(type: ChatType, id: Long, file: CryptoFile? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? {
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live, ttl)
val r = sendCmd(cmd)
return when (r) {
is CR.NewChatItem -> r.chatItem
@@ -1414,7 +1413,8 @@ object ChatController {
((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.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
withApi { receiveFile(r.user, file.fileId, encrypted = cItem.encryptLocalFile && chatController.appPrefs.privacyEncryptLocalFiles.get(), auto = true) }
// TODO encrypt images and voice
withApi { receiveFile(r.user, file.fileId, encrypted = false, auto = true) }
}
if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id)) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
@@ -1805,7 +1805,7 @@ sealed class CC {
class ApiGetChats(val userId: Long): CC()
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
class ApiSendMessage(val sendRef: SendRef, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC()
class ApiSendMessage(val type: ChatType, val id: Long, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC()
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
@@ -1909,7 +1909,7 @@ sealed class CC {
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
is ApiSendMessage -> {
val ttlStr = if (ttl != null) "$ttl" else "default"
"/_send ${sendRefStr(sendRef)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
}
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}"
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
@@ -2105,27 +2105,10 @@ sealed class CC {
companion object {
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
fun sendRefStr(sendRef: SendRef): String {
return when (sendRef) {
is SendRef.Direct -> "@${sendRef.contactId}"
is SendRef.Group -> {
when (sendRef.directMemberId) {
null -> "#${sendRef.groupId}"
else -> "#${sendRef.groupId} @${sendRef.directMemberId}"
}
}
}
}
fun protoServersStr(servers: List<ServerCfg>) = json.encodeToString(ProtoServersConfig(servers))
}
}
sealed class SendRef {
class Direct(val contactId: Long): SendRef()
class Group(val groupId: Long, val directMemberId: Long?): SendRef()
}
@Serializable
data class NewUser(
val profile: Profile?,
@@ -3837,7 +3820,6 @@ sealed class ChatErrorType {
is AgentCommandError -> "agentCommandError"
is InvalidFileDescription -> "invalidFileDescription"
is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited"
is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible"
is InternalError -> "internalError"
is CEException -> "exception $message"
}
@@ -3912,7 +3894,6 @@ sealed class ChatErrorType {
@Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType()
@Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType()
@Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType()
@Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType()
@Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType()
@Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType()
}
@@ -3930,7 +3911,6 @@ sealed class StoreError {
is UserNotFoundByContactRequestId -> "userNotFoundByContactRequestId"
is ContactNotFound -> "contactNotFound"
is ContactNotFoundByName -> "contactNotFoundByName"
is ContactNotFoundByMemberId -> "contactNotFoundByMemberId"
is ContactNotReady -> "contactNotReady"
is DuplicateContactLink -> "duplicateContactLink"
is UserContactLinkNotFound -> "userContactLinkNotFound"
@@ -3958,7 +3938,6 @@ sealed class StoreError {
is RcvFileNotFoundXFTP -> "rcvFileNotFoundXFTP"
is ConnectionNotFound -> "connectionNotFound"
is ConnectionNotFoundById -> "connectionNotFoundById"
is ConnectionNotFoundByMemberId -> "connectionNotFoundByMemberId"
is PendingConnectionNotFound -> "pendingConnectionNotFound"
is IntroNotFound -> "introNotFound"
is UniqueID -> "uniqueID"
@@ -3987,7 +3966,6 @@ sealed class StoreError {
@Serializable @SerialName("userNotFoundByContactRequestId") class UserNotFoundByContactRequestId(val contactRequestId: Long): StoreError()
@Serializable @SerialName("contactNotFound") class ContactNotFound(val contactId: Long): StoreError()
@Serializable @SerialName("contactNotFoundByName") class ContactNotFoundByName(val contactName: String): StoreError()
@Serializable @SerialName("contactNotFoundByMemberId") class ContactNotFoundByMemberId(val groupMemberId: Long): StoreError()
@Serializable @SerialName("contactNotReady") class ContactNotReady(val contactName: String): StoreError()
@Serializable @SerialName("duplicateContactLink") object DuplicateContactLink: StoreError()
@Serializable @SerialName("userContactLinkNotFound") object UserContactLinkNotFound: StoreError()
@@ -4015,7 +3993,6 @@ sealed class StoreError {
@Serializable @SerialName("rcvFileNotFoundXFTP") class RcvFileNotFoundXFTP(val agentRcvFileId: String): StoreError()
@Serializable @SerialName("connectionNotFound") class ConnectionNotFound(val agentConnId: String): StoreError()
@Serializable @SerialName("connectionNotFoundById") class ConnectionNotFoundById(val connId: Long): StoreError()
@Serializable @SerialName("connectionNotFoundByMemberId") class ConnectionNotFoundByMemberId(val groupMemberId: Long): StoreError()
@Serializable @SerialName("pendingConnectionNotFound") class PendingConnectionNotFound(val connId: Long): StoreError()
@Serializable @SerialName("introNotFound") object IntroNotFound: StoreError()
@Serializable @SerialName("uniqueID") object UniqueID: StoreError()

View File

@@ -1,9 +1,8 @@
package chat.simplex.common.platform
import chat.simplex.common.BuildConfigCommon
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController
import chat.simplex.common.ui.theme.DefaultTheme
import java.io.File
import java.util.*
enum class AppPlatform {

View File

@@ -4,7 +4,6 @@ import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
import kotlinx.serialization.decodeFromString
import java.nio.ByteBuffer
// ghc's rts
external fun initHS()
@@ -20,10 +19,6 @@ external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
external fun chatParseMarkdown(str: String): String
external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String
external fun chatWriteFile(path: String, buffer: ByteBuffer): String
external fun chatReadFile(path: String, key: String, nonce: String): Array<Any>
external fun chatEncryptFile(fromPath: String, toPath: String): String
external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String
val chatModel: ChatModel
get() = chatController.chatModel

View File

@@ -2,7 +2,6 @@ package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import chat.simplex.common.model.CIFile
import chat.simplex.common.model.CryptoFile
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import java.io.*
@@ -72,16 +71,6 @@ fun getLoadedFilePath(file: CIFile?): String? {
}
}
fun getLoadedFileSource(file: CIFile?): CryptoFile? {
val f = file?.fileSource?.filePath
return if (f != null && file.loaded) {
val filePath = getAppFilePath(f)
if (File(filePath).exists()) file.fileSource else null
} else {
null
}
}
/**
* [rememberedValue] is used in `remember(rememberedValue)`. So when the value changes, file saver will update a callback function
* */

View File

@@ -1,7 +1,7 @@
package chat.simplex.common.platform
import androidx.compose.runtime.MutableState
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatItem
import kotlinx.coroutines.CoroutineScope
interface RecorderInterface {
@@ -18,7 +18,7 @@ expect class RecorderNative(): RecorderInterface
interface AudioPlayerInterface {
fun play(
fileSource: CryptoFile,
filePath: String?,
audioPlaying: MutableState<Boolean>,
progress: MutableState<Int>,
duration: MutableState<Int>,

View File

@@ -2,9 +2,8 @@ package chat.simplex.common.platform
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.UriHandler
import chat.simplex.common.model.CryptoFile
expect fun UriHandler.sendEmail(subject: String, body: CharSequence)
expect fun ClipboardManager.shareText(text: String)
expect fun shareFile(text: String, fileSource: CryptoFile)
expect fun shareFile(text: String, filePath: String)

View File

@@ -1117,7 +1117,7 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha
}
sealed class ProviderMedia {
data class Image(val data: ByteArray, val image: ImageBitmap): ProviderMedia()
data class Image(val uri: URI, val image: ImageBitmap): ProviderMedia()
data class Video(val uri: URI, val preview: String): ProviderMedia()
}
@@ -1155,11 +1155,11 @@ private fun providerForGallery(
val item = item(internalIndex, initialChatId)?.second ?: return null
return when (item.content.msgContent) {
is MsgContent.MCImage -> {
val res = getLoadedImage(item.file)
val imageBitmap: ImageBitmap? = getLoadedImage(item.file)
val filePath = getLoadedFilePath(item.file)
if (res != null && filePath != null) {
val (imageBitmap: ImageBitmap, data: ByteArray) = res
ProviderMedia.Image(data, imageBitmap)
if (imageBitmap != null && filePath != null) {
val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
ProviderMedia.Image(uri, imageBitmap)
} else null
}
is MsgContent.MCVideo -> {
@@ -1286,20 +1286,20 @@ fun PreviewGroupChatLayout() {
SimpleXTheme {
val chatItems = listOf(
ChatItem.getSampleData(
1, CIDirection.GroupSnd(directMember = null), Clock.System.now(), "hello"
1, CIDirection.GroupSnd(), Clock.System.now(), "hello"
),
ChatItem.getSampleData(
2, CIDirection.GroupRcv(GroupMember.sampleData, MessageScope.MSGroup), Clock.System.now(), "hello"
2, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
),
ChatItem.getDeletedContentSampleData(3),
ChatItem.getSampleData(
4, CIDirection.GroupRcv(GroupMember.sampleData, MessageScope.MSGroup), Clock.System.now(), "hello"
4, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
),
ChatItem.getSampleData(
5, CIDirection.GroupSnd(directMember = null), Clock.System.now(), "hello"
5, CIDirection.GroupSnd(), Clock.System.now(), "hello"
),
ChatItem.getSampleData(
6, CIDirection.GroupRcv(GroupMember.sampleData, MessageScope.MSGroup), Clock.System.now(), "hello"
6, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
)
)
val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) }

View File

@@ -318,34 +318,25 @@ fun ComposeView(
}
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? {
// TODO group-direct: directMember in compose state
val chatId = chat.chatInfo.apiId
val sendRef = when (chat.chatInfo.chatType) {
ChatType.Direct -> SendRef.Direct(contactId = chatId)
ChatType.Group -> SendRef.Group(groupId = chatId, directMemberId = null)
else -> null
}
if (sendRef != null) {
val aChatItem = chatModel.controller.apiSendMessage(
sendRef = sendRef,
file = file,
quotedItemId = quoted,
mc = mc,
live = live,
ttl = ttl
)
if (aChatItem != null) {
chatModel.addChatItem(cInfo, aChatItem.chatItem)
return aChatItem.chatItem
}
if (file != null) removeFile(file.filePath)
return null
} else {
Log.e(TAG, "ComposeView send: sendRef is null")
return null
val aChatItem = chatModel.controller.apiSendMessage(
type = cInfo.chatType,
id = cInfo.apiId,
file = file,
quotedItemId = quoted,
mc = mc,
live = live,
ttl = ttl
)
if (aChatItem != null) {
chatModel.addChatItem(cInfo, aChatItem.chatItem)
return aChatItem.chatItem
}
if (file != null) removeFile(file.filePath)
return null
}
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
val cInfo = chat.chatInfo
val cs = composeState.value
@@ -420,8 +411,8 @@ fun ComposeView(
is ComposePreview.MediaPreview -> {
preview.content.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.SimpleImage -> saveImage(it.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get())
is UploadContent.AnimatedImage -> saveAnimImage(it.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get())
is UploadContent.SimpleImage -> saveImage(it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(it.uri)
is UploadContent.Video -> saveFileFromUri(it.uri, encrypted = false)
}
if (file != null) {
@@ -438,21 +429,16 @@ fun ComposeView(
val tmpFile = File(preview.voice)
AudioPlayer.stop(tmpFile.absolutePath)
val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderInterface.extension, "")))
files.add(withContext(Dispatchers.IO) {
if (chatController.appPrefs.privacyEncryptLocalFiles.get()) {
val args = encryptCryptoFile(tmpFile.absolutePath, actualFile.absolutePath)
tmpFile.delete()
CryptoFile(actualFile.name, args)
} else {
Files.move(tmpFile.toPath(), actualFile.toPath())
CryptoFile.plain(actualFile.name)
}
})
withContext(Dispatchers.IO) {
Files.move(tmpFile.toPath(), actualFile.toPath())
}
// TODO encrypt voice files
files.add(CryptoFile.plain(actualFile.name))
deleteUnusedFiles()
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000))
}
is ComposePreview.FilePreview -> {
val file = saveFileFromUri(preview.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get())
val file = saveFileFromUri(preview.uri, encrypted = false)
if (file != null) {
files.add((file))
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))

View File

@@ -17,7 +17,6 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.CryptoFile
import chat.simplex.common.model.durationText
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
@@ -53,7 +52,7 @@ fun ComposeVoiceView(
IconButton(
onClick = {
if (!audioPlaying.value) {
AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, false)
AudioPlayer.play(filePath, audioPlaying, progress, duration, false)
} else {
AudioPlayer.pause(audioPlaying, progress)
}

View File

@@ -71,8 +71,7 @@ fun CIFileView(
when (file.fileStatus) {
is CIFileStatus.RcvInvitation -> {
if (fileSizeValid()) {
val encrypted = file.fileProtocol == FileProtocol.XFTP && chatController.appPrefs.privacyEncryptLocalFiles.get()
receiveFile(file.fileId, encrypted)
receiveFile(file.fileId, false)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file),
@@ -185,9 +184,9 @@ fun CIFileView(
) {
fileIndicator()
val metaReserve = if (edited)
" "
" "
else
" "
" "
if (file != null) {
Column {
Text(
@@ -212,15 +211,7 @@ fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher =
rememberFileChooserLauncher(false, ciFile) { to: URI? ->
val filePath = getLoadedFilePath(ciFile)
if (filePath != null && to != null) {
if (ciFile?.fileSource?.cryptoArgs != null) {
createTmpFileAndDelete { tmpFile ->
decryptCryptoFile(filePath, ciFile.fileSource.cryptoArgs, tmpFile.absolutePath)
copyFileToFile(tmpFile, to) {}
tmpFile.delete()
}
} else {
copyFileToFile(File(filePath), to) {}
}
copyFileToFile(File(filePath), to) {}
}
}

View File

@@ -29,8 +29,6 @@ import java.net.URI
fun CIImageView(
image: String,
file: CIFile?,
encryptLocalFile: Boolean,
metaColor: Color,
imageProvider: () -> ImageGalleryProvider,
showMenu: MutableState<Boolean>,
receiveFile: (Long, Boolean) -> Unit
@@ -50,7 +48,7 @@ fun CIImageView(
icon,
stringResource(stringId),
Modifier.fillMaxSize(),
tint = metaColor
tint = Color.White
)
}
@@ -134,31 +132,28 @@ fun CIImageView(
return false
}
fun imageAndFilePath(file: CIFile?): Triple<ImageBitmap, ByteArray, String>? {
val res = getLoadedImage(file)
if (res != null) {
val (imageBitmap: ImageBitmap, data: ByteArray) = res
val filePath = getLoadedFilePath(file)!!
return Triple(imageBitmap, data, filePath)
}
return null
fun imageAndFilePath(file: CIFile?): Pair<ImageBitmap?, String?> {
val imageBitmap: ImageBitmap? = getLoadedImage(file)
val filePath = getLoadedFilePath(file)
return imageBitmap to filePath
}
Box(
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
contentAlignment = Alignment.TopEnd
) {
val res = remember(file) { imageAndFilePath(file) }
if (res != null) {
val (imageBitmap, data, _) = res
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, onClick) })
val (imageBitmap, filePath) = remember(file) { imageAndFilePath(file) }
if (imageBitmap != null && filePath != null) {
val uri = remember(filePath) { getAppFileUri(filePath.substringAfterLast(File.separator)) }
SimpleAndAnimatedImageView(uri, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, onClick) })
} else {
imageView(base64ToBitmap(image), onClick = {
if (file != null) {
when (file.fileStatus) {
CIFileStatus.RcvInvitation ->
if (fileSizeValid()) {
receiveFile(file.fileId, encryptLocalFile)
// TODO encrypt image
receiveFile(file.fileId, false)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file),
@@ -192,7 +187,7 @@ fun CIImageView(
@Composable
expect fun SimpleAndAnimatedImageView(
data: ByteArray,
uri: URI,
imageBitmap: ImageBitmap,
file: CIFile?,
imageProvider: () -> ImageGalleryProvider,

View File

@@ -44,14 +44,14 @@ fun CIMetaView(
modifier = Modifier.padding(start = 3.dp)
)
} else {
CIMetaText(chatItem.meta, timedMessagesTTL, encrypted = chatItem.encryptedFile, metaColor, paleMetaColor)
CIMetaText(chatItem.meta, timedMessagesTTL, metaColor, paleMetaColor)
}
}
}
@Composable
// changing this function requires updating reserveSpaceForMeta
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, color: Color, paleColor: Color) {
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color, paleColor: Color) {
if (meta.itemEdited) {
StatusIconText(painterResource(MR.images.ic_edit), color)
Spacer(Modifier.width(3.dp))
@@ -77,15 +77,11 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, color:
StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent)
Spacer(Modifier.width(4.dp))
}
if (encrypted != null) {
StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color)
Spacer(Modifier.width(4.dp))
}
Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
// the conditions in this function should match CIMetaText
fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?): String {
fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
val iconSpace = " "
var res = ""
if (meta.itemEdited) res += iconSpace
@@ -99,9 +95,6 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?): Strin
if (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing) {
res += iconSpace
}
if (encrypted != null) {
res += iconSpace
}
return res + meta.timestampText
}

View File

@@ -166,7 +166,7 @@ fun DecryptionErrorItemFixButton(
Text(
buildAnnotatedString {
append(generalGetString(MR.strings.fix_connection))
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) }
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) }
withStyle(reserveTimestampStyle) { append(" ") } // for icon
},
color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
@@ -196,7 +196,7 @@ fun DecryptionErrorItem(
Text(
buildAnnotatedString {
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) }
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) }
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
)

View File

@@ -20,7 +20,8 @@ import androidx.compose.ui.unit.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.platform.getLoadedFilePath
import chat.simplex.common.platform.AudioPlayer
import chat.simplex.res.MR
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -44,16 +45,14 @@ fun CIVoiceView(
) {
if (file != null) {
val f = file.fileSource?.filePath
val fileSource = remember(f, file.fileStatus) { getLoadedFileSource(file) }
val filePath = remember(f, file.fileStatus) { getLoadedFilePath(file) }
var brokenAudio by rememberSaveable(f) { mutableStateOf(false) }
val audioPlaying = rememberSaveable(f) { mutableStateOf(false) }
val progress = rememberSaveable(f) { mutableStateOf(0) }
val duration = rememberSaveable(f) { mutableStateOf(providedDurationSec * 1000) }
val play = {
if (fileSource != null) {
AudioPlayer.play(fileSource, audioPlaying, progress, duration, true)
brokenAudio = !audioPlaying.value
}
AudioPlayer.play(filePath, audioPlaying, progress, duration, true)
brokenAudio = !audioPlaying.value
}
val pause = {
AudioPlayer.pause(audioPlaying, progress)
@@ -68,7 +67,7 @@ fun CIVoiceView(
}
}
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) {
AudioPlayer.seekTo(it, progress, fileSource?.filePath)
AudioPlayer.seekTo(it, progress, filePath)
}
} else {
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick, receiveFile)
@@ -270,7 +269,8 @@ private fun VoiceMsgIndicator(
}
} else {
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId, chatController.appPrefs.privacyEncryptLocalFiles.get()) }, {}, longClick = longClick)
// TODO encrypt voice
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId, false) }, {}, longClick = longClick)
} else if (file?.fileStatus is CIFileStatus.RcvTransfer
|| file?.fileStatus is CIFileStatus.RcvAccepted
) {

View File

@@ -191,9 +191,9 @@ fun ChatItemView(
}
val clipboard = LocalClipboardManager.current
ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = {
val fileSource = getLoadedFileSource(cItem.file)
val filePath = getLoadedFilePath(cItem.file)
when {
fileSource != null -> shareFile(cItem.text, fileSource)
filePath != null -> shareFile(cItem.text, filePath)
else -> clipboard.shareText(cItem.content.text)
}
showMenu.value = false

View File

@@ -226,7 +226,7 @@ fun FramedItemView(
} else {
when (val mc = ci.content.msgContent) {
is MsgContent.MCImage -> {
CIImageView(image = mc.image, file = ci.file, ci.encryptLocalFile, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
if (mc.text == "" && !ci.meta.isLive) {
metaColor = Color.White
} else {

View File

@@ -123,8 +123,8 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
// LALAL
// https://github.com/JetBrains/compose-multiplatform/pull/2015/files#diff-841b3825c504584012e1d1c834d731bae794cce6acad425d81847c8bbbf239e0R24
if (media is ProviderMedia.Image) {
val (data: ByteArray, imageBitmap: ImageBitmap) = media
FullScreenImageView(modifier, data, imageBitmap)
val (uri: URI, imageBitmap: ImageBitmap) = media
FullScreenImageView(modifier, uri, imageBitmap)
} else if (media is ProviderMedia.Video) {
val preview = remember(media.uri.path) { base64ToBitmap(media.preview) }
VideoView(modifier, media.uri, preview, index == settledCurrentPage)
@@ -138,7 +138,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
}
@Composable
expect fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap)
expect fun FullScreenImageView(modifier: Modifier, uri: URI, imageBitmap: ImageBitmap)
@Composable
private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean) {

View File

@@ -76,7 +76,7 @@ fun MarkdownText (
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
"\n"
} else if (meta != null) {
reserveSpaceForMeta(meta, chatTTL, null) // LALAL
reserveSpaceForMeta(meta, chatTTL)
} else {
" "
}

View File

@@ -178,7 +178,7 @@ fun DatabaseLayout(
SectionView(stringResource(MR.strings.chat_database_section)) {
val unencrypted = chatDbEncrypted == false
SettingsActionItem(
if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled)
if (unencrypted) painterResource(MR.images.ic_lock_open) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled)
else painterResource(MR.images.ic_lock),
stringResource(MR.strings.database_passphrase),
click = showSettingsModal() { DatabaseEncryptionView(it) },

View File

@@ -8,6 +8,9 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
@@ -111,13 +114,22 @@ class ModalManager(private val placement: ModalPlacement? = null) {
modalViews.lastOrNull()?.second?.invoke(::closeModal)
return
}
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
AnimatedContent(targetState = modalCount.value,
transitionSpec = {
if (targetState > initialState) {
fromEndToStartTransition()
if (isRtl) {
if (targetState < initialState) {
fromStartToEndTransition()
} else {
fromStartToEndTransition().using(SizeTransform(clip = true) { _, target -> spring(visibilityThreshold = target) })
}
} else {
fromStartToEndTransition()
}.using(SizeTransform(clip = false))
if (targetState > initialState) {
fromEndToStartTransition()
} else {
fromStartToEndTransition()
}.using(SizeTransform(clip = false))
}
}
) {
modalViews.getOrNull(it - 1)?.second?.invoke(::closeModal)

View File

@@ -67,7 +67,7 @@ const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB
expect fun getAppFileUri(fileName: String): URI
// https://developer.android.com/training/data-storage/shared/documents-files#bitmap
expect fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>?
expect fun getLoadedImage(file: CIFile?): ImageBitmap?
expect fun getFileName(uri: URI): String?
@@ -77,8 +77,6 @@ expect fun getFileSize(uri: URI): Long?
expect fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean = true): ImageBitmap?
expect fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap?
expect fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean = true): Any?
fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverrides? {
@@ -97,34 +95,31 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri
return null
}
fun saveImage(uri: URI, encrypted: Boolean): CryptoFile? {
fun saveImage(uri: URI): CryptoFile? {
val bitmap = getBitmapFromUri(uri) ?: return null
return saveImage(bitmap, encrypted)
return saveImage(bitmap)
}
fun saveImage(image: ImageBitmap, encrypted: Boolean): CryptoFile? {
fun saveImage(image: ImageBitmap): CryptoFile? {
// TODO encrypt image
return try {
val ext = if (image.hasAlpha()) "png" else "jpg"
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
val destFileName = generateNewFileName("IMG", ext)
val destFile = File(getAppFilePath(destFileName))
if (encrypted) {
val args = writeCryptoFile(destFile.absolutePath, dataResized.toByteArray())
CryptoFile(destFileName, args)
} else {
val output = FileOutputStream(destFile)
dataResized.writeTo(output)
output.flush()
output.close()
CryptoFile.plain(destFileName)
}
val fileToSave = generateNewFileName("IMG", ext)
val file = File(getAppFilePath(fileToSave))
val output = FileOutputStream(file)
dataResized.writeTo(output)
output.flush()
output.close()
CryptoFile.plain(fileToSave)
} catch (e: Exception) {
Log.e(TAG, "Util.kt saveImage error: ${e.stackTraceToString()}")
null
}
}
fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? {
fun saveAnimImage(uri: URI): CryptoFile? {
// TODO encrypt image
return try {
val filename = getFileName(uri)?.lowercase()
var ext = when {
@@ -134,15 +129,15 @@ fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? {
}
// Just in case the image has a strange extension
if (ext.length < 3 || ext.length > 4) ext = "gif"
val destFileName = generateNewFileName("IMG", ext)
val destFile = File(getAppFilePath(destFileName))
if (encrypted) {
val args = writeCryptoFile(destFile.absolutePath, uri.inputStream()?.readAllBytes() ?: return null)
CryptoFile(destFileName, args)
} else {
Files.copy(uri.inputStream(), destFile.toPath())
CryptoFile.plain(destFileName)
val fileToSave = generateNewFileName("IMG", ext)
val file = File(getAppFilePath(fileToSave))
val output = FileOutputStream(file)
uri.inputStream().use { input ->
output.use { output ->
input?.copyTo(output)
}
}
CryptoFile.plain(fileToSave)
} catch (e: Exception) {
Log.e(TAG, "Util.kt saveAnimImage error: ${e.message}")
null
@@ -155,40 +150,22 @@ fun saveFileFromUri(uri: URI, encrypted: Boolean): CryptoFile? {
return try {
val inputStream = uri.inputStream()
val fileToSave = getFileName(uri)
return if (inputStream != null && fileToSave != null) {
// TODO encrypt file if "encrypted" is true
if (inputStream != null && fileToSave != null) {
val destFileName = uniqueCombine(fileToSave)
val destFile = File(getAppFilePath(destFileName))
if (encrypted) {
createTmpFileAndDelete { tmpFile ->
Files.copy(inputStream, tmpFile.toPath())
val args = encryptCryptoFile(tmpFile.absolutePath, destFile.absolutePath)
CryptoFile(destFileName, args)
}
} else {
Files.copy(inputStream, destFile.toPath())
CryptoFile.plain(destFileName)
}
Files.copy(inputStream, destFile.toPath())
CryptoFile.plain(destFileName)
} else {
Log.e(TAG, "Util.kt saveFileFromUri null inputStream")
null
}
} catch (e: Exception) {
Log.e(TAG, "Util.kt saveFileFromUri error: ${e.stackTraceToString()}")
Log.e(TAG, "Util.kt saveFileFromUri error: ${e.message}")
null
}
}
fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T {
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
tmpFile.deleteOnExit()
ChatModel.filesToDelete.add(tmpFile)
try {
return onCreated(tmpFile)
} finally {
tmpFile.delete()
}
}
fun generateNewFileName(prefix: String, ext: String): String {
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
sdf.timeZone = TimeZone.getTimeZone("GMT")
@@ -289,17 +266,6 @@ fun blendARGB(
return Color(r, g, b, a)
}
fun InputStream.toByteArray(): ByteArray =
ByteArrayOutputStream().use { output ->
val b = ByteArray(4096)
var n = read(b)
while (n != -1) {
output.write(b, 0, n);
n = read(b)
}
return output.toByteArray()
}
expect fun ByteArray.toBase64StringForPassphrase(): String
// Android's default implementation that was used before multiplatform, adds non-needed characters at the end of string

View File

@@ -12,7 +12,6 @@ import androidx.compose.ui.unit.dp
import dev.icerock.moko.resources.compose.stringResource
import boofcv.alg.drawing.FiducialImageEngine
import boofcv.alg.fiducial.qrcode.*
import chat.simplex.common.model.CryptoFile
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.SimpleXTheme
import chat.simplex.common.views.helpers.*
@@ -46,7 +45,7 @@ fun QRCode(
.let { if (withLogo) it.addLogo() else it }
val file = saveTempImageUncompressed(image, false)
if (file != null) {
shareFile("", CryptoFile.plain(file.absolutePath))
shareFile("", file.absolutePath)
}
}
}

View File

@@ -180,13 +180,7 @@ fun NetworkAndServersView(
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
}
if (networkUseSocksProxy.value) {
SectionCustomFooter {
Column {
Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
}
}
SectionCustomFooter { Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported)) }
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
} else {
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))

View File

@@ -64,7 +64,6 @@ fun PrivacySettingsView(
SectionDividerSpaced()
SectionView(stringResource(MR.strings.settings_section_title_chats)) {
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles)
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
SettingsPreferenceItem(

View File

@@ -164,9 +164,10 @@ private fun UserProfilesLayout(
) {
if (profileHidden.value) {
SectionView {
SettingsActionItem(painterResource(MR.images.ic_lock_open_right), stringResource(MR.strings.enter_password_to_show), click = {
SettingsActionItem(painterResource(MR.images.ic_lock_open), stringResource(MR.strings.enter_password_to_show), click = {
profileHidden.value = false
})
}
)
}
SectionSpacer()
}
@@ -222,7 +223,7 @@ private fun UserView(
Box(Modifier.padding(horizontal = DEFAULT_PADDING)) {
DefaultDropdownMenu(showMenu) {
if (user.hidden) {
ItemAction(stringResource(MR.strings.user_unhide), painterResource(MR.images.ic_lock_open_right), onClick = {
ItemAction(stringResource(MR.strings.user_unhide), painterResource(MR.images.ic_lock_open), onClick = {
showMenu.value = false
unhideUser(user)
})

View File

@@ -615,7 +615,7 @@
<string name="network_use_onion_hosts_required">Required</string>
<string name="network_use_onion_hosts_prefer_desc">Onion hosts will be used when available.</string>
<string name="network_use_onion_hosts_no_desc">Onion hosts will not be used.</string>
<string name="network_use_onion_hosts_required_desc">Onion hosts will be required for connection.\nPlease note: you will not be able to connect to the servers without .onion address.</string>
<string name="network_use_onion_hosts_required_desc">Onion hosts will be required for connection.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts will be used when available.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts will not be used.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts will be required for connection.</string>
@@ -626,7 +626,6 @@
<string name="network_session_mode_entity_description"><![CDATA[A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.]]></string>
<string name="update_network_session_mode_question">Update transport isolation mode?</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.]]></string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]></string>
<string name="appearance_settings">Appearance</string>
<string name="customize_theme_title">Customize theme</string>
<string name="theme_colors_section_title">THEME COLORS</string>
@@ -856,7 +855,6 @@
<string name="privacy_and_security">Privacy &amp; security</string>
<string name="your_privacy">Your privacy</string>
<string name="protect_app_screen">Protect app screen</string>
<string name="encrypt_local_files">Encrypt local files</string>
<string name="auto_accept_images">Auto-accept images</string>
<string name="send_link_previews">Send link previews</string>
<string name="privacy_show_last_messages">Show last messages</string>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M222 971q-23.719 0-40.609-16.891Q164.5 937.219 164.5 913.5v-431q0-23.719 16.891-40.609Q198.281 425 222 425h387v-95.385q0-53.782-37.373-91.198Q534.254 201 479.863 201q-46.363 0-81.363 28T354 300.5q-3 13-11.75 21.25T321.983 330q-12.311 0-20.397-8.5-8.086-8.5-6.086-20 10-68 61.902-113t122.629-45q77.383 0 131.926 54.551Q666.5 252.603 666.5 330v95H738q23.719 0 40.609 16.891Q795.5 458.781 795.5 482.5v431q0 23.719-16.891 40.609Q761.719 971 738 971H222Zm0-57.5h516v-431H222v431Zm258.084-140q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM222 482.5v431-431Z"/></svg>

After

Width:  |  Height:  |  Size: 804 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M222-142.5h516v-431H222v431Zm258.084-140q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM222-142.5v-431 431Zm0 57.5q-23.719 0-40.609-16.891Q164.5-118.781 164.5-142.5v-431q0-23.719 16.891-40.609Q198.281-631 222-631h329.5v-95.018q0-77.832 54.349-132.157Q660.198-912.5 738-912.5q70 0 121.25 44T922-759q2 11.5-6.638 22.25T895.75-726q-12.66 0-20.705-6-8.045-6-9.545-18.5-9-44.5-44.55-74.5T738-855q-54.333 0-91.667 37.333Q609-780.333 609-726.231V-631h129q23.719 0 40.609 16.891Q795.5-597.219 795.5-573.5v431q0 23.719-16.891 40.609Q761.719-85 738-85H222Z"/></svg>

Before

Width:  |  Height:  |  Size: 800 B

View File

@@ -25,8 +25,6 @@ fun initApp() {
initChatController()
runMigrations()
}
// LALAL
//testCrypto()
}
private fun applyAppLocale() {

View File

@@ -1,7 +1,7 @@
package chat.simplex.common.platform
import androidx.compose.runtime.MutableState
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatItem
import chat.simplex.common.views.usersettings.showInDevelopingAlert
import kotlinx.coroutines.CoroutineScope
@@ -18,7 +18,7 @@ actual class RecorderNative: RecorderInterface {
}
actual object AudioPlayer: AudioPlayerInterface {
override fun play(fileSource: CryptoFile, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean) {
override fun play(filePath: String?, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean) {
showInDevelopingAlert()
}

View File

@@ -3,8 +3,6 @@ package chat.simplex.common.platform
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.text.AnnotatedString
import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.getAppFileUri
import chat.simplex.common.views.helpers.withApi
import java.io.File
import java.net.URI
@@ -22,16 +20,12 @@ actual fun ClipboardManager.shareText(text: String) {
showToast(MR.strings.copied.localized())
}
actual fun shareFile(text: String, fileSource: CryptoFile) {
actual fun shareFile(text: String, filePath: String) {
withApi {
FileChooserLauncher(false) { to: URI? ->
if (to != null) {
if (fileSource.cryptoArgs != null) {
decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, to.path)
} else {
copyFileToFile(File(fileSource.filePath), to) {}
}
copyFileToFile(File(filePath), to) {}
}
}.launch(fileSource.filePath)
}.launch(filePath)
}
}

View File

@@ -11,7 +11,7 @@ import java.net.URI
@Composable
actual fun SimpleAndAnimatedImageView(
data: ByteArray,
uri: URI,
imageBitmap: ImageBitmap,
file: CIFile?,
imageProvider: () -> ImageGalleryProvider,

View File

@@ -31,7 +31,7 @@ actual fun ReactionIcon(text: String, fontSize: TextUnit) {
@Composable
actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) {
ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = {
ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = {
when (cItem.content.msgContent) {
is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") }
else -> {}

View File

@@ -4,16 +4,19 @@ import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale
import chat.simplex.common.platform.VideoPlayer
import chat.simplex.common.views.helpers.getBitmapFromByteArray
import chat.simplex.common.views.helpers.getBitmapFromUri
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import java.net.URI
@Composable
actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) {
actual fun FullScreenImageView(modifier: Modifier, uri: URI, imageBitmap: ImageBitmap) {
Image(
getBitmapFromByteArray(data, false) ?: MR.images.decentralized.image.toComposeImageBitmap(),
getBitmapFromUri(uri, false) ?: MR.images.decentralized.image.toComposeImageBitmap(),
contentDescription = stringResource(MR.strings.image_descr),
contentScale = ContentScale.Fit,
modifier = modifier,

View File

@@ -6,10 +6,8 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Density
import chat.simplex.common.model.CIFile
import chat.simplex.common.model.readCryptoFile
import chat.simplex.common.platform.*
import chat.simplex.common.simplexWindowState
import java.io.ByteArrayInputStream
import java.io.File
import java.net.URI
import javax.imageio.ImageIO
@@ -90,12 +88,11 @@ actual fun escapedHtmlToAnnotatedString(text: String, density: Density): Annotat
actual fun getAppFileUri(fileName: String): URI =
URI("file:" + appFilesDir.absolutePath + File.separator + fileName)
actual fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? {
actual fun getLoadedImage(file: CIFile?): ImageBitmap? {
val filePath = getLoadedFilePath(file)
return if (filePath != null) {
val data = if (file?.fileSource?.cryptoArgs != null) readCryptoFile(filePath, file.fileSource.cryptoArgs) else File(filePath).readBytes()
val bitmap = getBitmapFromByteArray(data, false)
if (bitmap != null) bitmap to data else null
val uri = getAppFileUri(filePath.substringAfterLast(File.separator))
getBitmapFromUri(uri, false)
} else {
null
}
@@ -110,9 +107,6 @@ actual fun getFileSize(uri: URI): Long? = uri.toPath().toFile().length()
actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? =
ImageIO.read(uri.inputStream()).toComposeImageBitmap()
actual fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap? =
ImageIO.read(ByteArrayInputStream(data)).toComposeImageBitmap()
// LALAL implement to support animated drawable
actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? = null

View File

@@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
android.version_name=5.3-beta.7
android.version_code=149
android.version_name=5.3-beta.6
android.version_code=148
desktop.version_name=1.5.0
desktop.version_code=7
desktop.version_name=1.4.0
desktop.version_code=6
kotlin.version=1.8.20
gradle.plugin.version=7.4.2

View File

@@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 0cabe0690beee90f460ad7bada72294222e7e109
tag: 351f42650c57f310fc1ea858ff9b7178823f1fd4
source-repository-package
type: git

View File

@@ -1,5 +1,5 @@
name: simplex-chat
version: 5.3.0.7
version: 5.3.0.6
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme

View File

@@ -2,7 +2,7 @@
set -e
langs=( en cs de es fi fr it ja nl pl ru uk zh-Hans )
langs=( en cs de es fr it ja nl pl ru zh-Hans )
for lang in "${langs[@]}"; do
echo "***"

View File

@@ -2,7 +2,7 @@
set -e
langs=( en cs de es fi fr it ja nl pl ru th uk zh-Hans )
langs=( en cs de es fr it ja nl pl ru th zh-Hans )
for lang in "${langs[@]}"; do
echo "***"

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