mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-04-27 15:06:12 +00:00
Merge branch 'master' into master-android
This commit is contained in:
@@ -357,6 +357,12 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws -
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiPlanForwardChatItems(type: ChatType, id: Int64, itemIds: [Int64]) async throws -> ([Int64], ForwardConfirmation?) {
|
||||
let r = await chatSendCmd(.apiPlanForwardChatItems(toChatType: type, toChatId: id, itemIds: itemIds))
|
||||
if case let .forwardPlan(_, chatItemIds, forwardConfimation) = r { return (chatItemIds, forwardConfimation) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?) async -> [ChatItem]? {
|
||||
let cmd: ChatCommand = .apiForwardChatItems(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemIds: itemIds, ttl: ttl)
|
||||
return await processSendMessageCmd(toChatType: toChatType, cmd: cmd)
|
||||
@@ -1039,77 +1045,122 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF
|
||||
}
|
||||
|
||||
func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async {
|
||||
if let chatItem = await apiReceiveFile(
|
||||
fileId: fileId,
|
||||
userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
|
||||
encrypted: privacyEncryptLocalFilesGroupDefault.get(),
|
||||
await receiveFiles(
|
||||
user: user,
|
||||
fileIds: [fileId],
|
||||
userApprovedRelays: userApprovedRelays,
|
||||
auto: auto
|
||||
) {
|
||||
await chatItemSimpleUpdate(user, chatItem)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
|
||||
let am = AlertManager.shared
|
||||
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
||||
if case .rcvFileAcceptedSndCancelled = r {
|
||||
logger.debug("apiReceiveFile error: sender cancelled file transfer")
|
||||
if !auto {
|
||||
am.showAlertMsg(
|
||||
title: "Cannot receive file",
|
||||
message: "Sender cancelled file transfer."
|
||||
func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool = false, auto: Bool = false) async {
|
||||
var fileIdsToApprove = [Int64]()
|
||||
var srvsToApprove = Set<String>()
|
||||
var otherFileErrs = [ChatResponse]()
|
||||
|
||||
for fileId in fileIds {
|
||||
let r = await chatSendCmd(
|
||||
.receiveFile(
|
||||
fileId: fileId,
|
||||
userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
|
||||
encrypted: privacyEncryptLocalFilesGroupDefault.get(),
|
||||
inline: nil
|
||||
)
|
||||
)
|
||||
switch r {
|
||||
case let .rcvFileAccepted(_, chatItem):
|
||||
await chatItemSimpleUpdate(user, chatItem)
|
||||
default:
|
||||
if let chatError = chatError(r) {
|
||||
switch chatError {
|
||||
case let .fileNotApproved(fileId, unknownServers):
|
||||
fileIdsToApprove.append(fileId)
|
||||
srvsToApprove.formUnion(unknownServers)
|
||||
default:
|
||||
otherFileErrs.append(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let networkErrorAlert = networkErrorAlert(r) {
|
||||
logger.error("apiReceiveFile network error: \(String(describing: r))")
|
||||
if !auto {
|
||||
am.showAlert(networkErrorAlert)
|
||||
}
|
||||
|
||||
if !auto {
|
||||
let otherErrsStr = if otherFileErrs.isEmpty {
|
||||
""
|
||||
} else if otherFileErrs.count == 1 {
|
||||
"\(otherFileErrs[0])"
|
||||
} else if otherFileErrs.count == 2 {
|
||||
"\(otherFileErrs[0])\n\(otherFileErrs[1])"
|
||||
} else {
|
||||
"\(otherFileErrs[0])\n\(otherFileErrs[1])\nand \(otherFileErrs.count - 2) other error(s)"
|
||||
}
|
||||
} else {
|
||||
switch chatError(r) {
|
||||
case .fileCancelled:
|
||||
logger.debug("apiReceiveFile ignoring fileCancelled error")
|
||||
case .fileAlreadyReceiving:
|
||||
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
|
||||
case let .fileNotApproved(fileId, unknownServers):
|
||||
logger.debug("apiReceiveFile fileNotApproved error")
|
||||
if !auto {
|
||||
let srvs = unknownServers.map { s in
|
||||
|
||||
// If there are not approved files, alert is shown the same way both in case of singular and plural files reception
|
||||
if !fileIdsToApprove.isEmpty {
|
||||
let srvs = srvsToApprove
|
||||
.map { s in
|
||||
if let srv = parseServerAddress(s), !srv.hostnames.isEmpty {
|
||||
srv.hostnames[0]
|
||||
} else {
|
||||
serverHost(s)
|
||||
}
|
||||
}
|
||||
am.showAlert(Alert(
|
||||
title: Text("Unknown servers!"),
|
||||
message: Text("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs.sorted().joined(separator: ", "))."),
|
||||
primaryButton: .default(
|
||||
Text("Download"),
|
||||
action: {
|
||||
Task {
|
||||
logger.debug("apiReceiveFile fileNotApproved alert - in Task")
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
await receiveFile(user: user, fileId: fileId, userApprovedRelays: true)
|
||||
}
|
||||
.sorted()
|
||||
.joined(separator: ", ")
|
||||
let fIds = fileIdsToApprove
|
||||
await MainActor.run {
|
||||
showAlert(
|
||||
title: NSLocalizedString("Unknown servers!", comment: "alert title"),
|
||||
message: (
|
||||
NSLocalizedString("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs).", comment: "alert message") +
|
||||
(otherErrsStr != "" ? "\n\n" + NSLocalizedString("Other file errors:\n\(otherErrsStr)", comment: "alert message") : "")
|
||||
),
|
||||
buttonTitle: NSLocalizedString("Download", comment: "alert button"),
|
||||
buttonAction: {
|
||||
Task {
|
||||
logger.debug("apiReceiveFile fileNotApproved alert - in Task")
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
await receiveFiles(user: user, fileIds: fIds, userApprovedRelays: true)
|
||||
}
|
||||
}
|
||||
),
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
},
|
||||
cancelButton: true
|
||||
)
|
||||
}
|
||||
default:
|
||||
logger.error("apiReceiveFile error: \(String(describing: r))")
|
||||
if !auto {
|
||||
am.showAlertMsg(
|
||||
title: "Error receiving file",
|
||||
message: "Error: \(responseError(r))"
|
||||
} else if otherFileErrs.count == 1 { // If there is a single other error, we differentiate on it
|
||||
let errorResponse = otherFileErrs.first!
|
||||
switch errorResponse {
|
||||
case let .rcvFileAcceptedSndCancelled(_, rcvFileTransfer):
|
||||
logger.debug("receiveFiles error: sender cancelled file transfer \(rcvFileTransfer.fileId)")
|
||||
await MainActor.run {
|
||||
showAlert(
|
||||
NSLocalizedString("Cannot receive file", comment: "alert title"),
|
||||
message: NSLocalizedString("Sender cancelled file transfer.", comment: "alert message")
|
||||
)
|
||||
}
|
||||
default:
|
||||
if let chatError = chatError(errorResponse) {
|
||||
switch chatError {
|
||||
case .fileCancelled, .fileAlreadyReceiving:
|
||||
logger.debug("receiveFiles ignoring FileCancelled or FileAlreadyReceiving error")
|
||||
default:
|
||||
await MainActor.run {
|
||||
showAlert(
|
||||
NSLocalizedString("Error receiving file", comment: "alert title"),
|
||||
message: responseError(errorResponse)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if otherFileErrs.count > 1 { // If there are multiple other errors, we show general alert
|
||||
await MainActor.run {
|
||||
showAlert(
|
||||
NSLocalizedString("Error receiving file", comment: "alert title"),
|
||||
message: NSLocalizedString("File errors:\n\(otherErrsStr)", comment: "alert message")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cancelFile(user: User, fileId: Int64) async {
|
||||
|
||||
@@ -14,7 +14,7 @@ struct ChatItemForwardingView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var ci: ChatItem
|
||||
var chatItems: [ChatItem]
|
||||
var fromChatInfo: ChatInfo
|
||||
@Binding var composeState: ComposeState
|
||||
|
||||
@@ -73,11 +73,14 @@ struct ChatItemForwardingView: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View {
|
||||
let prohibited = chat.prohibitedByPref(
|
||||
hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text),
|
||||
isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false,
|
||||
isVoice: ci.content.msgContent?.isVoice ?? false
|
||||
)
|
||||
let prohibited = chatItems.map { ci in
|
||||
chat.prohibitedByPref(
|
||||
hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text),
|
||||
isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false,
|
||||
isVoice: ci.content.msgContent?.isVoice ?? false
|
||||
)
|
||||
}.contains(true)
|
||||
|
||||
Button {
|
||||
if prohibited {
|
||||
alert = SomeAlert(
|
||||
@@ -93,10 +96,10 @@ struct ChatItemForwardingView: View {
|
||||
composeState = ComposeState(
|
||||
message: composeState.message,
|
||||
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
|
||||
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
|
||||
contextItem: .forwardingItems(chatItems: chatItems, fromChatInfo: fromChatInfo)
|
||||
)
|
||||
} else {
|
||||
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
|
||||
composeState = ComposeState.init(forwardingItems: chatItems, fromChatInfo: fromChatInfo)
|
||||
ItemsModel.shared.loadOpenChat(chat.id)
|
||||
}
|
||||
}
|
||||
@@ -123,7 +126,7 @@ struct ChatItemForwardingView: View {
|
||||
|
||||
#Preview {
|
||||
ChatItemForwardingView(
|
||||
ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
|
||||
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")],
|
||||
fromChatInfo: .direct(contact: Contact.sampleData),
|
||||
composeState: Binding.constant(ComposeState(message: "hello"))
|
||||
).environmentObject(CurrentColors.toAppTheme())
|
||||
|
||||
@@ -42,6 +42,7 @@ struct ChatView: View {
|
||||
@State private var showGroupLinkSheet: Bool = false
|
||||
@State private var groupLink: String?
|
||||
@State private var groupLinkMemberRole: GroupMemberRole = .member
|
||||
@State private var forwardedChatItems: [ChatItem] = []
|
||||
@State private var selectedChatItems: Set<Int64>? = nil
|
||||
@State private var showDeleteSelectedMessages: Bool = false
|
||||
@State private var allowToDeleteSelectedMessagesForAll: Bool = false
|
||||
@@ -98,7 +99,8 @@ struct ChatView: View {
|
||||
if case let .group(groupInfo) = chat.chatInfo {
|
||||
showModerateSelectedMessagesAlert(groupInfo)
|
||||
}
|
||||
}
|
||||
},
|
||||
forwardItems: forwardSelectedMessages
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -135,6 +137,22 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { !forwardedChatItems.isEmpty },
|
||||
set: { isPresented in
|
||||
if !isPresented {
|
||||
forwardedChatItems = []
|
||||
selectedChatItems = nil
|
||||
}
|
||||
}
|
||||
)) {
|
||||
if #available(iOS 16.0, *) {
|
||||
ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState)
|
||||
.presentationDetents([.fraction(0.8)])
|
||||
} else {
|
||||
ChatItemForwardingView(chatItems: forwardedChatItems, fromChatInfo: chat.chatInfo, composeState: $composeState)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
selectedChatItems = nil
|
||||
initChatView()
|
||||
@@ -411,7 +429,8 @@ struct ChatView: View {
|
||||
composeState: $composeState,
|
||||
selectedMember: $selectedMember,
|
||||
revealedChatItem: $revealedChatItem,
|
||||
selectedChatItems: $selectedChatItems
|
||||
selectedChatItems: $selectedChatItems,
|
||||
forwardedChatItems: $forwardedChatItems
|
||||
)
|
||||
.id(ci.id) // Required to trigger `onAppear` on iOS15
|
||||
} loadPage: {
|
||||
@@ -701,6 +720,116 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func forwardSelectedMessages() {
|
||||
Task {
|
||||
do {
|
||||
if let selectedChatItems {
|
||||
let (validItems, confirmation) = try await apiPlanForwardChatItems(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
itemIds: Array(selectedChatItems)
|
||||
)
|
||||
if let confirmation {
|
||||
if validItems.count > 0 {
|
||||
showAlert(
|
||||
String.localizedStringWithFormat(
|
||||
NSLocalizedString("Forward %d message(s)?", comment: "alert title"),
|
||||
validItems.count
|
||||
),
|
||||
message: forwardConfirmationText(confirmation) + "\n" +
|
||||
NSLocalizedString("Forward messages without files?", comment: "alert message")
|
||||
) {
|
||||
switch confirmation {
|
||||
case let .filesNotAccepted(fileIds):
|
||||
[forwardAction(validItems), downloadAction(fileIds), cancelAlertAction]
|
||||
default:
|
||||
[forwardAction(validItems), cancelAlertAction]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showAlert(
|
||||
NSLocalizedString("Nothing to forward!", comment: "alert title"),
|
||||
message: forwardConfirmationText(confirmation)
|
||||
) {
|
||||
switch confirmation {
|
||||
case let .filesNotAccepted(fileIds):
|
||||
[downloadAction(fileIds), cancelAlertAction]
|
||||
default:
|
||||
[okAlertAction]
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await openForwardingSheet(validItems)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("Plan forward chat items failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func forwardConfirmationText(_ fc: ForwardConfirmation) -> String {
|
||||
switch fc {
|
||||
case let .filesNotAccepted(fileIds):
|
||||
String.localizedStringWithFormat(
|
||||
NSLocalizedString("%d file(s) were not downloaded.", comment: "forward confirmation reason"),
|
||||
fileIds.count
|
||||
)
|
||||
case let .filesInProgress(filesCount):
|
||||
String.localizedStringWithFormat(
|
||||
NSLocalizedString("%d file(s) are still being downloaded.", comment: "forward confirmation reason"),
|
||||
filesCount
|
||||
)
|
||||
case let .filesMissing(filesCount):
|
||||
String.localizedStringWithFormat(
|
||||
NSLocalizedString("%d file(s) were deleted.", comment: "forward confirmation reason"),
|
||||
filesCount
|
||||
)
|
||||
case let .filesFailed(filesCount):
|
||||
String.localizedStringWithFormat(
|
||||
NSLocalizedString("%d file(s) failed to download.", comment: "forward confirmation reason"),
|
||||
filesCount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func forwardAction(_ items: [Int64]) -> UIAlertAction {
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Forward messages", comment: "alert action"),
|
||||
style: .default,
|
||||
handler: { _ in Task { await openForwardingSheet(items) } }
|
||||
)
|
||||
}
|
||||
|
||||
func downloadAction(_ fileIds: [Int64]) -> UIAlertAction {
|
||||
UIAlertAction(
|
||||
title: NSLocalizedString("Download files", comment: "alert action"),
|
||||
style: .default,
|
||||
handler: { _ in
|
||||
Task {
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
await receiveFiles(user: user, fileIds: fileIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func openForwardingSheet(_ items: [Int64]) async {
|
||||
let im = ItemsModel.shared
|
||||
var items = Set(items)
|
||||
var fci = [ChatItem]()
|
||||
for reversedChatItem in im.reversedChatItems {
|
||||
if items.contains(reversedChatItem.id) {
|
||||
items.remove(reversedChatItem.id)
|
||||
fci.insert(reversedChatItem, at: 0)
|
||||
}
|
||||
if items.isEmpty { break }
|
||||
}
|
||||
await MainActor.run { forwardedChatItems = fci }
|
||||
}
|
||||
}
|
||||
|
||||
private func loadChatItems(_ cInfo: ChatInfo) {
|
||||
Task {
|
||||
if loadingItems || firstPage { return }
|
||||
@@ -762,10 +891,10 @@ struct ChatView: View {
|
||||
@State private var showDeleteMessages = false
|
||||
@State private var showChatItemInfoSheet: Bool = false
|
||||
@State private var chatItemInfo: ChatItemInfo?
|
||||
@State private var showForwardingSheet: Bool = false
|
||||
@State private var msgWidth: CGFloat = 0
|
||||
|
||||
@Binding var selectedChatItems: Set<Int64>?
|
||||
@Binding var forwardedChatItems: [ChatItem]
|
||||
|
||||
@State private var allowMenu: Bool = true
|
||||
@State private var markedRead = false
|
||||
@@ -1079,14 +1208,6 @@ struct ChatView: View {
|
||||
}) {
|
||||
ChatItemInfoView(ci: ci, chatItemInfo: $chatItemInfo)
|
||||
}
|
||||
.sheet(isPresented: $showForwardingSheet) {
|
||||
if #available(iOS 16.0, *) {
|
||||
ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState)
|
||||
.presentationDetents([.fraction(0.8)])
|
||||
} else {
|
||||
ChatItemForwardingView(ci: ci, fromChatInfo: chat.chatInfo, composeState: $composeState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool {
|
||||
@@ -1227,7 +1348,7 @@ struct ChatView: View {
|
||||
|
||||
var forwardButton: Button<some View> {
|
||||
Button {
|
||||
showForwardingSheet = true
|
||||
forwardedChatItems = [chatItem]
|
||||
} label: {
|
||||
Label(
|
||||
NSLocalizedString("Forward", comment: "chat item action"),
|
||||
|
||||
@@ -23,7 +23,7 @@ enum ComposeContextItem {
|
||||
case noContextItem
|
||||
case quotedItem(chatItem: ChatItem)
|
||||
case editingItem(chatItem: ChatItem)
|
||||
case forwardingItem(chatItem: ChatItem, fromChatInfo: ChatInfo)
|
||||
case forwardingItems(chatItems: [ChatItem], fromChatInfo: ChatInfo)
|
||||
}
|
||||
|
||||
enum VoiceMessageRecordingState {
|
||||
@@ -73,10 +73,10 @@ struct ComposeState {
|
||||
}
|
||||
}
|
||||
|
||||
init(forwardingItem: ChatItem, fromChatInfo: ChatInfo) {
|
||||
init(forwardingItems: [ChatItem], fromChatInfo: ChatInfo) {
|
||||
self.message = ""
|
||||
self.preview = .noPreview
|
||||
self.contextItem = .forwardingItem(chatItem: forwardingItem, fromChatInfo: fromChatInfo)
|
||||
self.contextItem = .forwardingItems(chatItems: forwardingItems, fromChatInfo: fromChatInfo)
|
||||
self.voiceMessageRecordingState = .noRecording
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ struct ComposeState {
|
||||
|
||||
var forwarding: Bool {
|
||||
switch contextItem {
|
||||
case .forwardingItem: return true
|
||||
case .forwardingItems: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,13 @@ struct ComposeState {
|
||||
}
|
||||
}
|
||||
|
||||
var manyMediaPreviews: Bool {
|
||||
switch preview {
|
||||
case let .mediaPreviews(mediaPreviews): return mediaPreviews.count > 1
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var attachmentDisabled: Bool {
|
||||
if editing || forwarding || liveMessage != nil || inProgress { return true }
|
||||
switch preview {
|
||||
@@ -687,7 +694,7 @@ struct ComposeView: View {
|
||||
case let .quotedItem(chatItem: quotedItem):
|
||||
ContextItemView(
|
||||
chat: chat,
|
||||
contextItem: quotedItem,
|
||||
contextItems: [quotedItem],
|
||||
contextIcon: "arrowshape.turn.up.left",
|
||||
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
|
||||
)
|
||||
@@ -695,18 +702,17 @@ struct ComposeView: View {
|
||||
case let .editingItem(chatItem: editingItem):
|
||||
ContextItemView(
|
||||
chat: chat,
|
||||
contextItem: editingItem,
|
||||
contextItems: [editingItem],
|
||||
contextIcon: "pencil",
|
||||
cancelContextItem: { clearState() }
|
||||
)
|
||||
Divider()
|
||||
case let .forwardingItem(chatItem: forwardedItem, _):
|
||||
case let .forwardingItems(chatItems, _):
|
||||
ContextItemView(
|
||||
chat: chat,
|
||||
contextItem: forwardedItem,
|
||||
contextItems: chatItems,
|
||||
contextIcon: "arrowshape.turn.up.forward",
|
||||
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) },
|
||||
showSender: false
|
||||
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
|
||||
)
|
||||
Divider()
|
||||
}
|
||||
@@ -730,10 +736,11 @@ struct ComposeView: View {
|
||||
}
|
||||
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
||||
await sendMemberContactInvitation()
|
||||
} else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem {
|
||||
sent = await forwardItem(ci, fromChatInfo, ttl)
|
||||
} else if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem {
|
||||
// Composed text is send as a reply to the last forwarded item
|
||||
sent = await forwardItems(chatItems, fromChatInfo, ttl).last
|
||||
if !composeState.message.isEmpty {
|
||||
sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
|
||||
_ = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl)
|
||||
}
|
||||
} else if case let .editingItem(ci) = composeState.contextItem {
|
||||
sent = await updateMessage(ci, live: live)
|
||||
@@ -750,27 +757,28 @@ struct ComposeView: View {
|
||||
sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl)
|
||||
case .linkPreview:
|
||||
sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl)
|
||||
case let .mediaPreviews(mediaPreviews: media):
|
||||
// TODO batch send: batch media previews
|
||||
case let .mediaPreviews(media):
|
||||
let last = media.count - 1
|
||||
var msgs: [ComposedMessage] = []
|
||||
if last >= 0 {
|
||||
for i in 0..<last {
|
||||
if case (_, .video(_, _, _)) = media[i] {
|
||||
sent = await sendVideo(media[i], ttl: ttl)
|
||||
} else {
|
||||
sent = await sendImage(media[i], ttl: ttl)
|
||||
if i > 0 {
|
||||
// Sleep to allow `progressByTimeout` update be rendered
|
||||
try? await Task.sleep(nanoseconds: 100_000000)
|
||||
}
|
||||
if let (fileSource, msgContent) = mediaContent(media[i], text: "") {
|
||||
msgs.append(ComposedMessage(fileSource: fileSource, msgContent: msgContent))
|
||||
}
|
||||
_ = try? await Task.sleep(nanoseconds: 100_000000)
|
||||
}
|
||||
if case (_, .video(_, _, _)) = media[last] {
|
||||
sent = await sendVideo(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl)
|
||||
} else {
|
||||
sent = await sendImage(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl)
|
||||
if let (fileSource, msgContent) = mediaContent(media[last], text: msgText) {
|
||||
msgs.append(ComposedMessage(fileSource: fileSource, quotedItemId: quoted, msgContent: msgContent))
|
||||
}
|
||||
}
|
||||
if sent == nil {
|
||||
sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl)
|
||||
if msgs.isEmpty {
|
||||
msgs = [ComposedMessage(quotedItemId: quoted, msgContent: .text(msgText))]
|
||||
}
|
||||
sent = await send(msgs, live: live, ttl: ttl).last
|
||||
|
||||
case let .voicePreview(recordingFileName, duration):
|
||||
stopPlayback.toggle()
|
||||
let file = voiceCryptoFile(recordingFileName)
|
||||
@@ -792,6 +800,20 @@ struct ComposeView: View {
|
||||
}
|
||||
return sent
|
||||
|
||||
func mediaContent(_ media: (String, UploadContent?), text: String) -> (CryptoFile?, MsgContent)? {
|
||||
let (previewImage, uploadContent) = media
|
||||
return switch uploadContent {
|
||||
case let .simpleImage(image):
|
||||
(saveImage(image), .image(text: text, image: previewImage))
|
||||
case let .animatedImage(image):
|
||||
(saveAnimImage(image), .image(text: text, image: previewImage))
|
||||
case let .video(_, url, duration):
|
||||
(moveTempFileFromURL(url), .video(text: text, image: previewImage, duration: duration))
|
||||
case .none:
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
func sending() async {
|
||||
await MainActor.run { composeState.inProgress = true }
|
||||
}
|
||||
@@ -855,23 +877,6 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func sendImage(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
|
||||
let (image, data) = imageData
|
||||
if let data = data, let savedFile = saveAnyImage(data) {
|
||||
return await send(.image(text: text, image: image), quoted: quoted, file: savedFile, live: live, ttl: ttl)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
|
||||
let (image, data) = imageData
|
||||
if case let .video(_, url, duration) = data, let savedFile = moveTempFileFromURL(url) {
|
||||
ChatModel.shared.filesToDelete.remove(url)
|
||||
return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live, ttl: ttl)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func voiceCryptoFile(_ fileName: String) -> CryptoFile? {
|
||||
if !privacyEncryptLocalFilesGroupDefault.get() {
|
||||
return CryptoFile.plain(fileName)
|
||||
@@ -888,17 +893,22 @@ struct ComposeView: View {
|
||||
}
|
||||
|
||||
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
|
||||
await send(
|
||||
[ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc)],
|
||||
live: live,
|
||||
ttl: ttl
|
||||
).first
|
||||
}
|
||||
|
||||
func send(_ msgs: [ComposedMessage], live: Bool, ttl: Int?) async -> [ChatItem] {
|
||||
if let chatItems = chat.chatInfo.chatType == .local
|
||||
? await apiCreateChatItems(
|
||||
noteFolderId: chat.chatInfo.apiId,
|
||||
composedMessages: [ComposedMessage(fileSource: file, msgContent: mc)]
|
||||
)
|
||||
? await apiCreateChatItems(noteFolderId: chat.chatInfo.apiId, composedMessages: msgs)
|
||||
: await apiSendMessages(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
live: live,
|
||||
ttl: ttl,
|
||||
composedMessages: [ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc)]
|
||||
composedMessages: msgs
|
||||
) {
|
||||
await MainActor.run {
|
||||
chatModel.removeLiveDummy(animated: false)
|
||||
@@ -906,33 +916,43 @@ struct ComposeView: View {
|
||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||
}
|
||||
}
|
||||
// UI only supports sending one item at a time
|
||||
return chatItems.first
|
||||
return chatItems
|
||||
}
|
||||
if let file = file {
|
||||
removeFile(file.filePath)
|
||||
for msg in msgs {
|
||||
if let file = msg.fileSource {
|
||||
removeFile(file.filePath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return []
|
||||
}
|
||||
|
||||
func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> ChatItem? {
|
||||
func forwardItems(_ forwardedItems: [ChatItem], _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> [ChatItem] {
|
||||
if let chatItems = await apiForwardChatItems(
|
||||
toChatType: chat.chatInfo.chatType,
|
||||
toChatId: chat.chatInfo.apiId,
|
||||
fromChatType: fromChatInfo.chatType,
|
||||
fromChatId: fromChatInfo.apiId,
|
||||
itemIds: [forwardedItem.id],
|
||||
itemIds: forwardedItems.map { $0.id },
|
||||
ttl: ttl
|
||||
) {
|
||||
await MainActor.run {
|
||||
for chatItem in chatItems {
|
||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||
}
|
||||
if forwardedItems.count != chatItems.count {
|
||||
showAlert(
|
||||
String.localizedStringWithFormat(
|
||||
NSLocalizedString("%d messages not forwarded", comment: "alert title"),
|
||||
forwardedItems.count - chatItems.count
|
||||
),
|
||||
message: NSLocalizedString("Messages were deleted after you selected them.", comment: "alert message")
|
||||
)
|
||||
}
|
||||
}
|
||||
// TODO batch send: forward multiple messages
|
||||
return chatItems.first
|
||||
return chatItems
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkLinkPreview() -> MsgContent {
|
||||
@@ -949,14 +969,6 @@ struct ComposeView: View {
|
||||
return .text(msgText)
|
||||
}
|
||||
}
|
||||
|
||||
func saveAnyImage(_ img: UploadContent) -> CryptoFile? {
|
||||
switch img {
|
||||
case let .simpleImage(image): return saveImage(image)
|
||||
case let .animatedImage(image): return saveAnimImage(image)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startVoiceMessageRecording() async {
|
||||
|
||||
@@ -12,7 +12,7 @@ import SimpleXChat
|
||||
struct ContextItemView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
@ObservedObject var chat: Chat
|
||||
let contextItem: ChatItem
|
||||
let contextItems: [ChatItem]
|
||||
let contextIcon: String
|
||||
let cancelContextItem: () -> Void
|
||||
var showSender: Bool = true
|
||||
@@ -24,13 +24,22 @@ struct ContextItemView: View {
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 16, height: 16)
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
if showSender, let sender = contextItem.memberDisplayName {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(sender).font(.caption).foregroundColor(theme.colors.secondary)
|
||||
msgContentView(lines: 2)
|
||||
}
|
||||
if let singleItem = contextItems.first, contextItems.count == 1 {
|
||||
if showSender, let sender = singleItem.memberDisplayName {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(sender).font(.caption).foregroundColor(theme.colors.secondary)
|
||||
msgContentView(lines: 2, contextItem: singleItem)
|
||||
}
|
||||
} else {
|
||||
msgContentView(lines: 3, contextItem: singleItem)
|
||||
}
|
||||
} else {
|
||||
msgContentView(lines: 3)
|
||||
Text(
|
||||
chat.chatInfo.chatType == .local
|
||||
? "Saving \(contextItems.count) messages"
|
||||
: "Forwarding \(contextItems.count) messages"
|
||||
)
|
||||
.italic()
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
@@ -45,23 +54,32 @@ struct ContextItemView: View {
|
||||
.padding(12)
|
||||
.frame(minHeight: 54)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(chatItemFrameColor(contextItem, theme))
|
||||
.background(background)
|
||||
}
|
||||
|
||||
private func msgContentView(lines: Int) -> some View {
|
||||
contextMsgPreview()
|
||||
private var background: Color {
|
||||
contextItems.first
|
||||
.map { chatItemFrameColor($0, theme) }
|
||||
?? Color(uiColor: .tertiarySystemBackground)
|
||||
}
|
||||
|
||||
private func msgContentView(lines: Int, contextItem: ChatItem) -> some View {
|
||||
contextMsgPreview(contextItem)
|
||||
.multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading)
|
||||
.lineLimit(lines)
|
||||
}
|
||||
|
||||
private func contextMsgPreview() -> Text {
|
||||
private func contextMsgPreview(_ contextItem: ChatItem) -> Text {
|
||||
return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
|
||||
|
||||
func attachment() -> Text {
|
||||
let isFileLoaded = if let fileSource = getLoadedFileSource(contextItem.file) {
|
||||
FileManager.default.fileExists(atPath: getAppFilePath(fileSource.filePath).path)
|
||||
} else { false }
|
||||
switch contextItem.content.msgContent {
|
||||
case .file: return image("doc.fill")
|
||||
case .file: return isFileLoaded ? image("doc.fill") : Text("")
|
||||
case .image: return image("photo")
|
||||
case .voice: return image("play.fill")
|
||||
case .voice: return isFileLoaded ? image("play.fill") : Text("")
|
||||
default: return Text("")
|
||||
}
|
||||
}
|
||||
@@ -75,6 +93,6 @@ struct ContextItemView: View {
|
||||
struct ContextItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let contextItem: ChatItem = ChatItem.getSample(1, .directSnd, .now, "hello")
|
||||
return ContextItemView(chat: Chat.sampleData, contextItem: contextItem, contextIcon: "pencil.circle", cancelContextItem: {})
|
||||
return ContextItemView(chat: Chat.sampleData, contextItems: [contextItem], contextIcon: "pencil.circle", cancelContextItem: {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +227,7 @@ struct SendMessageView: View {
|
||||
!composeState.editing {
|
||||
if case .noContextItem = composeState.contextItem,
|
||||
!composeState.voicePreview,
|
||||
!composeState.manyMediaPreviews,
|
||||
let send = sendLiveMessage,
|
||||
let update = updateLiveMessage {
|
||||
Button {
|
||||
|
||||
@@ -32,12 +32,15 @@ struct SelectedItemsBottomToolbar: View {
|
||||
var deleteItems: (Bool) -> Void
|
||||
var moderateItems: () -> Void
|
||||
//var shareItems: () -> Void
|
||||
var forwardItems: () -> Void
|
||||
@State var deleteEnabled: Bool = false
|
||||
@State var deleteForEveryoneEnabled: Bool = false
|
||||
|
||||
@State var canModerate: Bool = false
|
||||
@State var moderateEnabled: Bool = false
|
||||
|
||||
@State var forwardEnabled: Bool = false
|
||||
|
||||
@State var allButtonsDisabled = false
|
||||
|
||||
var body: some View {
|
||||
@@ -50,6 +53,7 @@ struct SelectedItemsBottomToolbar: View {
|
||||
} label: {
|
||||
Image(systemName: "trash")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20, alignment: .center)
|
||||
.foregroundColor(!deleteEnabled || allButtonsDisabled ? theme.colors.secondary: .red)
|
||||
}
|
||||
@@ -61,24 +65,24 @@ struct SelectedItemsBottomToolbar: View {
|
||||
} label: {
|
||||
Image(systemName: "flag")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20, alignment: .center)
|
||||
.foregroundColor(!moderateEnabled || allButtonsDisabled ? theme.colors.secondary : .red)
|
||||
}
|
||||
.disabled(!moderateEnabled || allButtonsDisabled)
|
||||
.opacity(canModerate ? 1 : 0)
|
||||
|
||||
|
||||
Spacer()
|
||||
Button {
|
||||
//shareItems()
|
||||
forwardItems()
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
Image(systemName: "arrowshape.turn.up.forward")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 20, height: 20, alignment: .center)
|
||||
.foregroundColor(allButtonsDisabled ? theme.colors.secondary : theme.colors.primary)
|
||||
.foregroundColor(!forwardEnabled || allButtonsDisabled ? theme.colors.secondary : theme.colors.primary)
|
||||
}
|
||||
.disabled(allButtonsDisabled)
|
||||
.opacity(0)
|
||||
.disabled(!forwardEnabled || allButtonsDisabled)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding([.leading, .trailing], 12)
|
||||
@@ -106,15 +110,16 @@ struct SelectedItemsBottomToolbar: View {
|
||||
if let selected = selectedItems {
|
||||
let me: Bool
|
||||
let onlyOwnGroupItems: Bool
|
||||
(deleteEnabled, deleteForEveryoneEnabled, me, onlyOwnGroupItems, selectedChatItems) = chatItems.reduce((true, true, true, true, [])) { (r, ci) in
|
||||
(deleteEnabled, deleteForEveryoneEnabled, me, onlyOwnGroupItems, forwardEnabled, selectedChatItems) = chatItems.reduce((true, true, true, true, true, [])) { (r, ci) in
|
||||
if selected.contains(ci.id) {
|
||||
var (de, dee, me, onlyOwnGroupItems, sel) = r
|
||||
var (de, dee, me, onlyOwnGroupItems, fe, sel) = r
|
||||
de = de && ci.canBeDeletedForSelf
|
||||
dee = dee && ci.meta.deletable && !ci.localNote
|
||||
onlyOwnGroupItems = onlyOwnGroupItems && ci.chatDir == .groupSnd
|
||||
me = me && ci.content.msgContent != nil && ci.memberToModerate(chatInfo) != nil
|
||||
fe = fe && ci.content.msgContent != nil && ci.meta.itemDeleted == nil && !ci.isLiveDummy
|
||||
sel.insert(ci.id) // we are collecting new selected items here to account for any changes in chat items list
|
||||
return (de, dee, me, onlyOwnGroupItems, sel)
|
||||
return (de, dee, me, onlyOwnGroupItems, fe, sel)
|
||||
} else {
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -47,8 +47,24 @@ func showAlert(
|
||||
buttonAction()
|
||||
})
|
||||
if cancelButton {
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel))
|
||||
alert.addAction(cancelAlertAction)
|
||||
}
|
||||
topController.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func showAlert(
|
||||
_ title: String,
|
||||
message: String? = nil,
|
||||
actions: () -> [UIAlertAction] = { [okAlertAction] }
|
||||
) {
|
||||
if let topController = getTopViewController() {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
for action in actions() { alert.addAction(action) }
|
||||
topController.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default)
|
||||
|
||||
let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel)
|
||||
|
||||
@@ -222,11 +222,11 @@
|
||||
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
|
||||
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
|
||||
E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; };
|
||||
E55128DD2C9AA96B001D165C /* libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128D82C9AA96B001D165C /* libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63-ghc9.6.3.a */; };
|
||||
E55128DE2C9AA96B001D165C /* libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128D92C9AA96B001D165C /* libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63.a */; };
|
||||
E55128DF2C9AA96B001D165C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128DA2C9AA96B001D165C /* libffi.a */; };
|
||||
E55128E02C9AA96B001D165C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128DB2C9AA96B001D165C /* libgmpxx.a */; };
|
||||
E55128E12C9AA96B001D165C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128DC2C9AA96B001D165C /* libgmp.a */; };
|
||||
E55128E72C9AD063001D165C /* libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128E22C9AD063001D165C /* libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt.a */; };
|
||||
E55128E82C9AD063001D165C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128E32C9AD063001D165C /* libgmp.a */; };
|
||||
E55128E92C9AD063001D165C /* libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128E42C9AD063001D165C /* libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt-ghc9.6.3.a */; };
|
||||
E55128EA2C9AD063001D165C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128E52C9AD063001D165C /* libgmpxx.a */; };
|
||||
E55128EB2C9AD063001D165C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128E62C9AD063001D165C /* libffi.a */; };
|
||||
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
|
||||
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
|
||||
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
|
||||
@@ -566,11 +566,11 @@
|
||||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; };
|
||||
E55128D82C9AA96B001D165C /* libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E55128D92C9AA96B001D165C /* libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63.a"; sourceTree = "<group>"; };
|
||||
E55128DA2C9AA96B001D165C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
E55128DB2C9AA96B001D165C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
E55128DC2C9AA96B001D165C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E55128E22C9AD063001D165C /* libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt.a"; sourceTree = "<group>"; };
|
||||
E55128E32C9AD063001D165C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E55128E42C9AD063001D165C /* libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E55128E52C9AD063001D165C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
E55128E62C9AD063001D165C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
@@ -661,14 +661,14 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E55128E72C9AD063001D165C /* libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt.a in Frameworks */,
|
||||
E55128E82C9AD063001D165C /* libgmp.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
E55128DF2C9AA96B001D165C /* libffi.a in Frameworks */,
|
||||
E55128E12C9AA96B001D165C /* libgmp.a in Frameworks */,
|
||||
E55128DE2C9AA96B001D165C /* libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63.a in Frameworks */,
|
||||
E55128DD2C9AA96B001D165C /* libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63-ghc9.6.3.a in Frameworks */,
|
||||
E55128EB2C9AD063001D165C /* libffi.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
E55128E92C9AD063001D165C /* libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt-ghc9.6.3.a in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
E55128E02C9AA96B001D165C /* libgmpxx.a in Frameworks */,
|
||||
E55128EA2C9AD063001D165C /* libgmpxx.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -745,11 +745,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E55128DA2C9AA96B001D165C /* libffi.a */,
|
||||
E55128DC2C9AA96B001D165C /* libgmp.a */,
|
||||
E55128DB2C9AA96B001D165C /* libgmpxx.a */,
|
||||
E55128D82C9AA96B001D165C /* libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63-ghc9.6.3.a */,
|
||||
E55128D92C9AA96B001D165C /* libHSsimplex-chat-6.1.0.1-2faBJOPOSgP5OStZMK9U63.a */,
|
||||
E55128E62C9AD063001D165C /* libffi.a */,
|
||||
E55128E32C9AD063001D165C /* libgmp.a */,
|
||||
E55128E52C9AD063001D165C /* libgmpxx.a */,
|
||||
E55128E42C9AD063001D165C /* libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt-ghc9.6.3.a */,
|
||||
E55128E22C9AD063001D165C /* libHSsimplex-chat-6.1.0.2-ItgztLmvKyzFsOmChHMkFt.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -1901,7 +1901,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 237;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1926,7 +1926,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1950,7 +1950,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 237;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1975,7 +1975,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1991,11 +1991,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 237;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2011,11 +2011,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 237;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2036,7 +2036,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 237;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2051,7 +2051,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2073,7 +2073,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 237;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2088,7 +2088,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2110,7 +2110,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 237;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2136,7 +2136,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2161,7 +2161,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 237;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2187,7 +2187,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2212,7 +2212,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 237;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2227,7 +2227,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2246,7 +2246,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 237;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2261,7 +2261,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -49,6 +49,7 @@ public enum ChatCommand {
|
||||
case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode)
|
||||
case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64])
|
||||
case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction)
|
||||
case apiPlanForwardChatItems(toChatType: ChatType, toChatId: Int64, itemIds: [Int64])
|
||||
case apiForwardChatItems(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemIds: [Int64], ttl: Int?)
|
||||
case apiGetNtfToken
|
||||
case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode)
|
||||
@@ -204,6 +205,7 @@ public enum ChatCommand {
|
||||
case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)"
|
||||
case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
|
||||
case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))"
|
||||
case let .apiPlanForwardChatItems(type, id, itemIds): return "/_forward plan \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ","))"
|
||||
case let .apiForwardChatItems(toChatType, toChatId, fromChatType, fromChatId, itemIds, ttl):
|
||||
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||
return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) ttl=\(ttlStr)"
|
||||
@@ -359,6 +361,7 @@ public enum ChatCommand {
|
||||
case .apiConnectContactViaAddress: return "apiConnectContactViaAddress"
|
||||
case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem"
|
||||
case .apiChatItemReaction: return "apiChatItemReaction"
|
||||
case .apiPlanForwardChatItems: return "apiPlanForwardChatItems"
|
||||
case .apiForwardChatItems: return "apiForwardChatItems"
|
||||
case .apiGetNtfToken: return "apiGetNtfToken"
|
||||
case .apiRegisterToken: return "apiRegisterToken"
|
||||
@@ -601,6 +604,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case groupEmpty(user: UserRef, groupInfo: GroupInfo)
|
||||
case userContactLinkSubscribed
|
||||
case newChatItems(user: UserRef, chatItems: [AChatItem])
|
||||
case forwardPlan(user: UserRef, chatItemIds: [Int64], forwardConfirmation: ForwardConfirmation?)
|
||||
case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem])
|
||||
case chatItemUpdated(user: UserRef, chatItem: AChatItem)
|
||||
case chatItemNotChanged(user: UserRef, chatItem: AChatItem)
|
||||
@@ -772,6 +776,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .groupEmpty: return "groupEmpty"
|
||||
case .userContactLinkSubscribed: return "userContactLinkSubscribed"
|
||||
case .newChatItems: return "newChatItems"
|
||||
case .forwardPlan: return "forwardPlan"
|
||||
case .chatItemsStatusesUpdated: return "chatItemsStatusesUpdated"
|
||||
case .chatItemUpdated: return "chatItemUpdated"
|
||||
case .chatItemNotChanged: return "chatItemNotChanged"
|
||||
@@ -943,6 +948,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .newChatItems(u, chatItems):
|
||||
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
|
||||
return withUser(u, itemsString)
|
||||
case let .forwardPlan(u, chatItemIds, forwardConfirmation): return withUser(u, "items: \(chatItemIds) forwardConfirmation: \(String(describing: forwardConfirmation))")
|
||||
case let .chatItemsStatusesUpdated(u, chatItems):
|
||||
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
|
||||
return withUser(u, itemsString)
|
||||
@@ -1134,7 +1140,7 @@ public enum ChatPagination {
|
||||
public struct ComposedMessage: Encodable {
|
||||
public var fileSource: CryptoFile?
|
||||
var quotedItemId: Int64?
|
||||
var msgContent: MsgContent
|
||||
public var msgContent: MsgContent
|
||||
|
||||
public init(fileSource: CryptoFile? = nil, quotedItemId: Int64? = nil, msgContent: MsgContent) {
|
||||
self.fileSource = fileSource
|
||||
@@ -1595,6 +1601,13 @@ public enum NetworkStatus: Decodable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ForwardConfirmation: Decodable, Hashable {
|
||||
case filesNotAccepted(fileIds: [Int64])
|
||||
case filesInProgress(filesCount: Int)
|
||||
case filesMissing(filesCount: Int)
|
||||
case filesFailed(filesCount: Int)
|
||||
}
|
||||
|
||||
public struct ConnNetworkStatus: Decodable {
|
||||
public var agentConnId: String
|
||||
public var networkStatus: NetworkStatus
|
||||
|
||||
@@ -150,12 +150,9 @@ fun processIntent(intent: Intent?) {
|
||||
"android.intent.action.VIEW" -> {
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
val transformedUri = uri.toURIOrNull()
|
||||
if (transformedUri != null) {
|
||||
chatModel.appOpenUrl.value = null to transformedUri
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_parsing_uri_title), generalGetString(MR.strings.error_parsing_uri_desc))
|
||||
}
|
||||
chatModel.appOpenUrl.value = null to uri.toString()
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_parsing_uri_title), generalGetString(MR.strings.error_parsing_uri_desc))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -82,7 +82,7 @@ object ChatModel {
|
||||
val desktopOnboardingRandomPassword = mutableStateOf(false)
|
||||
|
||||
// set when app is opened via contact or invitation URI (rhId, uri)
|
||||
val appOpenUrl = mutableStateOf<Pair<Long?, URI>?>(null)
|
||||
val appOpenUrl = mutableStateOf<Pair<Long?, String>?>(null)
|
||||
|
||||
// Needed to check for bottom nav bar and to apply or not navigation bar color on Android
|
||||
val newChatSheetVisible = mutableStateOf(false)
|
||||
@@ -1404,6 +1404,14 @@ class Group (
|
||||
var members: List<GroupMember>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
sealed class ForwardConfirmation {
|
||||
@Serializable @SerialName("filesNotAccepted") data class FilesNotAccepted(val fileIds: List<Long>) : ForwardConfirmation()
|
||||
@Serializable @SerialName("filesInProgress") data class FilesInProgress(val filesCount: Int) : ForwardConfirmation()
|
||||
@Serializable @SerialName("filesMissing") data class FilesMissing(val filesCount: Int) : ForwardConfirmation()
|
||||
@Serializable @SerialName("filesFailed") data class FilesFailed(val filesCount: Int) : ForwardConfirmation()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GroupInfo (
|
||||
val groupId: Long,
|
||||
|
||||
+144
-58
@@ -1,18 +1,19 @@
|
||||
package chat.simplex.common.model
|
||||
|
||||
import SectionItemView
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.ChatController.getNetCfg
|
||||
import chat.simplex.common.model.ChatController.setNetCfg
|
||||
import chat.simplex.common.model.ChatModel.changingActiveUserMutex
|
||||
@@ -905,7 +906,15 @@ object ChatController {
|
||||
return processSendMessageCmd(rh, cmd)?.map { it.chatItem }
|
||||
}
|
||||
|
||||
|
||||
suspend fun apiPlanForwardChatItems(rh: Long?, fromChatType: ChatType, fromChatId: Long, chatItemIds: List<Long>): CR.ForwardPlan? {
|
||||
return when (val r = sendCmd(rh, CC.ApiPlanForwardChatItems(fromChatType, fromChatId, chatItemIds))) {
|
||||
is CR.ForwardPlan -> r
|
||||
else -> {
|
||||
apiErrorAlert("apiPlanForwardChatItems", generalGetString(MR.strings.error_forwarding_messages), r)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiUpdateChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, mc: MsgContent, live: Boolean = false): AChatItem? {
|
||||
val r = sendCmd(rh, CC.ApiUpdateChatItem(type, id, itemId, mc, live))
|
||||
@@ -1541,50 +1550,132 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiReceiveFile(rh: Long?, fileId: Long, userApprovedRelays: Boolean, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
|
||||
// -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected
|
||||
val r = sendCmd(rh, CC.ReceiveFile(fileId, userApprovedRelays = userApprovedRelays, encrypt = encrypted, inline = inline))
|
||||
return when (r) {
|
||||
is CR.RcvFileAccepted -> r.chatItem
|
||||
is CR.RcvFileAcceptedSndCancelled -> {
|
||||
Log.d(TAG, "apiReceiveFile error: sender cancelled file transfer")
|
||||
if (!auto) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.cannot_receive_file),
|
||||
generalGetString(MR.strings.sender_cancelled_file_transfer)
|
||||
)
|
||||
}
|
||||
null
|
||||
}
|
||||
suspend fun receiveFiles(rhId: Long?, user: UserLike, fileIds: List<Long>, userApprovedRelays: Boolean = false, auto: Boolean = false) {
|
||||
val fileIdsToApprove = mutableListOf<Long>()
|
||||
val srvsToApprove = mutableSetOf<String>()
|
||||
val otherFileErrs = mutableListOf<CR>()
|
||||
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
val maybeChatError = chatError(r)
|
||||
if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) {
|
||||
Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error")
|
||||
} else if (maybeChatError is ChatErrorType.FileNotApproved) {
|
||||
Log.d(TAG, "apiReceiveFile FileNotApproved error")
|
||||
if (!auto) {
|
||||
val srvs = maybeChatError.unknownServers.map{ serverHostname(it) }
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.file_not_approved_title),
|
||||
text = generalGetString(MR.strings.file_not_approved_descr).format(srvs.sorted().joinToString(separator = ", ")),
|
||||
confirmText = generalGetString(MR.strings.download_file),
|
||||
onConfirm = {
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
withBGApi { chatModel.controller.receiveFile(rh, user, fileId, userApprovedRelays = true) }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
} else if (!auto) {
|
||||
apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r)
|
||||
}
|
||||
for (fileId in fileIds) {
|
||||
val r = sendCmd(
|
||||
rhId, CC.ReceiveFile(
|
||||
fileId,
|
||||
userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(),
|
||||
encrypt = appPrefs.privacyEncryptLocalFiles.get(),
|
||||
inline = null
|
||||
)
|
||||
)
|
||||
if (r is CR.RcvFileAccepted) {
|
||||
chatItemSimpleUpdate(rhId, user, r.chatItem)
|
||||
} else {
|
||||
val maybeChatError = chatError(r)
|
||||
if (maybeChatError is ChatErrorType.FileNotApproved) {
|
||||
fileIdsToApprove.add(maybeChatError.fileId)
|
||||
srvsToApprove.addAll(maybeChatError.unknownServers.map { serverHostname(it) })
|
||||
} else {
|
||||
otherFileErrs.add(r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (!auto) {
|
||||
// If there are not approved files, alert is shown the same way both in case of singular and plural files reception
|
||||
if (fileIdsToApprove.isNotEmpty()) {
|
||||
showFilesToApproveAlert(
|
||||
srvsToApprove = srvsToApprove,
|
||||
otherFileErrs = otherFileErrs,
|
||||
approveFiles = {
|
||||
withBGApi {
|
||||
receiveFiles(
|
||||
rhId = rhId,
|
||||
user = user,
|
||||
fileIds = fileIdsToApprove,
|
||||
userApprovedRelays = true
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else if (otherFileErrs.size == 1) { // If there is a single other error, we differentiate on it
|
||||
when (val errCR = otherFileErrs.first()) {
|
||||
is CR.RcvFileAcceptedSndCancelled -> {
|
||||
Log.d(TAG, "receiveFiles error: sender cancelled file transfer")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.cannot_receive_file),
|
||||
generalGetString(MR.strings.sender_cancelled_file_transfer)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val maybeChatError = chatError(errCR)
|
||||
if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) {
|
||||
Log.d(TAG, "receiveFiles ignoring FileCancelled or FileAlreadyReceiving error")
|
||||
} else {
|
||||
apiErrorAlert("receiveFiles", generalGetString(MR.strings.error_receiving_file), errCR)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (otherFileErrs.size > 1) { // If there are multiple other errors, we show general alert
|
||||
val errsStr = otherFileErrs.map { json.encodeToString(it) }.joinToString(separator = "\n")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.error_receiving_file),
|
||||
text = String.format(generalGetString(MR.strings.n_file_errors), otherFileErrs.size, errsStr),
|
||||
shareText = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFilesToApproveAlert(
|
||||
srvsToApprove: Set<String>,
|
||||
otherFileErrs: List<CR>,
|
||||
approveFiles: (() -> Unit)
|
||||
) {
|
||||
val srvsToApproveStr = srvsToApprove.sorted().joinToString(separator = ", ")
|
||||
val alertText =
|
||||
generalGetString(MR.strings.file_not_approved_descr).format(srvsToApproveStr) +
|
||||
(if (otherFileErrs.isNotEmpty()) "\n" + generalGetString(MR.strings.n_other_file_errors).format(otherFileErrs.size) else "")
|
||||
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(generalGetString(MR.strings.file_not_approved_title), alertText, belowTextContent = {
|
||||
if (otherFileErrs.isNotEmpty()) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
SimpleButtonFrame(click = {
|
||||
clipboard.setText(AnnotatedString(otherFileErrs.map { json.encodeToString(it) }.joinToString(separator = "\n")))
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_content_copy),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(generalGetString(MR.strings.copy_error), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
// Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard
|
||||
delay(200)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
TextButton(onClick = AlertManager.shared::hideAlert) { Text(generalGetString(MR.strings.cancel_verb)) }
|
||||
TextButton(onClick = {
|
||||
approveFiles.invoke()
|
||||
AlertManager.shared.hideAlert()
|
||||
}, Modifier.focusRequester(focusRequester)) { Text(generalGetString(MR.strings.download_file)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) {
|
||||
receiveFiles(
|
||||
rhId = rhId,
|
||||
user = user,
|
||||
fileIds = listOf(fileId),
|
||||
userApprovedRelays = userApprovedRelays,
|
||||
auto = auto
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun cancelFile(rh: Long?, user: User, fileId: Long) {
|
||||
@@ -2689,19 +2780,6 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) {
|
||||
val chatItem = apiReceiveFile(
|
||||
rhId,
|
||||
fileId,
|
||||
userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(),
|
||||
encrypted = appPrefs.privacyEncryptLocalFiles.get(),
|
||||
auto = auto
|
||||
)
|
||||
if (chatItem != null) {
|
||||
chatItemSimpleUpdate(rhId, user, chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun leaveGroup(rh: Long?, groupId: Long) {
|
||||
val groupInfo = apiLeaveGroup(rh, groupId)
|
||||
if (groupInfo != null) {
|
||||
@@ -2914,6 +2992,7 @@ sealed class CC {
|
||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List<Long>, val mode: CIDeleteMode): CC()
|
||||
class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List<Long>): CC()
|
||||
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
|
||||
class ApiPlanForwardChatItems(val fromChatType: ChatType, val fromChatId: Long, val chatItemIds: List<Long>): CC()
|
||||
class ApiForwardChatItems(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemIds: List<Long>, val ttl: Int?): CC()
|
||||
class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC()
|
||||
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
|
||||
@@ -3072,6 +3151,9 @@ sealed class CC {
|
||||
val ttlStr = if (ttl != null) "$ttl" else "default"
|
||||
"/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} ${itemIds.joinToString(",")} ttl=${ttlStr}"
|
||||
}
|
||||
is ApiPlanForwardChatItems -> {
|
||||
"/_forward plan ${chatRef(fromChatType, fromChatId)} ${chatItemIds.joinToString(",")}"
|
||||
}
|
||||
is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}"
|
||||
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
|
||||
is ApiJoinGroup -> "/_join #$groupId"
|
||||
@@ -3216,6 +3298,7 @@ sealed class CC {
|
||||
is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem"
|
||||
is ApiChatItemReaction -> "apiChatItemReaction"
|
||||
is ApiForwardChatItems -> "apiForwardChatItems"
|
||||
is ApiPlanForwardChatItems -> "apiPlanForwardChatItems"
|
||||
is ApiNewGroup -> "apiNewGroup"
|
||||
is ApiAddMember -> "apiAddMember"
|
||||
is ApiJoinGroup -> "apiJoinGroup"
|
||||
@@ -4878,6 +4961,7 @@ sealed class CR {
|
||||
@Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: UserRef, val added: Boolean, val reaction: ACIReaction): CR()
|
||||
@Serializable @SerialName("chatItemsDeleted") class ChatItemsDeleted(val user: UserRef, val chatItemDeletions: List<ChatItemDeletion>, val byUser: Boolean): CR()
|
||||
@Serializable @SerialName("forwardPlan") class ForwardPlan(val user: UserRef, val itemsCount: Int, val chatItemIds: List<Long>, val forwardConfirmation: ForwardConfirmation? = null): CR()
|
||||
// group events
|
||||
@Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR()
|
||||
@@ -5055,6 +5139,7 @@ sealed class CR {
|
||||
is ChatItemNotChanged -> "chatItemNotChanged"
|
||||
is ChatItemReaction -> "chatItemReaction"
|
||||
is ChatItemsDeleted -> "chatItemsDeleted"
|
||||
is ForwardPlan -> "forwardPlan"
|
||||
is GroupCreated -> "groupCreated"
|
||||
is SentGroupInvitation -> "sentGroupInvitation"
|
||||
is UserAcceptedGroupSent -> "userAcceptedGroupSent"
|
||||
@@ -5224,6 +5309,7 @@ sealed class CR {
|
||||
is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem))
|
||||
is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}")
|
||||
is ChatItemsDeleted -> withUser(user, "${chatItemDeletions.map { (deletedChatItem, toChatItem) -> "deletedChatItem: ${json.encodeToString(deletedChatItem)}\ntoChatItem: ${json.encodeToString(toChatItem)}" }} \nbyUser: $byUser")
|
||||
is ForwardPlan -> withUser(user, "itemsCount: $itemsCount\nchatItemIds: ${json.encodeToString(chatItemIds)}\nforwardConfirmation: ${json.encodeToString(forwardConfirmation)}")
|
||||
is GroupCreated -> withUser(user, json.encodeToString(groupInfo))
|
||||
is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member")
|
||||
is UserAcceptedGroupSent -> json.encodeToString(groupInfo)
|
||||
|
||||
+163
-3
@@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
@@ -172,7 +173,30 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
forwardItems = {
|
||||
val itemIds = selectedChatItems.value
|
||||
|
||||
if (itemIds != null) {
|
||||
withBGApi {
|
||||
val chatItemIds = itemIds.toList()
|
||||
val forwardPlan = controller.apiPlanForwardChatItems(
|
||||
rh = chatRh,
|
||||
fromChatType = chatInfo.chatType,
|
||||
fromChatId = chatInfo.apiId,
|
||||
chatItemIds = chatItemIds
|
||||
)
|
||||
|
||||
if (forwardPlan != null) {
|
||||
if (forwardPlan.chatItemIds.count() < chatItemIds.count() || forwardPlan.forwardConfirmation != null) {
|
||||
handleForwardConfirmation(chatRh, forwardPlan, chatInfo)
|
||||
} else {
|
||||
forwardContent(forwardPlan.chatItemIds, chatInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -347,9 +371,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
openDirectChat(chatRh, contactId, chatModel)
|
||||
}
|
||||
},
|
||||
forwardItem = { cItem, cInfo ->
|
||||
forwardItem = { cInfo, cItem ->
|
||||
chatModel.chatId.value = null
|
||||
chatModel.sharedContent.value = SharedContent.Forward(cInfo, cItem)
|
||||
chatModel.sharedContent.value = SharedContent.Forward(listOf(cItem), cInfo)
|
||||
},
|
||||
updateContactStats = { contact ->
|
||||
withBGApi {
|
||||
@@ -1416,6 +1440,65 @@ private fun TopEndFloatingButton(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadFilesButton(
|
||||
forwardConfirmation: ForwardConfirmation.FilesNotAccepted,
|
||||
rhId: Long?,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding
|
||||
) {
|
||||
val user = chatModel.currentUser.value
|
||||
|
||||
if (user != null) {
|
||||
TextButton(
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
AlertManager.shared.hideAlert()
|
||||
|
||||
withBGApi {
|
||||
controller.receiveFiles(
|
||||
rhId = rhId,
|
||||
fileIds = forwardConfirmation.fileIds,
|
||||
user = user
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(MR.strings.forward_files_not_accepted_receive_files), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ForwardButton(
|
||||
forwardPlan: CR.ForwardPlan,
|
||||
chatInfo: ChatInfo,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
forwardContent(forwardPlan.chatItemIds, chatInfo)
|
||||
AlertManager.shared.hideAlert()
|
||||
},
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding
|
||||
) {
|
||||
Text(stringResource(MR.strings.forward_chat_item), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ButtonRow(horizontalArrangement: Arrangement.Horizontal, content: @Composable() (RowScope.() -> Unit)) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalArrangement = horizontalArrangement
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
val chatViewScrollState = MutableStateFlow(false)
|
||||
|
||||
fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) {
|
||||
@@ -1712,6 +1795,83 @@ private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConf
|
||||
override val touchSlop: Float get() = slop
|
||||
}
|
||||
|
||||
private fun forwardContent(chatItemsIds: List<Long>, chatInfo: ChatInfo) {
|
||||
chatModel.chatId.value = null
|
||||
chatModel.sharedContent.value = SharedContent.Forward(
|
||||
chatModel.chatItems.value.filter { chatItemsIds.contains(it.id) },
|
||||
chatInfo
|
||||
)
|
||||
}
|
||||
|
||||
private fun forwardConfirmationAlertDescription(forwardConfirmation: ForwardConfirmation): String {
|
||||
return when (forwardConfirmation) {
|
||||
is ForwardConfirmation.FilesNotAccepted -> String.format(generalGetString(MR.strings.forward_files_not_accepted_desc), forwardConfirmation.fileIds.count())
|
||||
is ForwardConfirmation.FilesInProgress -> String.format(generalGetString(MR.strings.forward_files_in_progress_desc), forwardConfirmation.filesCount)
|
||||
is ForwardConfirmation.FilesFailed -> String.format(generalGetString(MR.strings.forward_files_failed_to_receive_desc), forwardConfirmation.filesCount)
|
||||
is ForwardConfirmation.FilesMissing -> String.format(generalGetString(MR.strings.forward_files_missing_desc), forwardConfirmation.filesCount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleForwardConfirmation(
|
||||
rhId: Long?,
|
||||
forwardPlan: CR.ForwardPlan,
|
||||
chatInfo: ChatInfo
|
||||
) {
|
||||
var alertDescription = if (forwardPlan.forwardConfirmation != null) forwardConfirmationAlertDescription(forwardPlan.forwardConfirmation) else ""
|
||||
|
||||
if (forwardPlan.chatItemIds.isNotEmpty()) {
|
||||
alertDescription += "\n${generalGetString(MR.strings.forward_alert_forward_messages_without_files)}"
|
||||
}
|
||||
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = if (forwardPlan.chatItemIds.isNotEmpty())
|
||||
String.format(generalGetString(MR.strings.forward_alert_title_messages_to_forward), forwardPlan.chatItemIds.count()) else
|
||||
generalGetString(MR.strings.forward_alert_title_nothing_to_forward),
|
||||
text = alertDescription,
|
||||
buttons = {
|
||||
if (forwardPlan.chatItemIds.isNotEmpty()) {
|
||||
when (val confirmation = forwardPlan.forwardConfirmation) {
|
||||
is ForwardConfirmation.FilesNotAccepted -> {
|
||||
val fillMaxWidthModifier = Modifier.fillMaxWidth()
|
||||
val contentPadding = PaddingValues(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)
|
||||
Column {
|
||||
ForwardButton(forwardPlan, chatInfo, fillMaxWidthModifier, contentPadding)
|
||||
DownloadFilesButton(confirmation, rhId, fillMaxWidthModifier, contentPadding)
|
||||
TextButton(onClick = { AlertManager.shared.hideAlert() }, modifier = fillMaxWidthModifier, contentPadding = contentPadding) {
|
||||
Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
ButtonRow(Arrangement.SpaceBetween) {
|
||||
TextButton(onClick = { AlertManager.shared.hideAlert() }) {
|
||||
Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
ForwardButton(forwardPlan, chatInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
when (val confirmation = forwardPlan.forwardConfirmation) {
|
||||
is ForwardConfirmation.FilesNotAccepted -> {
|
||||
ButtonRow(Arrangement.SpaceBetween) {
|
||||
TextButton(onClick = { AlertManager.shared.hideAlert() }) {
|
||||
Text(stringResource(MR.strings.cancel_verb), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
DownloadFilesButton(confirmation, rhId)
|
||||
}
|
||||
}
|
||||
else -> ButtonRow(Arrangement.Center) {
|
||||
TextButton(onClick = { AlertManager.shared.hideAlert() }) {
|
||||
Text(stringResource(MR.strings.ok), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview/*(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
|
||||
+44
-26
@@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -49,7 +48,7 @@ sealed class ComposeContextItem {
|
||||
@Serializable object NoContextItem: ComposeContextItem()
|
||||
@Serializable class QuotedItem(val chatItem: ChatItem): ComposeContextItem()
|
||||
@Serializable class EditingItem(val chatItem: ChatItem): ComposeContextItem()
|
||||
@Serializable class ForwardingItem(val chatItem: ChatItem, val fromChatInfo: ChatInfo): ComposeContextItem()
|
||||
@Serializable class ForwardingItems(val chatItems: List<ChatItem>, val fromChatInfo: ChatInfo): ComposeContextItem()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -85,7 +84,7 @@ data class ComposeState(
|
||||
}
|
||||
val forwarding: Boolean
|
||||
get() = when (contextItem) {
|
||||
is ComposeContextItem.ForwardingItem -> true
|
||||
is ComposeContextItem.ForwardingItems -> true
|
||||
else -> false
|
||||
}
|
||||
val sendEnabled: () -> Boolean
|
||||
@@ -407,33 +406,41 @@ fun ComposeView(
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List<ChatItem>? {
|
||||
val cInfo = chat.chatInfo
|
||||
val cs = composeState.value
|
||||
var sent: ChatItem?
|
||||
var sent: List<ChatItem>?
|
||||
val msgText = text ?: cs.message
|
||||
|
||||
fun sending() {
|
||||
composeState.value = composeState.value.copy(inProgress = true)
|
||||
}
|
||||
|
||||
suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo, ttl: Int?): ChatItem? {
|
||||
suspend fun forwardItem(rhId: Long?, forwardedItem: List<ChatItem>, fromChatInfo: ChatInfo, ttl: Int?): List<ChatItem>? {
|
||||
val chatItems = controller.apiForwardChatItems(
|
||||
rh = rhId,
|
||||
toChatType = chat.chatInfo.chatType,
|
||||
toChatId = chat.chatInfo.apiId,
|
||||
fromChatType = fromChatInfo.chatType,
|
||||
fromChatId = fromChatInfo.apiId,
|
||||
itemIds = listOf(forwardedItem.id),
|
||||
itemIds = forwardedItem.map { it.id },
|
||||
ttl = ttl
|
||||
)
|
||||
|
||||
chatItems?.forEach { chatItem ->
|
||||
withChats {
|
||||
addChatItem(rhId, chat.chatInfo, chatItem)
|
||||
}
|
||||
}
|
||||
// TODO batch send: forward multiple messages
|
||||
return chatItems?.firstOrNull()
|
||||
|
||||
if (chatItems != null && chatItems.count() < forwardedItem.count()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = String.format(generalGetString(MR.strings.forward_files_messages_deleted_after_selection_title), forwardedItem.count() - chatItems.count()),
|
||||
text = generalGetString(MR.strings.forward_files_messages_deleted_after_selection_desc)
|
||||
)
|
||||
}
|
||||
|
||||
return chatItems
|
||||
}
|
||||
|
||||
fun checkLinkPreview(): MsgContent {
|
||||
@@ -506,16 +513,25 @@ fun ComposeView(
|
||||
if (chat.nextSendGrpInv) {
|
||||
sendMemberContactInvitation()
|
||||
sent = null
|
||||
} else if (cs.contextItem is ComposeContextItem.ForwardingItem) {
|
||||
sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItem, cs.contextItem.fromChatInfo, ttl = ttl)
|
||||
} else if (cs.contextItem is ComposeContextItem.ForwardingItems) {
|
||||
sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItems, cs.contextItem.fromChatInfo, ttl = ttl)
|
||||
if (cs.message.isNotEmpty()) {
|
||||
sent = send(chat, checkLinkPreview(), quoted = sent?.id, live = false, ttl = ttl)
|
||||
sent?.mapIndexed { index, message ->
|
||||
if (index == sent!!.lastIndex) {
|
||||
send(chat, checkLinkPreview(), quoted = message.id, live = false, ttl = ttl)
|
||||
} else {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (cs.contextItem is ComposeContextItem.EditingItem) {
|
||||
}
|
||||
else if (cs.contextItem is ComposeContextItem.EditingItem) {
|
||||
val ei = cs.contextItem.chatItem
|
||||
sent = updateMessage(ei, chat, live)
|
||||
val updatedMessage = updateMessage(ei, chat, live)
|
||||
sent = if (updatedMessage != null) listOf(updatedMessage) else null
|
||||
} else if (liveMessage != null && liveMessage.sent) {
|
||||
sent = updateMessage(liveMessage.chatItem, chat, live)
|
||||
val updatedMessage = updateMessage(liveMessage.chatItem, chat, live)
|
||||
sent = if (updatedMessage != null) listOf(updatedMessage) else null
|
||||
} else {
|
||||
val msgs: ArrayList<MsgContent> = ArrayList()
|
||||
val files: ArrayList<CryptoFile> = ArrayList()
|
||||
@@ -608,21 +624,23 @@ fun ComposeView(
|
||||
localPath = file.filePath
|
||||
)
|
||||
}
|
||||
sent = send(chat, content, if (index == 0) quotedItemId else null, file,
|
||||
val sendResult = send(chat, content, if (index == 0) quotedItemId else null, file,
|
||||
live = if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false,
|
||||
ttl = ttl
|
||||
)
|
||||
sent = if (sendResult != null) listOf(sendResult) else null
|
||||
}
|
||||
if (sent == null &&
|
||||
(cs.preview is ComposePreview.MediaPreview ||
|
||||
cs.preview is ComposePreview.FilePreview ||
|
||||
cs.preview is ComposePreview.VoicePreview)
|
||||
) {
|
||||
sent = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
|
||||
val sendResult = send(chat, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
|
||||
sent = if (sendResult != null) listOf(sendResult) else null
|
||||
}
|
||||
}
|
||||
val wasForwarding = cs.forwarding
|
||||
val forwardingFromChatId = (cs.contextItem as? ComposeContextItem.ForwardingItem)?.fromChatInfo?.id
|
||||
val forwardingFromChatId = (cs.contextItem as? ComposeContextItem.ForwardingItems)?.fromChatInfo?.id
|
||||
clearState(live)
|
||||
val draft = chatModel.draft.value
|
||||
if (wasForwarding && chatModel.draftChatId.value == chat.chatInfo.id && forwardingFromChatId != chat.chatInfo.id && draft != null) {
|
||||
@@ -724,8 +742,8 @@ fun ComposeView(
|
||||
val typedMsg = cs.message
|
||||
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage.sent)) {
|
||||
val ci = sendMessageAsync(typedMsg, live = true, ttl = null)
|
||||
if (ci != null) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
|
||||
if (!ci.isNullOrEmpty()) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci.last(), typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
|
||||
}
|
||||
} else if (cs.liveMessage == null) {
|
||||
val cItem = chatModel.addLiveDummy(chat.chatInfo)
|
||||
@@ -745,8 +763,8 @@ fun ComposeView(
|
||||
val sentMsg = liveMessageToSend(liveMessage, typedMsg)
|
||||
if (sentMsg != null) {
|
||||
val ci = sendMessageAsync(sentMsg, live = true, ttl = null)
|
||||
if (ci != null) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
|
||||
if (!ci.isNullOrEmpty()) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci.last(), typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
|
||||
}
|
||||
} else if (liveMessage.typedMsg != typedMsg) {
|
||||
composeState.value = composeState.value.copy(liveMessage = liveMessage.copy(typedMsg = typedMsg))
|
||||
@@ -805,13 +823,13 @@ fun ComposeView(
|
||||
fun contextItemView() {
|
||||
when (val contextItem = composeState.value.contextItem) {
|
||||
ComposeContextItem.NoContextItem -> {}
|
||||
is ComposeContextItem.QuotedItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_reply)) {
|
||||
is ComposeContextItem.QuotedItem -> ContextItemView(listOf(contextItem.chatItem), painterResource(MR.images.ic_reply), chatType = chat.chatInfo.chatType) {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem)
|
||||
}
|
||||
is ComposeContextItem.EditingItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_edit_filled)) {
|
||||
is ComposeContextItem.EditingItem -> ContextItemView(listOf(contextItem.chatItem), painterResource(MR.images.ic_edit_filled), chatType = chat.chatInfo.chatType) {
|
||||
clearState()
|
||||
}
|
||||
is ComposeContextItem.ForwardingItem -> ContextItemView(contextItem.chatItem, painterResource(MR.images.ic_forward), showSender = false) {
|
||||
is ComposeContextItem.ForwardingItems -> ContextItemView(contextItem.chatItems, painterResource(MR.images.ic_forward), showSender = false, chatType = chat.chatInfo.chatType) {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.NoContextItem)
|
||||
}
|
||||
}
|
||||
@@ -834,7 +852,7 @@ fun ComposeView(
|
||||
is SharedContent.Media -> composeState.processPickedMedia(shared.uris, shared.text)
|
||||
is SharedContent.File -> composeState.processPickedFile(shared.uri, shared.text)
|
||||
is SharedContent.Forward -> composeState.value = composeState.value.copy(
|
||||
contextItem = ComposeContextItem.ForwardingItem(shared.chatItem, shared.fromChatInfo),
|
||||
contextItem = ComposeContextItem.ForwardingItems(shared.chatItems, shared.fromChatInfo),
|
||||
preview = if (composeState.value.preview is ComposePreview.CLinkPreview) composeState.value.preview else ComposePreview.NoPreview
|
||||
)
|
||||
null -> {}
|
||||
|
||||
+41
-25
@@ -13,28 +13,31 @@ import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.getLoadedFilePath
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun ContextItemView(
|
||||
contextItem: ChatItem,
|
||||
contextItems: List<ChatItem>,
|
||||
contextIcon: Painter,
|
||||
showSender: Boolean = true,
|
||||
cancelContextItem: () -> Unit
|
||||
chatType: ChatType,
|
||||
cancelContextItem: () -> Unit,
|
||||
) {
|
||||
val sent = contextItem.chatDir.sent
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
|
||||
@Composable
|
||||
fun MessageText(attachment: ImageResource?, lines: Int) {
|
||||
fun MessageText(contextItem: ChatItem, attachment: ImageResource?, lines: Int) {
|
||||
val inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = if (attachment != null) {
|
||||
remember(contextItem.id) {
|
||||
val inlineContentBuilder: AnnotatedString.Builder.() -> Unit = {
|
||||
@@ -62,19 +65,24 @@ fun ContextItemView(
|
||||
)
|
||||
}
|
||||
|
||||
fun attachment(): ImageResource? =
|
||||
when (contextItem.content.msgContent) {
|
||||
is MsgContent.MCFile -> MR.images.ic_draft_filled
|
||||
fun attachment(contextItem: ChatItem): ImageResource? {
|
||||
val fileIsLoaded = getLoadedFilePath(contextItem.file) != null
|
||||
|
||||
return when (contextItem.content.msgContent) {
|
||||
is MsgContent.MCFile -> if (fileIsLoaded) MR.images.ic_draft_filled else null
|
||||
is MsgContent.MCImage -> MR.images.ic_image
|
||||
is MsgContent.MCVoice -> MR.images.ic_play_arrow_filled
|
||||
is MsgContent.MCVoice -> if (fileIsLoaded) MR.images.ic_play_arrow_filled else null
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContextMsgPreview(lines: Int) {
|
||||
MessageText(remember(contextItem.id) { attachment() }, lines)
|
||||
fun ContextMsgPreview(contextItem: ChatItem, lines: Int) {
|
||||
MessageText(contextItem, remember(contextItem.id) { attachment(contextItem) }, lines)
|
||||
}
|
||||
|
||||
val sent = contextItems[0].chatDir.sent
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
@@ -97,20 +105,27 @@ fun ContextItemView(
|
||||
contentDescription = stringResource(MR.strings.icon_descr_context),
|
||||
tint = MaterialTheme.colors.secondary,
|
||||
)
|
||||
val sender = contextItem.memberDisplayName
|
||||
if (showSender && sender != null) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
sender,
|
||||
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
|
||||
)
|
||||
ContextMsgPreview(lines = 2)
|
||||
|
||||
if (contextItems.count() == 1) {
|
||||
val contextItem = contextItems[0]
|
||||
val sender = contextItem.memberDisplayName
|
||||
|
||||
if (showSender && sender != null) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
sender,
|
||||
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary)
|
||||
)
|
||||
ContextMsgPreview(contextItem, lines = 2)
|
||||
}
|
||||
} else {
|
||||
ContextMsgPreview(contextItem, lines = 3)
|
||||
}
|
||||
} else {
|
||||
ContextMsgPreview(lines = 3)
|
||||
} else if (contextItems.isNotEmpty()) {
|
||||
Text(String.format(generalGetString(if (chatType == ChatType.Local) MR.strings.compose_save_messages_n else MR.strings.compose_forward_messages_n), contextItems.count()), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = cancelContextItem) {
|
||||
@@ -129,8 +144,9 @@ fun ContextItemView(
|
||||
fun PreviewContextItemView() {
|
||||
SimpleXTheme {
|
||||
ContextItemView(
|
||||
contextItem = ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello"),
|
||||
contextIcon = painterResource(MR.images.ic_edit_filled)
|
||||
contextItems = listOf(ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), "hello")),
|
||||
contextIcon = painterResource(MR.images.ic_edit_filled),
|
||||
chatType = ChatType.Direct
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-6
@@ -7,6 +7,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -51,17 +52,29 @@ fun SelectedItemsBottomToolbar(
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible
|
||||
moderateItems: () -> Unit,
|
||||
// shareItems: () -> Unit,
|
||||
forwardItems: () -> Unit,
|
||||
) {
|
||||
val deleteEnabled = remember { mutableStateOf(false) }
|
||||
val deleteForEveryoneEnabled = remember { mutableStateOf(false) }
|
||||
val canModerate = remember { mutableStateOf(false) }
|
||||
val moderateEnabled = remember { mutableStateOf(false) }
|
||||
val forwardEnabled = remember { mutableStateOf(false) }
|
||||
val allButtonsDisabled = remember { mutableStateOf(false) }
|
||||
Box {
|
||||
// It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty
|
||||
ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {})
|
||||
Row(Modifier.matchParentSize().background(MaterialTheme.colors.background), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
.background(MaterialTheme.colors.background)
|
||||
.pointerInput(Unit) {
|
||||
detectGesture {
|
||||
true
|
||||
}
|
||||
},
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton({ deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !allButtonsDisabled.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_delete),
|
||||
@@ -80,18 +93,18 @@ fun SelectedItemsBottomToolbar(
|
||||
)
|
||||
}
|
||||
|
||||
IconButton({ /*shareItems()*/ }, Modifier.alpha(0f), enabled = false/*!allButtonsDisabled.value*/) {
|
||||
IconButton({ forwardItems() }, enabled = forwardEnabled.value && !allButtonsDisabled.value) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_share),
|
||||
painterResource(MR.images.ic_forward),
|
||||
null,
|
||||
Modifier.size(22.dp),
|
||||
tint = if (allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
tint = if (!forwardEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) {
|
||||
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, allButtonsDisabled)
|
||||
recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, allButtonsDisabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +115,7 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||
deleteForEveryoneEnabled: MutableState<Boolean>,
|
||||
canModerate: MutableState<Boolean>,
|
||||
moderateEnabled: MutableState<Boolean>,
|
||||
forwardEnabled: MutableState<Boolean>,
|
||||
allButtonsDisabled: MutableState<Boolean>
|
||||
) {
|
||||
val count = selectedChatItems.value?.size ?: 0
|
||||
@@ -112,6 +126,7 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||
var rDeleteForEveryoneEnabled = true
|
||||
var rModerateEnabled = true
|
||||
var rOnlyOwnGroupItems = true
|
||||
var rForwardEnabled = true
|
||||
val rSelectedChatItems = mutableSetOf<Long>()
|
||||
for (ci in chatItems) {
|
||||
if (selected.contains(ci.id)) {
|
||||
@@ -119,6 +134,7 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||
rDeleteForEveryoneEnabled = rDeleteForEveryoneEnabled && ci.meta.deletable && !ci.localNote
|
||||
rOnlyOwnGroupItems = rOnlyOwnGroupItems && ci.chatDir is CIDirection.GroupSnd
|
||||
rModerateEnabled = rModerateEnabled && ci.content.msgContent != null && ci.memberToModerate(chatInfo) != null
|
||||
rForwardEnabled = rForwardEnabled && ci.content.msgContent != null && ci.meta.itemDeleted == null && !ci.isLiveDummy
|
||||
rSelectedChatItems.add(ci.id) // we are collecting new selected items here to account for any changes in chat items list
|
||||
}
|
||||
}
|
||||
@@ -126,6 +142,7 @@ private fun recheckItems(chatInfo: ChatInfo,
|
||||
deleteEnabled.value = rDeleteEnabled
|
||||
deleteForEveryoneEnabled.value = rDeleteForEveryoneEnabled
|
||||
moderateEnabled.value = rModerateEnabled
|
||||
forwardEnabled.value = rForwardEnabled
|
||||
selectedChatItems.value = rSelectedChatItems
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -665,9 +665,8 @@ private fun updateMemberRoleDialog(
|
||||
|
||||
fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) {
|
||||
try {
|
||||
val uri = URI(connReqUri)
|
||||
withBGApi {
|
||||
planAndConnect(rhId, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
|
||||
planAndConnect(rhId, connReqUri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() })
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
|
||||
+2
-2
@@ -478,7 +478,7 @@ private fun ToggleFilterEnabledButton() {
|
||||
@Composable
|
||||
expect fun ActiveCallInteractiveArea(call: Call)
|
||||
|
||||
fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) {
|
||||
fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||
if (chatModel.currentUser.value == null) {
|
||||
chatModel.appOpenUrl.value = rhId to uri
|
||||
@@ -566,7 +566,7 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<
|
||||
withBGApi {
|
||||
planAndConnect(
|
||||
chatModel.remoteHostId(),
|
||||
URI.create(link),
|
||||
link,
|
||||
incognito = null,
|
||||
filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id },
|
||||
filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id },
|
||||
|
||||
+9
-7
@@ -58,11 +58,13 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
hasSimplexLink = hasSimplexLink(sharedContent.text)
|
||||
}
|
||||
is SharedContent.Forward -> {
|
||||
val mc = sharedContent.chatItem.content.msgContent
|
||||
if (mc != null) {
|
||||
isMediaOrFileAttachment = mc.isMediaOrFileAttachment
|
||||
isVoice = mc.isVoice
|
||||
hasSimplexLink = hasSimplexLink(mc.text)
|
||||
sharedContent.chatItems.forEach { ci ->
|
||||
val mc = ci.content.msgContent
|
||||
if (mc != null) {
|
||||
isMediaOrFileAttachment = isMediaOrFileAttachment || mc.isMediaOrFileAttachment
|
||||
isVoice = isVoice || mc.isVoice
|
||||
hasSimplexLink = hasSimplexLink || hasSimplexLink(mc.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
null -> {}
|
||||
@@ -175,11 +177,11 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
when (chatModel.sharedContent.value) {
|
||||
when (val v = chatModel.sharedContent.value) {
|
||||
is SharedContent.Text -> stringResource(MR.strings.share_message)
|
||||
is SharedContent.Media -> stringResource(MR.strings.share_image)
|
||||
is SharedContent.File -> stringResource(MR.strings.share_file)
|
||||
is SharedContent.Forward -> stringResource(MR.strings.forward_message)
|
||||
is SharedContent.Forward -> if (v.chatItems.size > 1) stringResource(MR.strings.forward_multiple) else stringResource(MR.strings.forward_message)
|
||||
null -> stringResource(MR.strings.share_message)
|
||||
},
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
|
||||
+2
-3
@@ -2,8 +2,7 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import chat.simplex.common.model.ChatInfo
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.model.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
@@ -15,7 +14,7 @@ sealed class SharedContent {
|
||||
data class Text(val text: String): SharedContent()
|
||||
data class Media(val text: String, val uris: List<URI>): SharedContent()
|
||||
data class File(val text: String, val uri: URI): SharedContent()
|
||||
data class Forward(val chatItem: ChatItem, val fromChatInfo: ChatInfo): SharedContent()
|
||||
data class Forward(val chatItems: List<ChatItem>, val fromChatInfo: ChatInfo): SharedContent()
|
||||
}
|
||||
|
||||
enum class AnimatedViewState {
|
||||
|
||||
+3
-4
@@ -480,12 +480,11 @@ inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
|
||||
)
|
||||
|
||||
fun UriHandler.openVerifiedSimplexUri(uri: String) {
|
||||
val URI = try { URI.create(uri) } catch (e: Exception) { null }
|
||||
if (URI != null) {
|
||||
connectIfOpenedViaUri(chatModel.remoteHostId(), URI, ChatModel)
|
||||
}
|
||||
connectIfOpenedViaUri(chatModel.remoteHostId(), uri, ChatModel)
|
||||
}
|
||||
|
||||
fun uriCreateOrNull(uri: String) = try { URI.create(uri) } catch (e: Exception) { null }
|
||||
|
||||
fun UriHandler.openUriCatching(uri: String) {
|
||||
try {
|
||||
openUri(uri)
|
||||
|
||||
+6
-6
@@ -20,7 +20,7 @@ enum class ConnectionLinkType {
|
||||
|
||||
suspend fun planAndConnect(
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
uri: String,
|
||||
incognito: Boolean?,
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)? = null,
|
||||
@@ -29,7 +29,7 @@ suspend fun planAndConnect(
|
||||
) {
|
||||
val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri.toString())
|
||||
if (connectionPlan != null) {
|
||||
val link = strHasSingleSimplexLink(uri.toString().trim())
|
||||
val link = strHasSingleSimplexLink(uri.trim())
|
||||
val linkText = if (link?.format is Format.SimplexLink)
|
||||
"<br><br><u>${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}</u>"
|
||||
else
|
||||
@@ -323,13 +323,13 @@ suspend fun planAndConnect(
|
||||
suspend fun connectViaUri(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
uri: String,
|
||||
incognito: Boolean,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
close: (() -> Unit)?,
|
||||
cleanup: (() -> Unit)?,
|
||||
) {
|
||||
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString())
|
||||
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri)
|
||||
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
|
||||
if (pcc != null) {
|
||||
withChats {
|
||||
@@ -361,7 +361,7 @@ fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType
|
||||
fun askCurrentOrIncognitoProfileAlert(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
uri: String,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
close: (() -> Unit)?,
|
||||
title: String,
|
||||
@@ -417,7 +417,7 @@ fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, co
|
||||
fun ownGroupLinkConfirmConnect(
|
||||
chatModel: ChatModel,
|
||||
rhId: Long?,
|
||||
uri: URI,
|
||||
uri: String,
|
||||
linkText: String,
|
||||
incognito: Boolean?,
|
||||
connectionPlan: ConnectionPlan?,
|
||||
|
||||
+1
-1
@@ -482,7 +482,7 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState<
|
||||
withBGApi {
|
||||
planAndConnect(
|
||||
chatModel.remoteHostId(),
|
||||
URI.create(link),
|
||||
link,
|
||||
incognito = null,
|
||||
filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id },
|
||||
close = close,
|
||||
|
||||
+4
-2
@@ -68,7 +68,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
|
||||
* Otherwise, it will be called here AFTER [AddContactLearnMore] is launched and will clear the value too soon.
|
||||
* It will be dropped automatically when connection established or when user goes away from this screen.
|
||||
**/
|
||||
if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() == 1) {
|
||||
if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() <= 1) {
|
||||
val conn = contactConnection.value
|
||||
if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
@@ -308,6 +308,7 @@ fun ActiveProfilePicker(
|
||||
switchingProfile.value = true
|
||||
withApi {
|
||||
try {
|
||||
appPreferences.incognito.set(false)
|
||||
var updatedConn: PendingContactConnection? = null;
|
||||
|
||||
if (contactConnection != null) {
|
||||
@@ -361,6 +362,7 @@ fun ActiveProfilePicker(
|
||||
switchingProfile.value = true
|
||||
withApi {
|
||||
try {
|
||||
appPreferences.incognito.set(true)
|
||||
val conn = controller.apiSetConnectionIncognito(rhId, contactConnection.pccConnId, true)
|
||||
if (conn != null) {
|
||||
withChats {
|
||||
@@ -653,7 +655,7 @@ private suspend fun verify(rhId: Long?, text: String?, close: () -> Unit): Boole
|
||||
private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanup: (() -> Unit)? = null) {
|
||||
planAndConnect(
|
||||
rhId,
|
||||
URI.create(link),
|
||||
link,
|
||||
close = close,
|
||||
cleanup = cleanup,
|
||||
incognito = null
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
<string name="proxy_destination_error_broker_version">Destination server version of %1$s is incompatible with forwarding server %2$s.</string>
|
||||
<string name="please_try_later">Please try later.</string>
|
||||
<string name="error_sending_message">Error sending message</string>
|
||||
<string name="error_forwarding_messages">Error forwarding messages</string>
|
||||
<string name="error_creating_message">Error creating message</string>
|
||||
<string name="error_loading_details">Error loading details</string>
|
||||
<string name="error_adding_members">Error adding member(s)</string>
|
||||
@@ -133,7 +134,9 @@
|
||||
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>
|
||||
<string name="file_not_approved_title">Unknown servers!</string>
|
||||
<string name="file_not_approved_descr">Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s.</string>
|
||||
<string name="n_other_file_errors">%1$d other file error(s).</string>
|
||||
<string name="error_receiving_file">Error receiving file</string>
|
||||
<string name="n_file_errors">%1$d file error(s):\n%2$s</string>
|
||||
<string name="error_creating_address">Error creating address</string>
|
||||
<string name="contact_already_exists">Contact already exists</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">You are already connected to %1$s.</string>
|
||||
@@ -378,12 +381,23 @@
|
||||
<string name="no_selected_chat">No selected chat</string>
|
||||
<string name="selected_chat_items_nothing_selected">Nothing selected</string>
|
||||
<string name="selected_chat_items_selected_n">Selected %d</string>
|
||||
<string name="forward_alert_title_messages_to_forward">Forward %1$s message(s)?</string>
|
||||
<string name="forward_alert_title_nothing_to_forward">Nothing to forward!</string>
|
||||
<string name="forward_alert_forward_messages_without_files">Forward messages without files?</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_desc">Messages were deleted after you selected them.</string>
|
||||
<string name="forward_files_not_accepted_desc">%1$d file(s) were not downloaded.</string>
|
||||
<string name="forward_files_in_progress_desc">%1$d file(s) are still being downloaded.</string>
|
||||
<string name="forward_files_failed_to_receive_desc">%1$d file(s) failed to download.</string>
|
||||
<string name="forward_files_missing_desc">%1$d file(s) were deleted.</string>
|
||||
<string name="forward_files_not_accepted_receive_files">Download</string>
|
||||
<string name="forward_files_messages_deleted_after_selection_title">%1$s messages not forwarded</string>
|
||||
|
||||
<!-- ShareListView.kt -->
|
||||
<string name="share_message">Share message…</string>
|
||||
<string name="share_image">Share media…</string>
|
||||
<string name="share_file">Share file…</string>
|
||||
<string name="forward_message">Forward message…</string>
|
||||
<string name="forward_multiple">Forward messages…</string>
|
||||
<string name="cannot_share_message_alert_title">Cannot send message</string>
|
||||
<string name="cannot_share_message_alert_text">Selected chat preferences prohibit this message.</string>
|
||||
|
||||
@@ -405,6 +419,8 @@
|
||||
<string name="files_and_media_prohibited">Files and media prohibited!</string>
|
||||
<string name="only_owners_can_enable_files_and_media">Only group owners can enable files and media.</string>
|
||||
<string name="compose_send_direct_message_to_connect">Send direct message to connect</string>
|
||||
<string name="compose_forward_messages_n">Forwarding %1$s messages</string>
|
||||
<string name="compose_save_messages_n">Saving %1$s messages</string>
|
||||
<string name="simplex_links_not_allowed">SimpleX links not allowed</string>
|
||||
<string name="files_and_media_not_allowed">Files and media not allowed</string>
|
||||
<string name="voice_messages_not_allowed">Voice messages not allowed</string>
|
||||
|
||||
@@ -34,6 +34,9 @@ var useWorker = false;
|
||||
var isDesktop = false;
|
||||
var localizedState = "";
|
||||
var localizedDescription = "";
|
||||
// When one side of a call sends candidates tot fast (until local & remote descriptions are set), that candidates
|
||||
// will be stored here and then set when the call will be ready to process them
|
||||
let afterCallInitializedCandidates = [];
|
||||
const processCommand = (function () {
|
||||
const defaultIceServers = [
|
||||
{ urls: ["stuns:stun.simplex.im:443"] },
|
||||
@@ -234,6 +237,8 @@ const processCommand = (function () {
|
||||
const pc = activeCall.connection;
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
addIceCandidates(pc, afterCallInitializedCandidates);
|
||||
afterCallInitializedCandidates = [];
|
||||
// for debugging, returning the command for callee to use
|
||||
// resp = {
|
||||
// type: "offer",
|
||||
@@ -272,6 +277,8 @@ const processCommand = (function () {
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
addIceCandidates(pc, remoteIceCandidates);
|
||||
addIceCandidates(pc, afterCallInitializedCandidates);
|
||||
afterCallInitializedCandidates = [];
|
||||
// same as command for caller to use
|
||||
resp = {
|
||||
type: "answer",
|
||||
@@ -297,17 +304,20 @@ const processCommand = (function () {
|
||||
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(answer));
|
||||
addIceCandidates(pc, remoteIceCandidates);
|
||||
addIceCandidates(pc, afterCallInitializedCandidates);
|
||||
afterCallInitializedCandidates = [];
|
||||
resp = { type: "ok" };
|
||||
}
|
||||
break;
|
||||
case "ice":
|
||||
const remoteIceCandidates = parse(command.iceCandidates);
|
||||
if (pc) {
|
||||
const remoteIceCandidates = parse(command.iceCandidates);
|
||||
addIceCandidates(pc, remoteIceCandidates);
|
||||
resp = { type: "ok" };
|
||||
}
|
||||
else {
|
||||
resp = { type: "error", message: "ice: call not started" };
|
||||
afterCallInitializedCandidates.push(...remoteIceCandidates);
|
||||
resp = { type: "error", message: "ice: call not started yet, will add candidates later" };
|
||||
}
|
||||
break;
|
||||
case "media":
|
||||
|
||||
+1
-1
@@ -204,7 +204,7 @@ fun startServer(onResponse: (WVAPIMessage) -> Unit): NanoWSD {
|
||||
return when {
|
||||
session.headers["upgrade"] == "websocket" -> super.handle(session)
|
||||
session.uri.contains("/simplex/call/") -> resourcesToResponse("/desktop/call.html")
|
||||
else -> resourcesToResponse(URI.create(session.uri).path)
|
||||
else -> resourcesToResponse(uriCreateOrNull(session.uri)?.path ?: return newFixedLengthResponse("Error parsing URL"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.0.4
|
||||
android.version_code=237
|
||||
android.version_name=6.1-beta.0
|
||||
android.version_code=239
|
||||
|
||||
desktop.version_name=6.0.4
|
||||
desktop.version_code=65
|
||||
desktop.version_name=6.1-beta.0
|
||||
desktop.version_code=66
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
||||
Reference in New Issue
Block a user