mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 04:39:53 +00:00
mobile: increased size of voice messages (#2150)
* android: increased size of voice messages * ios: increased size of voice messages * size changes * increase audio quality to 16kHz/32Kbps, refactor auto-accept logic --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
parent
0ec2468dce
commit
baf3a12009
14 changed files with 69 additions and 66 deletions
|
@ -1355,12 +1355,12 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
|||
}
|
||||
val file = cItem.file
|
||||
val mc = cItem.content.msgContent
|
||||
if (file != null && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) {
|
||||
val acceptImages = appPrefs.privacyAcceptImages.get()
|
||||
if ((mc is MsgContent.MCImage && acceptImages)
|
||||
|| (mc is MsgContent.MCVoice && ((file.fileSize > MAX_VOICE_SIZE_FOR_SENDING && acceptImages) || cInfo is ChatInfo.Group))) {
|
||||
withApi { receiveFile(r.user, file.fileId) } // TODO check inlineFileMode != IFMSent
|
||||
}
|
||||
if (file != null &&
|
||||
appPrefs.privacyAcceptImages.get() &&
|
||||
((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|
||||
|| (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|
||||
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
|
||||
withApi { receiveFile(r.user, file.fileId) }
|
||||
}
|
||||
if (cItem.showNotification && (!SimplexApp.context.isAppOnForeground || chatModel.chatId.value != cInfo.id)) {
|
||||
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
|
||||
|
|
|
@ -307,7 +307,7 @@ private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>)
|
|||
|
||||
@Composable
|
||||
private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNextClick: MutableState<Boolean>) {
|
||||
val rec: Recorder = remember { RecorderNative(MAX_VOICE_SIZE_FOR_SENDING) }
|
||||
val rec: Recorder = remember { RecorderNative() }
|
||||
DisposableEffect(Unit) { onDispose { rec.stop() } }
|
||||
val stopRecordingAndAddAudio: () -> Unit = {
|
||||
recState.value.filePathNullable?.let {
|
||||
|
|
|
@ -36,6 +36,7 @@ fun CIVoiceView(
|
|||
ci: ChatItem,
|
||||
timedMessagesTTL: Int?,
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = if (hasText) 6.dp else 0.dp, end = if (hasText) 6.dp else 0.dp),
|
||||
|
@ -64,11 +65,11 @@ fun CIVoiceView(
|
|||
durationText(time / 1000)
|
||||
}
|
||||
}
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick) {
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) {
|
||||
AudioPlayer.seekTo(it, progress, filePath)
|
||||
}
|
||||
} else {
|
||||
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick)
|
||||
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick, receiveFile)
|
||||
val metaReserve = if (edited)
|
||||
" "
|
||||
else
|
||||
|
@ -93,8 +94,8 @@ private fun VoiceLayout(
|
|||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
onProgressChanged: (Int) -> Unit,
|
||||
|
||||
) {
|
||||
@Composable
|
||||
fun RowScope.Slider(backgroundColor: Color, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||
|
@ -142,7 +143,7 @@ private fun VoiceLayout(
|
|||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||
Spacer(Modifier.width(6.dp))
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
DurationText(text, PaddingValues(start = 12.dp))
|
||||
Slider(if (ci.chatDir.sent) sentColor else receivedColor)
|
||||
|
@ -156,7 +157,7 @@ private fun VoiceLayout(
|
|||
DurationText(text, PaddingValues(end = 12.dp))
|
||||
}
|
||||
Column {
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
|
@ -166,7 +167,7 @@ private fun VoiceLayout(
|
|||
else -> {
|
||||
Row {
|
||||
Column {
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
|
@ -245,7 +246,8 @@ private fun VoiceMsgIndicator(
|
|||
error: Boolean,
|
||||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
longClick: () -> Unit
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
) {
|
||||
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
|
||||
val strokeColor = MaterialTheme.colors.primary
|
||||
|
@ -264,8 +266,9 @@ private fun VoiceMsgIndicator(
|
|||
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
|
||||
}
|
||||
} else {
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation
|
||||
|| file?.fileStatus is CIFileStatus.RcvTransfer
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
|
||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick)
|
||||
} else if (file?.fileStatus is CIFileStatus.RcvTransfer
|
||||
|| file?.fileStatus is CIFileStatus.RcvAccepted
|
||||
) {
|
||||
Box(
|
||||
|
|
|
@ -203,7 +203,7 @@ fun ChatItemView(
|
|||
EmojiItemView(cItem, cInfo.timedMessagesTTL)
|
||||
MsgContentItemDropdownMenu()
|
||||
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") })
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") }, receiveFile)
|
||||
MsgContentItemDropdownMenu()
|
||||
} else {
|
||||
framedItemView()
|
||||
|
|
|
@ -221,7 +221,7 @@ fun FramedItemView(
|
|||
}
|
||||
}
|
||||
is MsgContent.MCVoice -> {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") })
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile)
|
||||
if (mc.text != "") {
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ interface Recorder {
|
|||
fun stop(): Int
|
||||
}
|
||||
|
||||
class RecorderNative(private val recordedBytesLimit: Long): Recorder {
|
||||
class RecorderNative(): Recorder {
|
||||
companion object {
|
||||
// Allows to stop the recorder from outside without having the recorder in a variable
|
||||
var stopRecording: (() -> Unit)? = null
|
||||
|
@ -48,9 +48,8 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
|
|||
rec.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
|
||||
rec.setAudioChannels(1)
|
||||
rec.setAudioSamplingRate(16000)
|
||||
rec.setAudioEncodingBitRate(16000)
|
||||
rec.setAudioEncodingBitRate(32000)
|
||||
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
|
||||
rec.setMaxFileSize(recordedBytesLimit)
|
||||
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
|
||||
val fileToSave = File.createTempFile(generateNewFileName(SimplexApp.context, "voice", "${extension}_"), ".tmp", tmpDir)
|
||||
fileToSave.deleteOnExit()
|
||||
|
|
|
@ -236,16 +236,16 @@ private fun spannableStringToAnnotatedString(
|
|||
}
|
||||
|
||||
// maximum image file size to be auto-accepted
|
||||
const val MAX_IMAGE_SIZE: Long = 236700
|
||||
const val MAX_IMAGE_SIZE: Long = 261_120 // 255KB
|
||||
const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
|
||||
const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE
|
||||
const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
|
||||
const val MAX_VIDEO_SIZE_AUTO_RCV: Long = 1_047_552 // 1023KB
|
||||
|
||||
const val MAX_VOICE_SIZE_FOR_SENDING: Long = 94680 // 6 chunks * 15780 bytes per chunk
|
||||
const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 43_000
|
||||
const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 300_000
|
||||
|
||||
const val MAX_FILE_SIZE_SMP: Long = 8000000
|
||||
|
||||
const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824
|
||||
const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB
|
||||
|
||||
fun getFilesDirectory(context: Context): String {
|
||||
return context.filesDir.toString()
|
||||
|
|
|
@ -36,8 +36,9 @@ class AudioRecorder {
|
|||
try av.setActive(true)
|
||||
let settings: [String : Any] = [
|
||||
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
||||
AVSampleRateKey: 12000,
|
||||
AVEncoderBitRateKey: 12000,
|
||||
AVSampleRateKey: 16000,
|
||||
AVEncoderBitRateKey: 32000,
|
||||
AVEncoderBitRateStrategyKey: AVAudioBitRateStrategy_VariableConstrained,
|
||||
AVNumberOfChannelsKey: 1
|
||||
]
|
||||
let url = getAppFilePath(fileName)
|
||||
|
|
|
@ -1240,15 +1240,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
|
||||
m.increaseUnreadCounter(user: user)
|
||||
}
|
||||
if let file = cItem.file,
|
||||
let mc = cItem.content.msgContent,
|
||||
file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV {
|
||||
let acceptImages = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
if (mc.isImage && acceptImages)
|
||||
|| (mc.isVoice && ((file.fileSize > MAX_VOICE_MESSAGE_SIZE_INLINE_SEND && acceptImages) || cInfo.chatType == .group)) {
|
||||
Task {
|
||||
await receiveFile(user: user, fileId: file.fileId) // TODO check inlineFileMode != IFMSent
|
||||
}
|
||||
if let file = cItem.autoReceiveFile() {
|
||||
Task {
|
||||
await receiveFile(user: user, fileId: file.fileId)
|
||||
}
|
||||
}
|
||||
if cItem.showNotification {
|
||||
|
|
|
@ -251,7 +251,6 @@ struct CIVideoView: View {
|
|||
if let user = ChatModel.shared.currentUser {
|
||||
await receiveFile(user, file.fileId)
|
||||
}
|
||||
// TODO image accepted alert?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -138,7 +138,7 @@ struct VoiceMessagePlayer: View {
|
|||
case .sndComplete: playbackButton()
|
||||
case .sndCancelled: playbackButton()
|
||||
case .sndError: playbackButton()
|
||||
case .rcvInvitation: loadingIcon()
|
||||
case .rcvInvitation: downloadButton(recordingFile)
|
||||
case .rcvAccepted: loadingIcon()
|
||||
case .rcvTransfer: loadingIcon()
|
||||
case .rcvComplete: playbackButton()
|
||||
|
@ -214,6 +214,18 @@ struct VoiceMessagePlayer: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func downloadButton(_ recordingFile: CIFile) -> some View {
|
||||
Button {
|
||||
Task {
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
await receiveFile(user: user, fileId: recordingFile.fileId)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
}
|
||||
}
|
||||
|
||||
private struct ProgressCircle: View {
|
||||
var length: TimeInterval
|
||||
@Binding var progress: TimeInterval?
|
||||
|
|
|
@ -271,25 +271,8 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
|||
if !cInfo.ntfsEnabled {
|
||||
ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1))
|
||||
}
|
||||
if case .image = cItem.content.msgContent {
|
||||
if let file = cItem.file,
|
||||
file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV,
|
||||
privacyAcceptImagesGroupDefault.get() {
|
||||
cItem = autoReceiveFile(file) ?? cItem
|
||||
}
|
||||
} else if case .video = cItem.content.msgContent {
|
||||
if let file = cItem.file,
|
||||
file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV,
|
||||
privacyAcceptImagesGroupDefault.get() {
|
||||
cItem = autoReceiveFile(file) ?? cItem
|
||||
}
|
||||
} else if case .voice = cItem.content.msgContent { // TODO check inlineFileMode != IFMSent
|
||||
if let file = cItem.file,
|
||||
file.fileSize <= MAX_IMAGE_SIZE,
|
||||
file.fileSize > MAX_VOICE_MESSAGE_SIZE_INLINE_SEND,
|
||||
privacyAcceptImagesGroupDefault.get() {
|
||||
cItem = autoReceiveFile(file) ?? cItem
|
||||
}
|
||||
if let file = cItem.autoReceiveFile() {
|
||||
cItem = autoReceiveFile(file) ?? cItem
|
||||
}
|
||||
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(notification: createMessageReceivedNtf(user, cInfo, cItem)) : .empty
|
||||
return cItem.showMutableNotification ? (aChatItem.chatId, ntf) : nil
|
||||
|
|
|
@ -1851,6 +1851,18 @@ public struct ChatItem: Identifiable, Decodable {
|
|||
}
|
||||
}
|
||||
|
||||
public func autoReceiveFile() -> CIFile? {
|
||||
if let file = file,
|
||||
let mc = content.msgContent,
|
||||
privacyAcceptImagesGroupDefault.get(),
|
||||
(mc.isImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|
||||
|| (mc.isVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|
||||
|| (mc.isVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus != .rcvAccepted) {
|
||||
return file
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public var showMutableNotification: Bool {
|
||||
switch content {
|
||||
case .rcvCall: return false
|
||||
|
@ -2383,7 +2395,7 @@ public enum FileProtocol: String, Decodable {
|
|||
case xftp = "xftp"
|
||||
}
|
||||
|
||||
public enum CIFileStatus: Decodable {
|
||||
public enum CIFileStatus: Decodable, Equatable {
|
||||
case sndStored
|
||||
case sndTransfer(sndProgress: Int64, sndTotal: Int64)
|
||||
case sndComplete
|
||||
|
|
|
@ -11,20 +11,20 @@ import OSLog
|
|||
|
||||
let logger = Logger()
|
||||
|
||||
// maximum image file size to be auto-accepted
|
||||
public let MAX_IMAGE_SIZE: Int64 = 236700
|
||||
// image file size for complession
|
||||
public let MAX_IMAGE_SIZE: Int64 = 261_120 // 255KB
|
||||
|
||||
public let MAX_IMAGE_SIZE_AUTO_RCV: Int64 = MAX_IMAGE_SIZE * 2
|
||||
|
||||
public let MAX_VIDEO_SIZE_AUTO_RCV: Int64 = 8000000
|
||||
public let MAX_VOICE_SIZE_AUTO_RCV: Int64 = MAX_IMAGE_SIZE * 2
|
||||
|
||||
public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824
|
||||
public let MAX_VIDEO_SIZE_AUTO_RCV: Int64 = 1_047_552 // 1023KB
|
||||
|
||||
public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824 // 1GB
|
||||
|
||||
public let MAX_FILE_SIZE_SMP: Int64 = 8000000
|
||||
|
||||
public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(30)
|
||||
|
||||
public let MAX_VOICE_MESSAGE_SIZE_INLINE_SEND: Int64 = 94680
|
||||
public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(300)
|
||||
|
||||
private let CHAT_DB: String = "_chat.db"
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue