Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ plugins {

val kotlinVersion: String by rootProject.extra

val androidxCameraVersion = "1.6.1"
val coilKtVersion = "2.7.0"
val daggerVersion = "2.59.2"
val emojiVersion = "1.6.0"
Expand Down Expand Up @@ -232,10 +231,6 @@ dependencies {
implementation("org.conscrypt:conscrypt-android:2.5.3")
implementation("com.github.nextcloud-deps:qrcodescanner:0.1.2.4") // "com.github.blikoon:QRCodeScanner:0.1.2"

implementation("androidx.camera:camera-core:$androidxCameraVersion")
implementation("androidx.camera:camera-camera2:$androidxCameraVersion")
implementation("androidx.camera:camera-lifecycle:$androidxCameraVersion")
implementation("androidx.camera:camera-view:$androidxCameraVersion")
implementation("androidx.exifinterface:exifinterface:1.4.2")

implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
Expand Down Expand Up @@ -302,6 +297,8 @@ dependencies {

implementation("androidx.media3:media3-exoplayer:$media3Version")
implementation("androidx.media3:media3-ui:$media3Version")
implementation("androidx.media3:media3-transformer:$media3Version")
implementation("androidx.media3:media3-effect:$media3Version")

implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("pl.droidsonroids.gif:android-gif-drawable:1.2.32")
Expand Down
5 changes: 0 additions & 5 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,6 @@
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/FullScreenTextTheme" />

<activity
android:name=".activities.TakePhotoActivity"
android:theme="@style/TakePhotoTheme"
android:windowSoftInputMode="stateHidden" />

<activity
android:name=".shareditems.activities.SharedItemsActivity"
android:theme="@style/AppTheme" />
Expand Down
427 changes: 0 additions & 427 deletions app/src/main/java/com/nextcloud/talk/activities/TakePhotoActivity.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.attachmentpreview

import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.nextcloud.talk.utils.FileUtils
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

private enum class CameraCaptureType { PHOTO, VIDEO }

private const val CAMERA_FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss"

internal data class CameraCaptureActions(val onTakePhoto: () -> Unit, val onTakeVideo: () -> Unit)

@Composable
internal fun rememberCameraCaptureActions(currentFiles: MutableList<String>): CameraCaptureActions {
val context = LocalContext.current
var pendingCameraUri by remember { mutableStateOf<Uri?>(null) }
var pendingCameraPermissionType by remember { mutableStateOf<CameraCaptureType?>(null) }

val onCaptureResult: (Boolean) -> Unit = { success ->
val uri = pendingCameraUri
if (success && uri != null) {
currentFiles.add(uri.toString())
}
pendingCameraUri = null
}

val takePicture = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture(), onCaptureResult)
val captureVideo = rememberLauncherForActivityResult(ActivityResultContracts.CaptureVideo(), onCaptureResult)

fun launchCameraCapture(type: CameraCaptureType) {
val uri = createCameraOutputUri(context, type)
pendingCameraUri = uri
when (type) {
CameraCaptureType.PHOTO -> takePicture.launch(uri)
CameraCaptureType.VIDEO -> captureVideo.launch(uri)
}
}

val cameraPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
val type = pendingCameraPermissionType
pendingCameraPermissionType = null
if (granted && type != null) {
launchCameraCapture(type)
}
}

fun requestCameraCapture(type: CameraCaptureType) {
val hasPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED
if (hasPermission) {
launchCameraCapture(type)
} else {
pendingCameraPermissionType = type
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}

return CameraCaptureActions(
onTakePhoto = { requestCameraCapture(CameraCaptureType.PHOTO) },
onTakeVideo = { requestCameraCapture(CameraCaptureType.VIDEO) }
)
}

private fun createCameraOutputUri(context: Context, type: CameraCaptureType): Uri {
val outputDir = FileUtils.getSharedAttachmentsDirectory(context.cacheDir) ?: context.cacheDir
val timestamp = SimpleDateFormat(CAMERA_FILE_DATE_PATTERN, Locale.ROOT).format(Date())
val extension = if (type == CameraCaptureType.PHOTO) "jpg" else "mp4"
val file = File(outputDir, "$timestamp.$extension")
return FileProvider.getUriForFile(context, context.packageName, file)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.attachmentpreview

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.nextcloud.talk.R

/** The bottom bar where the user types an optional caption and triggers sending. */
@Composable
internal fun CaptionInputBar(
caption: String,
onCaptionChange: (String) -> Unit,
sendEnabled: Boolean,
onSend: () -> Unit
) {
val shape = RoundedCornerShape(dimensionResource(R.dimen.button_corner_radius))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.clip(shape)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
Box(
modifier = Modifier
.weight(1f)
.heightIn(min = dimensionResource(R.dimen.min_size_clickable_area))
.padding(start = 16.dp),
contentAlignment = Alignment.CenterStart
) {
if (caption.isEmpty()) {
Text(
text = stringResource(R.string.nc_caption),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
BasicTextField(
value = caption,
onValueChange = onCaptionChange,
maxLines = 5,
textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
modifier = Modifier.fillMaxWidth()
)
}

val sendTint = if (sendEnabled) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
IconButton(onClick = onSend, enabled = sendEnabled) {
Icon(
painter = painterResource(R.drawable.ic_send_24px),
contentDescription = stringResource(R.string.nc_description_send_message_button),
tint = sendTint
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2023 Julius Linus <julius.linus@nextcloud.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.attachmentpreview

import android.app.Dialog
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.ui.theme.ViewThemeUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import javax.inject.Inject

@AutoInjector(NextcloudTalkApplication::class)
class FileAttachmentPreviewFragment : DialogFragment() {
private lateinit var filesList: ArrayList<String>
private var conversationName: String = ""
private var uploadFiles: (files: MutableList<String>, caption: String, compressImages: Boolean) -> Unit =
{ _, _, _ -> }
private var composeView: ComposeView? = null

@Inject
lateinit var viewThemeUtils: ViewThemeUtils

@Inject
lateinit var appPreferences: AppPreferences

@Inject
lateinit var viewModelFactory: ViewModelProvider.Factory

private val viewModel: FileAttachmentPreviewViewModel by lazy {
ViewModelProvider(this, viewModelFactory)[FileAttachmentPreviewViewModel::class.java]
}

fun setListener(uploadFiles: (files: MutableList<String>, caption: String, compressImages: Boolean) -> Unit) {
this.uploadFiles = uploadFiles
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
arguments?.let {
filesList = it.getStringArrayList(FILES_TO_UPLOAD_ARG)!!
conversationName = it.getString(CONVERSATION_NAME_ARG, "")
}

composeView = ComposeView(requireContext())
return MaterialAlertDialogBuilder(requireContext()).setView(composeView).create()
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
composeView

@Suppress("DEPRECATION")
override fun onStart() {
super.onStart()
dialog?.window?.apply {
setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT)
setBackgroundDrawableResource(android.R.color.transparent)
setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
WindowCompat.setDecorFitsSystemWindows(this, false)
statusBarColor = Color.TRANSPARENT
navigationBarColor = Color.TRANSPARENT

val surfaceColor = viewThemeUtils.getColorScheme(requireActivity()).surface
val isLightSurface = surfaceColor.luminance() > LIGHT_LUMINANCE_THRESHOLD
WindowInsetsControllerCompat(this, decorView).apply {
isAppearanceLightStatusBars = isLightSurface
isAppearanceLightNavigationBars = isLightSurface
}
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)

viewModel.setInitialFiles(filesList)

composeView?.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme(colorScheme = viewThemeUtils.getColorScheme(requireActivity())) {
FileAttachmentPreviewContent(
viewModel = viewModel,
conversationName = conversationName,
initialCompressImages = appPreferences.compressUploadImages,
onDismiss = { dismiss() },
onSend = { files, caption, compressImages ->
uploadFiles(files.toMutableList(), caption, compressImages)
dismiss()
}
)
}
}
}
}

override fun onDestroyView() {
super.onDestroyView()
composeView = null
}

companion object {

private const val LIGHT_LUMINANCE_THRESHOLD = 0.5f
private const val FILES_TO_UPLOAD_ARG = "FILES_TO_UPLOAD_ARG"
private const val CONVERSATION_NAME_ARG = "CONVERSATION_NAME_ARG"

@JvmStatic
fun newInstance(filesToUpload: MutableList<String>, conversationName: String): FileAttachmentPreviewFragment {
val fileAttachmentFragment = FileAttachmentPreviewFragment()
val args = Bundle()
args.putStringArrayList(FILES_TO_UPLOAD_ARG, ArrayList(filesToUpload))
args.putString(CONVERSATION_NAME_ARG, conversationName)
fileAttachmentFragment.arguments = args
return fileAttachmentFragment
}

val TAG: String = FileAttachmentPreviewFragment::class.java.simpleName
}
}
Loading
Loading