mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 12:53:18 +00:00
Merge branch 'master' into master-android
This commit is contained in:
32 files changed
+448
-246
No files matched your search
+1
-1
@@ -46,7 +46,7 @@ actual fun SaveOrOpenFileMenu(
|
||||
}
|
||||
ItemAction(
|
||||
stringResource(MR.strings.save_verb),
|
||||
painterResource(if (encrypted) MR.images.ic_lock_open_right else MR.images.ic_download),
|
||||
painterResource(MR.images.ic_download),
|
||||
color = MaterialTheme.colors.primary,
|
||||
onClick = {
|
||||
saveFile()
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ actual fun ReactionIcon(text: String, fontSize: TextUnit) {
|
||||
@Composable
|
||||
actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) {
|
||||
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = {
|
||||
ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = {
|
||||
when (cItem.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.status == PermissionStatus.Granted) {
|
||||
|
||||
+2
-2
@@ -1743,9 +1743,9 @@ data class Contact(
|
||||
val active get() = contactStatus == ContactStatus.Active
|
||||
override val nextConnect get() = sendMsgToConnect
|
||||
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
|
||||
override val nextConnectPrepared get() = preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared)
|
||||
override val nextConnectPrepared get() = active && preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared)
|
||||
override val profileChangeProhibited get() = activeConn != null
|
||||
val nextAcceptContactRequest get() = contactRequestId != null && (activeConn == null || activeConn.connStatus == ConnStatus.New)
|
||||
val nextAcceptContactRequest get() = active && contactRequestId != null && (activeConn == null || activeConn.connStatus == ConnStatus.New)
|
||||
val sendMsgToConnect get() = nextSendGrpInv || nextConnectPrepared
|
||||
override val incognito get() = contactConnIncognito
|
||||
override fun featureEnabled(feature: ChatFeature) = when (feature) {
|
||||
|
||||
+1
@@ -2872,6 +2872,7 @@ object ChatController {
|
||||
}
|
||||
if (
|
||||
chatModel.chatId.value == r.groupInfo.id
|
||||
&& !r.groupInfo.membership.memberPending
|
||||
&& ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)
|
||||
&& chatModel.secondaryChatsContext.value?.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext
|
||||
) {
|
||||
|
||||
+1
-1
@@ -989,7 +989,7 @@ fun ChatLayout(
|
||||
if (oneHandUI.value) {
|
||||
StatusBarBackground()
|
||||
}
|
||||
Column(if (oneHandUI.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
|
||||
Column(if (oneHandUI.value && chatBottomBar.value) Modifier.align(Alignment.BottomStart).imePadding() else Modifier) {
|
||||
Box {
|
||||
if (selectedChatItems.value == null) {
|
||||
MemberSupportChatAppBar(chatsCtx, chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_, { ModalManager.end.closeModal() }, onSearchValueChanged)
|
||||
|
||||
+75
-74
@@ -1101,86 +1101,87 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) }
|
||||
LaunchedEffect(allowedVoiceByPrefs) {
|
||||
if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
// Voice was disabled right when this user records it, just cancel it
|
||||
cancelVoice()
|
||||
}
|
||||
}
|
||||
val needToAllowVoiceToContact = remember(chat.chatInfo) {
|
||||
chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.voice) {
|
||||
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
|
||||
contactPreference.allow == FeatureAllowed.YES
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { recState.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
when (it) {
|
||||
is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false)
|
||||
is RecordingState.Finished -> if (it.durationMs > 300) {
|
||||
onAudioAdded(it.filePath, it.durationMs, true)
|
||||
} else {
|
||||
cancelVoice()
|
||||
}
|
||||
is RecordingState.NotStarted -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(rememberUpdatedState(chat.chatInfo.sendMsgEnabled).value) {
|
||||
if (!chat.chatInfo.sendMsgEnabled) {
|
||||
clearCurrentDraft()
|
||||
clearState()
|
||||
}
|
||||
}
|
||||
|
||||
KeyChangeEffect(chatModel.chatId.value) { prevChatId ->
|
||||
val cs = composeState.value
|
||||
if (cs.liveMessage != null && (cs.message.text.isNotEmpty() || cs.liveMessage.sent)) {
|
||||
sendMessage(null)
|
||||
resetLinkPreview()
|
||||
clearPrevDraft(prevChatId)
|
||||
deleteUnusedFiles()
|
||||
} else if (cs.inProgress) {
|
||||
clearPrevDraft(prevChatId)
|
||||
} else if (!cs.empty) {
|
||||
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
|
||||
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
|
||||
}
|
||||
if (saveLastDraft) {
|
||||
chatModel.draft.value = composeState.value
|
||||
chatModel.draftChatId.value = prevChatId
|
||||
}
|
||||
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
} else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) {
|
||||
composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
clearPrevDraft(prevChatId)
|
||||
deleteUnusedFiles()
|
||||
}
|
||||
chatModel.removeLiveDummy()
|
||||
CIFile.cachedRemoteFileRequests.clear()
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
// Don't enable this on Android, it breaks it, This method only works on desktop. For Android there is a `KeyChangeEffect(chatModel.chatId.value)`
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (chatModel.sharedContent.value is SharedContent.Forward && saveLastDraft && !composeState.value.empty) {
|
||||
chatModel.draft.value = composeState.value
|
||||
chatModel.draftChatId.value = chat.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SendMsgView_(
|
||||
disableSendButton: Boolean,
|
||||
placeholder: String? = null,
|
||||
sendToConnect: (() -> Unit)? = null
|
||||
) {
|
||||
val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) }
|
||||
LaunchedEffect(allowedVoiceByPrefs) {
|
||||
if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
// Voice was disabled right when this user records it, just cancel it
|
||||
cancelVoice()
|
||||
}
|
||||
}
|
||||
val needToAllowVoiceToContact = remember(chat.chatInfo) {
|
||||
chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.voice) {
|
||||
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
|
||||
contactPreference.allow == FeatureAllowed.YES
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { recState.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
when (it) {
|
||||
is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false)
|
||||
is RecordingState.Finished -> if (it.durationMs > 300) {
|
||||
onAudioAdded(it.filePath, it.durationMs, true)
|
||||
} else {
|
||||
cancelVoice()
|
||||
}
|
||||
is RecordingState.NotStarted -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(rememberUpdatedState(chat.chatInfo.sendMsgEnabled).value) {
|
||||
if (!chat.chatInfo.sendMsgEnabled) {
|
||||
clearCurrentDraft()
|
||||
clearState()
|
||||
}
|
||||
}
|
||||
|
||||
KeyChangeEffect(chatModel.chatId.value) { prevChatId ->
|
||||
val cs = composeState.value
|
||||
if (cs.liveMessage != null && (cs.message.text.isNotEmpty() || cs.liveMessage.sent)) {
|
||||
sendMessage(null)
|
||||
resetLinkPreview()
|
||||
clearPrevDraft(prevChatId)
|
||||
deleteUnusedFiles()
|
||||
} else if (cs.inProgress) {
|
||||
clearPrevDraft(prevChatId)
|
||||
} else if (!cs.empty) {
|
||||
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
|
||||
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
|
||||
}
|
||||
if (saveLastDraft) {
|
||||
chatModel.draft.value = composeState.value
|
||||
chatModel.draftChatId.value = prevChatId
|
||||
}
|
||||
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
} else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) {
|
||||
composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
clearPrevDraft(prevChatId)
|
||||
deleteUnusedFiles()
|
||||
}
|
||||
chatModel.removeLiveDummy()
|
||||
CIFile.cachedRemoteFileRequests.clear()
|
||||
}
|
||||
if (appPlatform.isDesktop) {
|
||||
// Don't enable this on Android, it breaks it, This method only works on desktop. For Android there is a `KeyChangeEffect(chatModel.chatId.value)`
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (chatModel.sharedContent.value is SharedContent.Forward && saveLastDraft && !composeState.value.empty) {
|
||||
chatModel.draft.value = composeState.value
|
||||
chatModel.draftChatId.value = chat.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }
|
||||
val sendButtonColor =
|
||||
if (chat.chatInfo.incognito)
|
||||
|
||||
+3
-2
@@ -56,6 +56,7 @@ fun MemberSupportChatAppBar(
|
||||
onSearchValueChanged: (String) -> Unit
|
||||
) {
|
||||
val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state }
|
||||
val chatBottomBar = remember { ChatController.appPrefs.chatBottomBar.state }
|
||||
val showSearch = rememberSaveable { mutableStateOf(false) }
|
||||
val onBackClicked = {
|
||||
if (!showSearch.value) {
|
||||
@@ -71,7 +72,7 @@ fun MemberSupportChatAppBar(
|
||||
navigationButton = { NavigationButtonBack(onBackClicked) },
|
||||
title = { MemberSupportChatToolbarTitle(scopeMember_) },
|
||||
onTitleClick = null,
|
||||
onTop = !oneHandUI.value,
|
||||
onTop = !oneHandUI.value || !chatBottomBar.value,
|
||||
showSearch = showSearch.value,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = {
|
||||
@@ -85,7 +86,7 @@ fun MemberSupportChatAppBar(
|
||||
navigationButton = { NavigationButtonBack(onBackClicked) },
|
||||
fixedTitleText = stringResource(MR.strings.support_chat),
|
||||
onTitleClick = null,
|
||||
onTop = !oneHandUI.value,
|
||||
onTop = !oneHandUI.value || !chatBottomBar.value,
|
||||
showSearch = showSearch.value,
|
||||
onSearchValueChanged = onSearchValueChanged,
|
||||
buttons = {
|
||||
|
||||
+67
-53
@@ -212,61 +212,75 @@ fun ChatPreviewView(
|
||||
fun chatPreviewText() {
|
||||
val previewText = chatPreviewInfoText()
|
||||
val ci = chat.chatItems.lastOrNull()
|
||||
if (ci?.content?.hasMsgContent != true && previewText != null) {
|
||||
if (chatModelDraftChatId == chat.id && chatModelDraft != null) {
|
||||
val sp20 = with(LocalDensity.current) { 20.sp.toDp() }
|
||||
val (text: CharSequence, inlineTextContent) = remember(chatModelDraft) { chatModelDraft.message.text to messageDraft(chatModelDraft, sp20) }
|
||||
val formattedText = null
|
||||
MarkdownText(
|
||||
text,
|
||||
formattedText,
|
||||
toggleSecrets = false,
|
||||
linkMode = linkMode,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(
|
||||
fontFamily = Inter,
|
||||
fontSize = 15.sp,
|
||||
color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight,
|
||||
lineHeight = 21.sp
|
||||
),
|
||||
inlineContent = inlineTextContent,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} else if (ci?.content?.hasMsgContent != true && previewText != null) {
|
||||
Text(previewText.first, color = previewText.second)
|
||||
} else if (ci != null) {
|
||||
if (showChatPreviews || (chatModelDraftChatId == chat.id && chatModelDraft != null)) {
|
||||
val sp20 = with(LocalDensity.current) { 20.sp.toDp() }
|
||||
val (text: CharSequence, inlineTextContent) = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { chatModelDraft.message.text to messageDraft(chatModelDraft, sp20) }
|
||||
ci.meta.itemDeleted == null -> ci.text to null
|
||||
else -> markedDeletedText(ci, chat.chatInfo) to null
|
||||
}
|
||||
val formattedText = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
ci.meta.itemDeleted == null -> ci.formattedText
|
||||
else -> null
|
||||
}
|
||||
val prefix = when (val mc = ci.content.msgContent) {
|
||||
is MsgContent.MCReport ->
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = Color.Red, fontStyle = FontStyle.Italic)) {
|
||||
append(if (text.isEmpty()) mc.reason.text else "${mc.reason.text}: ")
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
MarkdownText(
|
||||
text,
|
||||
formattedText,
|
||||
sender = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
cInfo is ChatInfo.Group && !ci.chatDir.sent && !ci.meta.showGroupAsSender -> ci.memberDisplayName
|
||||
else -> null
|
||||
},
|
||||
mentions = ci.mentions,
|
||||
userMemberId = when {
|
||||
cInfo is ChatInfo.Group -> cInfo.groupInfo.membership.memberId
|
||||
else -> null
|
||||
},
|
||||
toggleSecrets = false,
|
||||
linkMode = linkMode,
|
||||
senderBold = true,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(
|
||||
fontFamily = Inter,
|
||||
fontSize = 15.sp,
|
||||
color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight,
|
||||
lineHeight = 21.sp
|
||||
),
|
||||
inlineContent = inlineTextContent,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
prefix = prefix
|
||||
)
|
||||
} else if (ci != null && showChatPreviews) {
|
||||
val (text: CharSequence, inlineTextContent) = when {
|
||||
ci.meta.itemDeleted == null -> ci.text to null
|
||||
else -> markedDeletedText(ci, chat.chatInfo) to null
|
||||
}
|
||||
val formattedText = when {
|
||||
ci.meta.itemDeleted == null -> ci.formattedText
|
||||
else -> null
|
||||
}
|
||||
val prefix = when (val mc = ci.content.msgContent) {
|
||||
is MsgContent.MCReport ->
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = Color.Red, fontStyle = FontStyle.Italic)) {
|
||||
append(if (text.isEmpty()) mc.reason.text else "${mc.reason.text}: ")
|
||||
}
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
MarkdownText(
|
||||
text,
|
||||
formattedText,
|
||||
sender = when {
|
||||
cInfo is ChatInfo.Group && !ci.chatDir.sent && !ci.meta.showGroupAsSender -> ci.memberDisplayName
|
||||
else -> null
|
||||
},
|
||||
mentions = ci.mentions,
|
||||
userMemberId = when {
|
||||
cInfo is ChatInfo.Group -> cInfo.groupInfo.membership.memberId
|
||||
else -> null
|
||||
},
|
||||
toggleSecrets = false,
|
||||
linkMode = linkMode,
|
||||
senderBold = true,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(
|
||||
fontFamily = Inter,
|
||||
fontSize = 15.sp,
|
||||
color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight,
|
||||
lineHeight = 21.sp
|
||||
),
|
||||
inlineContent = inlineTextContent,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
prefix = prefix
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, hasSess: Boolean, leadi
|
||||
val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg())
|
||||
val statusColorAndPercentage = subscriptionStatusColorAndPercentage(chatModel.networkInfo.value.online, netCfg.value.socksProxy, subs, hasSess)
|
||||
val pref = remember { chatModel.controller.appPrefs.networkShowSubscriptionPercentage }
|
||||
val percentageText = "${(floor(statusColorAndPercentage.statusPercent * 100)).toInt()}%"
|
||||
val percentageText = if (subs.total > 0 || hasSess) "${(floor(statusColorAndPercentage.statusPercent * 100)).toInt()}%" else "%"
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ actual fun ReactionIcon(text: String, fontSize: TextUnit) {
|
||||
|
||||
@Composable
|
||||
actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = {
|
||||
ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = {
|
||||
val saveIfExists = {
|
||||
when (cItem.content.msgContent) {
|
||||
is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withLongRunningApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") }
|
||||
|
||||
Reference in new issue
Block a user