Merge branch 'master' into f/ios-servers-summary

This commit is contained in:
spaced4ndy
2024-06-25 17:45:06 +04:00
56 changed files with 1183 additions and 219 deletions
@@ -19,6 +19,9 @@ struct ChatItemForwardingView: View {
@State private var searchText: String = ""
@FocusState private var searchFocused
@State private var alert: SomeAlert?
@State private var hasSimplexLink_: Bool?
private let chatsToForwardTo = filterChatsToForwardTo()
var body: some View {
NavigationView {
@@ -35,47 +38,29 @@ struct ChatItemForwardingView: View {
}
}
}
.alert(item: $alert) { $0.alert }
}
@ViewBuilder private func forwardListView() -> some View {
VStack(alignment: .leading) {
let chatsToForwardTo = filterChatsToForwardTo()
if !chatsToForwardTo.isEmpty {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
searchFieldView(text: $searchText, focussed: $searchFocused)
.padding(.leading, 2)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { filterChatSearched($0, s) }
ForEach(chats) { chat in
Divider()
forwardListNavLinkView(chat)
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
}
List {
searchFieldView(text: $searchText, focussed: $searchFocused)
.padding(.leading, 2)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { foundChat($0, s) }
ForEach(chats) { chat in
forwardListChatView(chat)
.disabled(chatModel.deletedChats.contains(chat.chatInfo.id))
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(uiColor: .systemBackground))
.cornerRadius(12)
.padding(.horizontal)
}
.background(Color(.systemGroupedBackground))
} else {
emptyList()
}
}
}
private func filterChatsToForwardTo() -> [Chat] {
var filteredChats = chatModel.chats.filter({ canForwardToChat($0) })
if let index = filteredChats.firstIndex(where: { $0.chatInfo.chatType == .local }) {
let privateNotes = filteredChats.remove(at: index)
filteredChats.insert(privateNotes, at: 0)
}
return filteredChats
}
private func filterChatSearched(_ chat: Chat, _ searchStr: String) -> Bool {
private func foundChat(_ chat: Chat, _ searchStr: String) -> Bool {
let cInfo = chat.chatInfo
return switch cInfo {
case let .direct(contact):
@@ -91,42 +76,70 @@ struct ChatItemForwardingView: View {
}
}
private func canForwardToChat(_ chat: Chat) -> Bool {
switch chat.chatInfo {
case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv
case let .group(groupInfo): groupInfo.sendMsgEnabled
case let .local(noteFolder): noteFolder.sendMsgEnabled
private func prohibitedByPref(_ chat: Chat) -> Bool {
// preference checks should match checks in compose view
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
let fileProhibited = (ci.content.msgContent?.isMediaOrFileAttachment ?? false) && !chat.groupFeatureEnabled(.files)
let voiceProhibited = (ci.content.msgContent?.isVoice ?? false) && !chat.chatInfo.featureEnabled(.voice)
return switch chat.chatInfo {
case .direct: voiceProhibited
case .group: simplexLinkProhibited || fileProhibited || voiceProhibited
case .local: false
case .contactRequest: false
case .contactConnection: false
case .invalidJSON: false
}
}
private var hasSimplexLink: Bool {
if let hasSimplexLink_ { return hasSimplexLink_ }
let r =
if let mcText = ci.content.msgContent?.text,
let parsedMsg = parseSimpleXMarkdown(mcText) {
parsedMsgHasSimplexLink(parsedMsg)
} else {
false
}
hasSimplexLink_ = r
return r
}
private func emptyList() -> some View {
Text("No filtered chats")
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
}
@ViewBuilder private func forwardListNavLinkView(_ chat: Chat) -> some View {
@ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View {
let prohibited = prohibitedByPref(chat)
Button {
dismiss()
if chat.id == fromChatInfo.id {
composeState = ComposeState(
message: composeState.message,
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
)
if prohibited {
alert = SomeAlert(
alert: mkAlert(
title: "Cannot forward message",
message: "Selected chat preferences prohibit this message."
),
id: "forward prohibited by preferences"
)
} else {
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
chatModel.chatId = chat.id
dismiss()
if chat.id == fromChatInfo.id {
composeState = ComposeState(
message: composeState.message,
preview: composeState.linkPreview != nil ? composeState.preview : .noPreview,
contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo)
)
} else {
composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo)
chatModel.chatId = chat.id
}
}
} label: {
HStack {
ChatInfoImage(chat: chat, size: 30)
.padding(.trailing, 2)
Text(chat.chatInfo.chatViewName)
.foregroundColor(.primary)
.foregroundColor(prohibited ? .secondary : .primary)
.lineLimit(1)
if chat.chatInfo.incognito {
Spacer()
@@ -142,6 +155,27 @@ struct ChatItemForwardingView: View {
}
}
private func filterChatsToForwardTo() -> [Chat] {
var filteredChats = ChatModel.shared.chats.filter { c in
c.chatInfo.chatType != .local && canForwardToChat(c)
}
if let privateNotes = ChatModel.shared.chats.first(where: { $0.chatInfo.chatType == .local }) {
filteredChats.insert(privateNotes, at: 0)
}
return filteredChats
}
private func canForwardToChat(_ chat: Chat) -> Bool {
switch chat.chatInfo {
case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv
case let .group(groupInfo): groupInfo.sendMsgEnabled
case let .local(noteFolder): noteFolder.sendMsgEnabled
case .contactRequest: false
case .contactConnection: false
case .invalidJSON: false
}
}
#Preview {
ChatItemForwardingView(
ci: ChatItem.getSample(1, .directSnd, .now, "hello"),
@@ -286,6 +286,7 @@ struct ComposeView: View {
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
ContextInvitingContactMemberView()
}
// preference checks should match checks in forwarding list
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
@@ -1065,7 +1066,7 @@ struct ComposeView: View {
} else {
nil
}
let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
let simplexLink = parsedMsgHasSimplexLink(parsedMsg)
return (url, simplexLink)
}
@@ -1105,6 +1106,10 @@ struct ComposeView: View {
}
}
func parsedMsgHasSimplexLink(_ parsedMsg: [FormattedText]) -> Bool {
parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false })
}
struct ComposeView_Previews: PreviewProvider {
static var previews: some View {
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
@@ -252,7 +252,7 @@ struct SMPServerSummaryView: View {
} footer: {
if let known = summary.known, known {
// TODO open settings?
Text("Server can be configured in **Settings** -> **Network & servers** -> **SMP servers**")
Text("Server is configured in **Settings** -> **Network & servers** -> **SMP servers**.")
}
}
@@ -11,14 +11,9 @@ import SimpleXChat
import CodeScanner
import AVFoundation
enum SomeAlert: Identifiable {
case someAlert(alert: Alert, id: String)
var id: String {
switch self {
case let .someAlert(_, id): return id
}
}
struct SomeAlert: Identifiable {
var alert: Alert
var id: String
}
private enum NewChatViewAlert: Identifiable {
@@ -142,8 +137,8 @@ struct NewChatView: View {
switch(a) {
case let .planAndConnectAlert(alert):
return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" })
case let .newChatSomeAlert(.someAlert(alert, _)):
return alert
case let .newChatSomeAlert(a):
return a.alert
}
}
}
@@ -181,7 +176,7 @@ struct NewChatView: View {
await MainActor.run {
creatingConnReq = false
if let apiAlert = apiAlert {
alert = .newChatSomeAlert(alert: .someAlert(alert: apiAlert, id: "createInvitation error"))
alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error"))
}
}
}
@@ -315,7 +310,7 @@ private struct ConnectView: View {
// showQRCodeScanner = false
connect(pastedLink)
} else {
alert = .newChatSomeAlert(alert: .someAlert(
alert = .newChatSomeAlert(alert: SomeAlert(
alert: mkAlert(title: "Invalid link", message: "The text you pasted is not a SimpleX link."),
id: "pasteLinkView: code is not a SimpleX link"
))
@@ -338,14 +333,14 @@ private struct ConnectView: View {
if strIsSimplexLink(r.string) {
connect(link)
} else {
alert = .newChatSomeAlert(alert: .someAlert(
alert = .newChatSomeAlert(alert: SomeAlert(
alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
id: "processQRCode: code is not a SimpleX link"
))
}
case let .failure(e):
logger.error("processQRCode QR code error: \(e.localizedDescription)")
alert = .newChatSomeAlert(alert: .someAlert(
alert = .newChatSomeAlert(alert: SomeAlert(
alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
id: "processQRCode: failure"
))
+9
View File
@@ -3440,6 +3440,15 @@ public enum MsgContent: Equatable {
}
}
public var isMediaOrFileAttachment: Bool {
switch self {
case .image: true
case .video: true
case .file: true
default: false
}
}
var cmdString: String {
"json \(encodeJSON(self))"
}
@@ -2925,6 +2925,20 @@ sealed class MsgContent {
@Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
val isVoice: Boolean get() =
when (this) {
is MCVoice -> true
else -> false
}
val isMediaOrFileAttachment: Boolean get() =
when (this) {
is MCImage -> true
is MCVideo -> true
is MCFile -> true
else -> false
}
val cmdString: String get() =
if (this is MCUnknown) "json $json" else "json ${json.encodeToString(this)}"
}
@@ -9,30 +9,55 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import chat.simplex.common.views.helpers.ProfileImage
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
@Composable
fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
fun ShareListNavLinkView(
chat: Chat,
chatModel: ChatModel,
isMediaOrFileAttachment: Boolean,
isVoice: Boolean,
hasSimplexLink: Boolean
) {
val stopped = chatModel.chatRunning.value == false
when (chat.chatInfo) {
is ChatInfo.Direct ->
is ChatInfo.Direct -> {
val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat) },
click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) },
chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited) },
click = {
if (voiceProhibited) {
showForwardProhibitedByPrefAlert()
} else {
directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel)
}
},
stopped
)
is ChatInfo.Group ->
}
is ChatInfo.Group -> {
val simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)
val fileProhibited = isMediaOrFileAttachment && !chat.groupFeatureEnabled(GroupFeature.Files)
val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
val prohibitedByPref = simplexLinkProhibited || fileProhibited || voiceProhibited
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat) },
click = { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) },
chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref) },
click = {
if (prohibitedByPref) {
showForwardProhibitedByPrefAlert()
} else {
groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel)
}
},
stopped
)
}
is ChatInfo.Local ->
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat) },
chatLinkPreview = { SharePreviewView(chat, disabled = false) },
click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) },
stopped
)
@@ -40,6 +65,13 @@ fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
}
}
private fun showForwardProhibitedByPrefAlert() {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.cannot_share_message_alert_title),
text = generalGetString(MR.strings.cannot_share_message_alert_text),
)
}
@Composable
private fun ShareListNavLinkLayout(
chatLinkPreview: @Composable () -> Unit,
@@ -53,7 +85,7 @@ private fun ShareListNavLinkLayout(
}
@Composable
private fun SharePreviewView(chat: Chat) {
private fun SharePreviewView(chat: Chat, disabled: Boolean) {
Row(
Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -70,7 +102,7 @@ private fun SharePreviewView(chat: Chat) {
}
Text(
chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
color = if (chat.chatInfo.incognito) Indigo else Color.Unspecified
color = if (disabled) MaterialTheme.colors.secondary else if (chat.chatInfo.incognito) Indigo else Color.Unspecified
)
}
}
@@ -31,13 +31,44 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
scaffoldState = scaffoldState,
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
) {
val sharedContent = chatModel.sharedContent.value
var isMediaOrFileAttachment = false
var isVoice = false
var hasSimplexLink = false
when (sharedContent) {
is SharedContent.Text ->
hasSimplexLink = hasSimplexLink(sharedContent.text)
is SharedContent.Media -> {
isMediaOrFileAttachment = true
hasSimplexLink = hasSimplexLink(sharedContent.text)
}
is SharedContent.File -> {
isMediaOrFileAttachment = true
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)
}
}
null -> {}
}
Box(Modifier.padding(it)) {
Column(
modifier = Modifier
.fillMaxSize()
) {
if (chatModel.chats.isNotEmpty()) {
ShareList(chatModel, search = searchInList)
ShareList(
chatModel,
search = searchInList,
isMediaOrFileAttachment = isMediaOrFileAttachment,
isVoice = isVoice,
hasSimplexLink = hasSimplexLink
)
} else {
EmptyList()
}
@@ -54,6 +85,11 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe
}
}
private fun hasSimplexLink(msg: String): Boolean {
val parsedMsg = parseToMarkdown(msg) ?: return false
return parsedMsg.any { ft -> ft.format is Format.SimplexLink }
}
@Composable
private fun EmptyList() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -141,7 +177,13 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
}
@Composable
private fun ShareList(chatModel: ChatModel, search: String) {
private fun ShareList(
chatModel: ChatModel,
search: String,
isMediaOrFileAttachment: Boolean,
isVoice: Boolean,
hasSimplexLink: Boolean
) {
val chats by remember(search) {
derivedStateOf {
val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local }
@@ -156,7 +198,13 @@ private fun ShareList(chatModel: ChatModel, search: String) {
modifier = Modifier.fillMaxWidth()
) {
items(chats) { chat ->
ShareListNavLinkView(chat, chatModel)
ShareListNavLinkView(
chat,
chatModel,
isMediaOrFileAttachment = isMediaOrFileAttachment,
isVoice = isVoice,
hasSimplexLink = hasSimplexLink
)
}
}
}
@@ -358,6 +358,8 @@
<string name="share_image">Share media…</string>
<string name="share_file">Share file…</string>
<string name="forward_message">Forward message…</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>
<!-- ComposeView.kt, helpers -->
<string name="attach">Attach</string>
+4 -4
View File
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=5.8
android.version_code=219
android.version_name=5.8.1
android.version_code=221
desktop.version_name=5.8
desktop.version_code=53
desktop.version_name=5.8.1
desktop.version_code=54
kotlin.version=1.9.23
gradle.plugin.version=8.2.0