mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-20 12:22:23 +00:00
ui: open file after tap when it is received (#7700)
* ui: open file after tap when it is received * fix ios * simplify * simplify * better --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
co-authored by
Evgeny @ SimpleX Chat
parent
1bd7c95006
commit
7d870c6ff5
@@ -474,10 +474,27 @@ final class ChatModel: ObservableObject {
|
||||
// another in its place; only the badge alert shows regardless. Set while rendering, so not published.
|
||||
var chatListBanner: ChatListBanner?
|
||||
|
||||
var fileToOpen: (fileId: Int64, chatId: String?, open: (CIFile) -> Void)?
|
||||
|
||||
func bannerSlotFree(for banner: ChatListBanner) -> Bool {
|
||||
chatListBanner == nil || chatListBanner == banner
|
||||
}
|
||||
|
||||
func openWhenReceived(_ file: CIFile, open: @escaping (CIFile) -> Void) {
|
||||
fileToOpen = (fileId: file.fileId, chatId: chatId, open: open)
|
||||
}
|
||||
|
||||
func receivedFileToOpen(_ file: CIFile) -> Bool {
|
||||
if let f = fileToOpen { f.fileId == file.fileId && f.chatId == chatId } else { false }
|
||||
}
|
||||
|
||||
func openReceivedFile(_ file: CIFile) {
|
||||
if let f = fileToOpen, receivedFileToOpen(file) {
|
||||
fileToOpen = nil
|
||||
f.open(file)
|
||||
}
|
||||
}
|
||||
|
||||
static let shared = ChatModel()
|
||||
|
||||
let im = ItemsModel.shared
|
||||
|
||||
@@ -2873,6 +2873,9 @@ func processReceivedMsg(_ res: ChatEvent) async {
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileComplete(user, aChatItem):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
if active(user), let file = aChatItem.chatItem.file, m.receivedFileToOpen(file) {
|
||||
await MainActor.run { m.openReceivedFile(file) }
|
||||
}
|
||||
case let .rcvFileSndCancelled(user, aChatItem, _):
|
||||
await chatItemSimpleUpdate(user, aChatItem)
|
||||
Task { cleanupFile(aChatItem) }
|
||||
|
||||
@@ -26,7 +26,7 @@ struct CIFileView: View {
|
||||
var body: some View {
|
||||
if smallViewSize != nil {
|
||||
fileIndicator()
|
||||
.simultaneousGesture(TapGesture().onEnded(fileAction))
|
||||
.simultaneousGesture(TapGesture().onEnded { fileAction(file) })
|
||||
} else {
|
||||
// reserve exact space for the overlaid meta (timestamp + all icons), rendered transparently - matches MsgContentView
|
||||
let encrypted: Bool? = if let fileSource = file?.fileSource { fileSource.cryptoArgs != nil } else { nil }
|
||||
@@ -56,7 +56,7 @@ struct CIFileView: View {
|
||||
.padding(.bottom, 6)
|
||||
.padding(.leading, 10)
|
||||
.padding(.trailing, 12)
|
||||
.simultaneousGesture(TapGesture().onEnded(fileAction))
|
||||
.simultaneousGesture(TapGesture().onEnded { fileAction(file) })
|
||||
.disabled(!itemInteractive)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ struct CIFileView: View {
|
||||
return false
|
||||
}
|
||||
|
||||
private func fileAction() {
|
||||
private func fileAction(_ file: CIFile?) {
|
||||
logger.debug("CIFileView fileAction")
|
||||
if let file = file {
|
||||
switch (file.fileStatus) {
|
||||
@@ -93,6 +93,7 @@ struct CIFileView: View {
|
||||
if let prohibited = file.fileProhibited {
|
||||
showProhibitedFileAlert(file, prohibited)
|
||||
} else {
|
||||
m.openWhenReceived(file) { fileAction($0) }
|
||||
Task {
|
||||
logger.debug("CIFileView fileAction - in .rcvInvitation, .rcvAborted, in Task")
|
||||
if let user = m.currentUser {
|
||||
|
||||
@@ -54,6 +54,7 @@ struct CIImageView: View {
|
||||
if let prohibited = file.fileProhibited {
|
||||
showProhibitedFileAlert(file, prohibited)
|
||||
} else {
|
||||
m.openWhenReceived(file) { _ in showFullScreenImage = true }
|
||||
Task {
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user: user, fileId: file.fileId)
|
||||
|
||||
@@ -120,6 +120,25 @@ struct CIVideoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func playReceived(_ file: CIFile) {
|
||||
setupPlayer(file)
|
||||
if urlDecrypted == nil {
|
||||
decrypt(file: file, completed: playVideo)
|
||||
} else {
|
||||
playVideo()
|
||||
}
|
||||
}
|
||||
|
||||
private func playVideo() {
|
||||
if smallView {
|
||||
showFullScreenPlayer = urlDecrypted != nil
|
||||
} else if let urlDecrypted {
|
||||
m.stopPreviousRecPlay = urlDecrypted
|
||||
videoPlaying = true
|
||||
player?.play()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupPlayer(_ file: CIFile?) {
|
||||
let newUrl = getLoadedVideo(file)
|
||||
if newUrl == url {
|
||||
@@ -424,6 +443,7 @@ struct CIVideoView: View {
|
||||
if let prohibited = file.fileProhibited {
|
||||
showProhibitedFileAlert(file, prohibited)
|
||||
} else {
|
||||
m.openWhenReceived(file, open: playReceived)
|
||||
Task {
|
||||
if let user = m.currentUser {
|
||||
await receiveFile(user, file.fileId, false, false)
|
||||
|
||||
@@ -303,6 +303,11 @@ struct VoiceMessagePlayer: View {
|
||||
private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View {
|
||||
playPauseIcon(icon, theme.colors.primary)
|
||||
.simultaneousGesture(TapGesture().onEnded {
|
||||
chatModel.openWhenReceived(recordingFile) { file in
|
||||
if let recordingSource = getLoadedFileSource(file) {
|
||||
startPlayback(recordingSource)
|
||||
}
|
||||
}
|
||||
Task {
|
||||
if let user = chatModel.currentUser {
|
||||
await receiveFile(user: user, fileId: recordingFile.fileId)
|
||||
|
||||
+15
@@ -132,6 +132,8 @@ object BadgeModel {
|
||||
|
||||
enum class ChatListBanner { BadgeExpired, BadgeIssueFailed, BadgePitch, GetStake }
|
||||
|
||||
data class FileToOpen(val fileId: Long, val inChatList: Boolean)
|
||||
|
||||
/*
|
||||
* Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it
|
||||
* */
|
||||
@@ -205,8 +207,21 @@ object ChatModel {
|
||||
// another in its place; only the badge alert shows regardless. Set while rendering, so not a state.
|
||||
var chatListBanner: ChatListBanner? = null
|
||||
|
||||
var fileToOpen: FileToOpen? = null
|
||||
|
||||
fun bannerSlotFree(banner: ChatListBanner): Boolean = chatListBanner == null || chatListBanner == banner
|
||||
|
||||
fun openWhenReceived(file: CIFile, inChatList: Boolean) {
|
||||
fileToOpen = FileToOpen(file.fileId, inChatList)
|
||||
}
|
||||
|
||||
fun receivedFileToOpen(file: CIFile, inChatList: Boolean): Boolean {
|
||||
val f = fileToOpen
|
||||
if (f == null || f.fileId != file.fileId || f.inChatList != inChatList || !file.loaded) return false
|
||||
fileToOpen = null
|
||||
return !inChatList || appPlatform.isDesktop || chatId.value == null
|
||||
}
|
||||
|
||||
// preferences
|
||||
val notificationPreviewMode by lazy {
|
||||
mutableStateOf(
|
||||
|
||||
+17
-7
@@ -80,6 +80,7 @@ fun CIFileView(
|
||||
if (prohibited != null) {
|
||||
showProhibitedFileAlert(file, prohibited)
|
||||
} else {
|
||||
chatModel.openWhenReceived(file, smallView)
|
||||
receiveFile(file.fileId)
|
||||
}
|
||||
}
|
||||
@@ -188,16 +189,18 @@ fun CIFileView(
|
||||
val encrypted = file.fileSource.cryptoArgs != null
|
||||
SaveOrOpenFileMenu(showOpenSaveMenu, encrypted, ext, File(loadedFilePath).toURI(), file.fileSource, saveFile = { fileAction() })
|
||||
}
|
||||
val onClick: () -> Unit = {
|
||||
if (appPlatform.isAndroid && loadedFilePath != null) {
|
||||
showOpenSaveMenu.value = true
|
||||
} else {
|
||||
fileAction()
|
||||
}
|
||||
}
|
||||
if (file != null) OpenWhenReceivedEffect(file, smallView, onClick)
|
||||
Row(
|
||||
Modifier
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
if (appPlatform.isAndroid && loadedFilePath != null) {
|
||||
showOpenSaveMenu.value = true
|
||||
} else {
|
||||
fileAction()
|
||||
}
|
||||
},
|
||||
onClick = onClick,
|
||||
onLongClick = { showMenu.value = true }
|
||||
)
|
||||
.padding(if (smallView) PaddingValues() else PaddingValues(top = 4.sp.toDp(), bottom = 6.sp.toDp(), start = 6.sp.toDp(), end = 12.sp.toDp())),
|
||||
@@ -239,6 +242,13 @@ fun CIFileView(
|
||||
// the core decides whether a received file is above the size the sender's badge allows
|
||||
fun fileSizeValid(file: CIFile): Boolean = file.fileProhibited == null
|
||||
|
||||
@Composable
|
||||
fun OpenWhenReceivedEffect(file: CIFile, inChatList: Boolean, open: () -> Unit) {
|
||||
KeyChangeEffect(file.loaded) {
|
||||
if (chatModel.receivedFileToOpen(file, inChatList)) open()
|
||||
}
|
||||
}
|
||||
|
||||
fun showProhibitedFileAlert(file: CIFile, prohibited: FileProhibited) {
|
||||
val badgeIssue = when (prohibited.badgeStatus) {
|
||||
null, BadgeStatus.Active -> ""
|
||||
|
||||
+10
@@ -173,6 +173,15 @@ fun CIImageView(
|
||||
return null
|
||||
}
|
||||
|
||||
val view = LocalMultiplatformView()
|
||||
if (file != null) {
|
||||
OpenWhenReceivedEffect(file, smallView) {
|
||||
hideKeyboard(view)
|
||||
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(imageProvider, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID)
|
||||
.then(
|
||||
@@ -222,6 +231,7 @@ fun CIImageView(
|
||||
if (file.fileProhibited != null) {
|
||||
showProhibitedFileAlert(file, file.fileProhibited)
|
||||
} else {
|
||||
chatModel.openWhenReceived(file, smallView)
|
||||
receiveFile(file.fileId)
|
||||
}
|
||||
file.fileStatus is CIFileStatus.RcvAccepted ->
|
||||
|
||||
+38
-28
@@ -70,6 +70,8 @@ fun CIVideoView(
|
||||
}
|
||||
}
|
||||
val f = filePath.value
|
||||
val autoPlay = remember { mutableStateOf(false) }
|
||||
if (file != null) OpenWhenReceivedEffect(file, smallView) { autoPlay.value = true }
|
||||
if (file != null && f != null) {
|
||||
val view = LocalMultiplatformView()
|
||||
val openFullscreen = {
|
||||
@@ -80,7 +82,6 @@ fun CIVideoView(
|
||||
}
|
||||
|
||||
val uri = remember(filePath) { getAppFileUri(f.substringAfterLast(File.separator)) }
|
||||
val autoPlay = remember { mutableStateOf(false) }
|
||||
val uriDecrypted = remember(filePath) { mutableStateOf(if (file.fileSource?.cryptoArgs == null) uri else file.fileSource.decryptedGet()) }
|
||||
val decrypted = uriDecrypted.value
|
||||
if (decrypted != null && smallView) {
|
||||
@@ -98,7 +99,7 @@ fun CIVideoView(
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
|
||||
receiveFileIfValidSize(file, receiveFile)
|
||||
receiveFileIfValidSize(file, smallView, receiveFile)
|
||||
CIFileStatus.RcvAccepted ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP ->
|
||||
@@ -128,7 +129,7 @@ fun CIVideoView(
|
||||
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
||||
}
|
||||
if (showDownloadButton(file?.fileStatus) && !blurred.value && file != null) {
|
||||
PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
|
||||
PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, smallView, receiveFile) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,22 +157,26 @@ private fun VideoViewEncrypted(
|
||||
) {
|
||||
var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) }
|
||||
val onLongClick = { showMenu.value = true }
|
||||
val play: () -> Unit = {
|
||||
decryptionInProgress = true
|
||||
withBGApi {
|
||||
try {
|
||||
uriUnencrypted.value = file.fileSource?.decryptedGetOrCreate()
|
||||
autoPlay.value = uriUnencrypted.value != null
|
||||
} finally {
|
||||
decryptionInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(autoPlay.value) {
|
||||
if (autoPlay.value) play()
|
||||
}
|
||||
Box {
|
||||
VideoPreviewImageView(defaultPreview, smallView = false, blurred = blurred, if (decryptionInProgress) {{}} else openFullscreen, onLongClick)
|
||||
if (decryptionInProgress) {
|
||||
VideoDecryptionProgress(1f, onLongClick = onLongClick)
|
||||
} else if (!blurred.value) {
|
||||
PlayButton(false, 1f, onLongClick = onLongClick) {
|
||||
decryptionInProgress = true
|
||||
withBGApi {
|
||||
try {
|
||||
uriUnencrypted.value = file.fileSource?.decryptedGetOrCreate()
|
||||
autoPlay.value = uriUnencrypted.value != null
|
||||
} finally {
|
||||
decryptionInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
PlayButton(false, 1f, onLongClick = onLongClick, onClick = play)
|
||||
}
|
||||
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(defaultDuration) }, remember { mutableStateOf(0L) })
|
||||
}
|
||||
@@ -190,22 +195,26 @@ private fun SmallVideoViewEncrypted(
|
||||
) {
|
||||
var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) }
|
||||
val onLongClick = { showMenu.value = true }
|
||||
val play: () -> Unit = {
|
||||
decryptionInProgress = true
|
||||
withBGApi {
|
||||
try {
|
||||
uriUnencrypted.value = file.fileSource?.decryptedGetOrCreate()
|
||||
autoPlay.value = uriUnencrypted.value != null
|
||||
} finally {
|
||||
decryptionInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(autoPlay.value) {
|
||||
if (autoPlay.value) play()
|
||||
}
|
||||
Box {
|
||||
VideoPreviewImageView(defaultPreview, smallView = true, blurred = blurred, onClick = if (decryptionInProgress) {{}} else openFullscreen, onLongClick = onLongClick)
|
||||
if (decryptionInProgress) {
|
||||
VideoDecryptionProgress(sizeMultiplier, onLongClick = onLongClick)
|
||||
} else if (!file.showStatusIconInSmallView && !blurHidesMedia(true, blurred)) {
|
||||
PlayButton(false, sizeMultiplier, onLongClick = onLongClick) {
|
||||
decryptionInProgress = true
|
||||
withBGApi {
|
||||
try {
|
||||
uriUnencrypted.value = file.fileSource?.decryptedGetOrCreate()
|
||||
autoPlay.value = uriUnencrypted.value != null
|
||||
} finally {
|
||||
decryptionInProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
PlayButton(false, sizeMultiplier, onLongClick = onLongClick, onClick = play)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,7 +249,7 @@ private fun SmallVideoView(
|
||||
PlayButton(brokenVideo, sizeMultiplier, onLongClick = {}, onClick = openFullscreen)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(uri) {
|
||||
LaunchedEffect(uri, autoPlay.value) {
|
||||
if (autoPlay.value) openFullscreen()
|
||||
}
|
||||
}
|
||||
@@ -272,7 +281,7 @@ private fun VideoView(
|
||||
player.stop()
|
||||
}
|
||||
val showPreview = remember { derivedStateOf { !videoPlaying.value || progress.value == 0L } }
|
||||
LaunchedEffect(uri) {
|
||||
LaunchedEffect(uri, autoPlay.value) {
|
||||
if (autoPlay.value) play()
|
||||
}
|
||||
// Drop autoPlay only when show preview changes to prevent blinking of the view
|
||||
@@ -566,10 +575,11 @@ private fun fileStatusIcon(file: CIFile?, smallView: Boolean) {
|
||||
private fun showDownloadButton(status: CIFileStatus?): Boolean =
|
||||
status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted
|
||||
|
||||
private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
|
||||
private fun receiveFileIfValidSize(file: CIFile, smallView: Boolean, receiveFile: (Long) -> Unit) {
|
||||
if (file.fileProhibited != null) {
|
||||
showProhibitedFileAlert(file, file.fileProhibited)
|
||||
} else {
|
||||
chatModel.openWhenReceived(file, smallView)
|
||||
receiveFile(file.fileId)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -87,7 +87,7 @@ fun CIVoiceView(
|
||||
durationText(time / 1000)
|
||||
}
|
||||
}
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, showTimestamp, sizeMultiplier, play, pause, longClick, receiveFile) {
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, showTimestamp, sizeMultiplier, play, pause, longClick, { chatModel.openWhenReceived(file, smallView); receiveFile(it) }) {
|
||||
AudioPlayer.seekTo(it, progress, fileSource.value?.filePath)
|
||||
}
|
||||
if (smallView) {
|
||||
@@ -95,6 +95,7 @@ fun CIVoiceView(
|
||||
AudioPlayer.stop()
|
||||
}
|
||||
}
|
||||
OpenWhenReceivedEffect(file, smallView, play)
|
||||
} else if (smallView) {
|
||||
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, sizeMultiplier, {}, {}, longClick, receiveFile)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user