449 lines
18 KiB
Kotlin
449 lines
18 KiB
Kotlin
package chat.stoat.activities
|
|
|
|
import android.content.Intent
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import android.os.Parcelable
|
|
import android.util.Log
|
|
import android.widget.Toast
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.compose.BackHandler
|
|
import androidx.activity.compose.setContent
|
|
import androidx.compose.animation.AnimatedVisibility
|
|
import androidx.compose.foundation.background
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.Spacer
|
|
import androidx.compose.foundation.layout.fillMaxHeight
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.height
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.layout.size
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.material3.CircularProgressIndicator
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.OutlinedTextField
|
|
import androidx.compose.material3.Scaffold
|
|
import androidx.compose.material3.Surface
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.TopAppBar
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableFloatStateOf
|
|
import androidx.compose.runtime.mutableStateListOf
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.platform.LocalResources
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.core.view.WindowCompat
|
|
import androidx.documentfile.provider.DocumentFile
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.viewModelScope
|
|
import chat.stoat.R
|
|
import chat.stoat.api.StoatAPI
|
|
import chat.stoat.api.internals.ChannelUtils
|
|
import chat.stoat.api.routes.channel.sendMessage
|
|
import chat.stoat.api.routes.microservices.autumn.FileArgs
|
|
import chat.stoat.api.routes.microservices.autumn.MAX_ATTACHMENTS_PER_MESSAGE
|
|
import chat.stoat.api.routes.microservices.autumn.uploadToAutumn
|
|
import chat.stoat.api.settings.LoadedSettings
|
|
import chat.stoat.api.settings.SyncedSettings
|
|
import chat.stoat.composables.chat.MessageField
|
|
import chat.stoat.composables.emoji.EmojiPicker
|
|
import chat.stoat.composables.screens.chat.AttachmentManager
|
|
import chat.stoat.composables.screens.chat.drawer.ChannelItem
|
|
import chat.stoat.composables.screens.chat.drawer.ChannelItemIconType
|
|
import chat.stoat.composables.screens.chat.drawer.DMOrGroupItem
|
|
import chat.stoat.core.model.schemas.ChannelType
|
|
import chat.stoat.persistence.KVStorage
|
|
import chat.stoat.screens.chat.views.channel.ChannelScreenActivePane
|
|
import chat.stoat.ui.theme.StoatTheme
|
|
import io.ktor.http.ContentType
|
|
import kotlinx.coroutines.launch
|
|
import org.koin.androidx.compose.koinViewModel
|
|
import java.io.File
|
|
|
|
class ShareTargetActivity : ComponentActivity() {
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
val text: String? = intent.getStringExtra(Intent.EXTRA_TEXT)
|
|
val media: List<Uri?> = when (intent?.action) {
|
|
// We receive one of something. Could be text, could be media.
|
|
Intent.ACTION_SEND -> {
|
|
when {
|
|
// No media if we receive text/plain
|
|
intent.type == "text/plain" -> {
|
|
listOf()
|
|
}
|
|
|
|
// Otherwise, we receive a single Uri
|
|
else -> {
|
|
listOf(
|
|
when {
|
|
// due to a bug in Android 13 we still use the deprecated method on Android 13, despite the new method being available
|
|
Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU -> {
|
|
intent.getParcelableExtra(
|
|
Intent.EXTRA_STREAM,
|
|
Parcelable::class.java
|
|
) as? Uri
|
|
}
|
|
|
|
else -> {
|
|
@Suppress("DEPRECATION")
|
|
intent.getParcelableExtra(Intent.EXTRA_STREAM)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// We receive multiple URIs, definitely media
|
|
Intent.ACTION_SEND_MULTIPLE -> {
|
|
try {
|
|
val bundle: ArrayList<Uri>? = when {
|
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
|
|
@Suppress("UNCHECKED_CAST")
|
|
intent.getParcelableArrayListExtra(
|
|
Intent.EXTRA_STREAM,
|
|
Parcelable::class.java
|
|
) as? ArrayList<Uri>
|
|
}
|
|
|
|
else -> {
|
|
@Suppress("DEPRECATION")
|
|
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
|
|
}
|
|
}
|
|
|
|
bundle ?: listOf()
|
|
} catch (e: Exception) {
|
|
Log.e("ShareTargetActivity", "Failed to get multiple URIs", e)
|
|
listOf()
|
|
}
|
|
}
|
|
|
|
// We don't know what we're receiving
|
|
else -> {
|
|
Toast.makeText(
|
|
this,
|
|
getString(R.string.share_target_invalid_intent),
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
|
|
finish()
|
|
return
|
|
}
|
|
}
|
|
|
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
|
|
setContent {
|
|
ShareTargetScreen(
|
|
text = text,
|
|
media = media.filterNotNull(),
|
|
onFinished = { finish() }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
class ShareTargetScreenViewModel(
|
|
private val kvStorage: KVStorage,
|
|
) : ViewModel() {
|
|
var apiIsReady by mutableStateOf(false)
|
|
|
|
var messageContent by mutableStateOf("")
|
|
var attachments = mutableStateListOf<FileArgs>()
|
|
var attachmentsUploading by mutableStateOf(false)
|
|
var attachmentProgress by mutableFloatStateOf(0f)
|
|
var activeBottomPane by mutableStateOf<ChannelScreenActivePane>(ChannelScreenActivePane.None)
|
|
|
|
suspend fun isLoggedIn(): Boolean {
|
|
return kvStorage.get("sessionToken") != null
|
|
}
|
|
|
|
suspend fun initialiseAPI() {
|
|
if (!StoatAPI.isLoggedIn()) {
|
|
val token = kvStorage.get("sessionToken") ?: return
|
|
StoatAPI.loginAs(token)
|
|
StoatAPI.initialize()
|
|
}
|
|
apiIsReady = true
|
|
}
|
|
|
|
fun send(channelId: String, onFinished: () -> Unit) {
|
|
viewModelScope.launch {
|
|
val attachmentIds = arrayListOf<String>()
|
|
val takenAttachments = attachments.take(MAX_ATTACHMENTS_PER_MESSAGE)
|
|
val totalTaken = takenAttachments.size
|
|
|
|
takenAttachments.forEachIndexed { index, it ->
|
|
try {
|
|
val id = uploadToAutumn(
|
|
it.file,
|
|
it.filename,
|
|
"attachments",
|
|
ContentType.parse(it.contentType),
|
|
onProgress = { current, total ->
|
|
attachmentProgress =
|
|
((current.toFloat() / total.toFloat()) / totalTaken.toFloat()) + (index.toFloat() / totalTaken.toFloat())
|
|
}
|
|
)
|
|
attachmentIds.add(id)
|
|
} catch (e: Exception) {
|
|
return@launch
|
|
}
|
|
}
|
|
|
|
sendMessage(
|
|
channelId = channelId,
|
|
content = messageContent,
|
|
attachments = attachmentIds
|
|
)
|
|
|
|
onFinished()
|
|
}
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun ShareTargetScreen(
|
|
text: String?,
|
|
media: List<Uri>?,
|
|
onFinished: () -> Unit = {},
|
|
viewModel: ShareTargetScreenViewModel = koinViewModel()
|
|
) {
|
|
val context = LocalContext.current
|
|
val resources = LocalResources.current
|
|
|
|
LaunchedEffect(Unit) {
|
|
if (!viewModel.isLoggedIn()) {
|
|
Toast.makeText(
|
|
context,
|
|
resources.getString(R.string.share_target_login_first),
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
|
|
onFinished()
|
|
return@LaunchedEffect
|
|
}
|
|
|
|
viewModel.initialiseAPI()
|
|
}
|
|
|
|
LaunchedEffect(Unit) {
|
|
media?.forEach { uri ->
|
|
DocumentFile.fromSingleUri(context, uri)?.let { file ->
|
|
val mFile = File(context.cacheDir, file.name ?: "attachment")
|
|
|
|
mFile.outputStream().use { output ->
|
|
@Suppress("Recycle")
|
|
context.contentResolver.openInputStream(uri)?.copyTo(output)
|
|
}
|
|
|
|
viewModel.attachments.add(
|
|
FileArgs(
|
|
file = mFile,
|
|
contentType = file.type ?: "application/octet-stream",
|
|
filename = file.name ?: "attachment",
|
|
pickerIdentifier = null
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
text?.let {
|
|
viewModel.messageContent = it
|
|
}
|
|
}
|
|
|
|
var channelSearchContent by remember { mutableStateOf("") }
|
|
var selectedChannel by rememberSaveable { mutableStateOf<String?>(null) }
|
|
|
|
StoatTheme(
|
|
requestedTheme = LoadedSettings.theme,
|
|
requestedUserInterfaceFont = LoadedSettings.font,
|
|
colourOverrides = SyncedSettings.android.colourOverrides
|
|
) {
|
|
Scaffold(
|
|
topBar = {
|
|
TopAppBar(title = {
|
|
Text(text = stringResource(R.string.share))
|
|
})
|
|
}
|
|
) { pv ->
|
|
Surface(
|
|
modifier = Modifier
|
|
.padding(pv)
|
|
.background(MaterialTheme.colorScheme.background)
|
|
.fillMaxSize()
|
|
) {
|
|
if (!viewModel.apiIsReady) {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize(),
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
verticalArrangement = Arrangement.Center
|
|
) {
|
|
CircularProgressIndicator(
|
|
modifier = Modifier
|
|
.size(48.dp)
|
|
)
|
|
}
|
|
return@Surface
|
|
}
|
|
|
|
Column {
|
|
OutlinedTextField(
|
|
value = channelSearchContent,
|
|
onValueChange = {
|
|
channelSearchContent = it
|
|
},
|
|
label = {
|
|
Text(text = stringResource(R.string.share_target_search_channels))
|
|
},
|
|
modifier = Modifier
|
|
.padding(16.dp)
|
|
.fillMaxWidth()
|
|
)
|
|
|
|
Box(
|
|
modifier = Modifier.weight(1f)
|
|
) {
|
|
val filteredChannels = StoatAPI.channelCache.values.asSequence().filter {
|
|
it.name?.contains(
|
|
channelSearchContent,
|
|
ignoreCase = true
|
|
) == true
|
|
|| ChannelUtils.resolveName(it)
|
|
?.contains(
|
|
channelSearchContent,
|
|
ignoreCase = true
|
|
) == true
|
|
}
|
|
|
|
LazyColumn {
|
|
items(filteredChannels.count()) {
|
|
val channel = filteredChannels.elementAt(it)
|
|
|
|
when (channel.channelType) {
|
|
ChannelType.Group, ChannelType.DirectMessage -> DMOrGroupItem(
|
|
channel = channel,
|
|
partner = ChannelUtils.resolveDMPartner(channel)?.let { u ->
|
|
StoatAPI.userCache[u]
|
|
},
|
|
isCurrent = selectedChannel == channel.id,
|
|
hasUnread = false,
|
|
onDestinationChanged = { selectedChannel = channel.id },
|
|
onOpenChannelContextSheet = {}
|
|
)
|
|
|
|
else -> ChannelItem(
|
|
iconType = ChannelItemIconType.Channel(
|
|
channel.channelType ?: ChannelType.TextChannel
|
|
),
|
|
channel = channel,
|
|
isCurrent = selectedChannel == channel.id,
|
|
onDestinationChanged = { selectedChannel = channel.id },
|
|
onOpenChannelContextSheet = {},
|
|
appendServerName = true
|
|
)
|
|
}
|
|
|
|
Spacer(modifier = Modifier.height(8.dp))
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
AnimatedVisibility(viewModel.attachments.isNotEmpty()) {
|
|
AttachmentManager(
|
|
attachments = viewModel.attachments,
|
|
uploading = viewModel.attachmentsUploading,
|
|
uploadProgress = viewModel.attachmentProgress,
|
|
onToggleSpoiler = {
|
|
val index = viewModel.attachments
|
|
.indexOfFirst { a -> a.pickerIdentifier == it.pickerIdentifier }
|
|
if (index != -1) {
|
|
val attachment = viewModel.attachments[index]
|
|
viewModel.attachments[index] = attachment.copy(
|
|
spoiler = !attachment.spoiler
|
|
)
|
|
}
|
|
},
|
|
onRemove = {},
|
|
canRemove = false
|
|
)
|
|
}
|
|
|
|
MessageField(
|
|
initialValue = "",
|
|
onValueChange = { viewModel.messageContent = it },
|
|
canAttach = false,
|
|
forceSendButton = viewModel.attachments.isNotEmpty(),
|
|
disabled = viewModel.attachmentsUploading,
|
|
onAddAttachment = {},
|
|
onCommitAttachment = {},
|
|
onPickEmoji = {
|
|
if (viewModel.activeBottomPane is ChannelScreenActivePane.EmojiPicker) {
|
|
viewModel.activeBottomPane = ChannelScreenActivePane.None
|
|
} else {
|
|
viewModel.activeBottomPane = ChannelScreenActivePane.EmojiPicker
|
|
}
|
|
},
|
|
onSendMessage = {
|
|
if (selectedChannel == null) {
|
|
Toast.makeText(
|
|
context,
|
|
resources.getString(R.string.share_target_select_channel),
|
|
Toast.LENGTH_SHORT
|
|
).show()
|
|
return@MessageField
|
|
} else {
|
|
viewModel.send(selectedChannel!!) {
|
|
onFinished()
|
|
}
|
|
}
|
|
},
|
|
channelType = StoatAPI.channelCache[selectedChannel]?.channelType
|
|
?: ChannelType.TextChannel,
|
|
channelName = StoatAPI.channelCache[selectedChannel]?.name ?: "",
|
|
)
|
|
|
|
AnimatedVisibility(viewModel.activeBottomPane is ChannelScreenActivePane.EmojiPicker) {
|
|
BackHandler(enabled = viewModel.activeBottomPane == ChannelScreenActivePane.EmojiPicker) {
|
|
viewModel.activeBottomPane = ChannelScreenActivePane.None
|
|
}
|
|
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.fillMaxHeight(0.5f)
|
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
|
.padding(4.dp)
|
|
) {
|
|
EmojiPicker {
|
|
viewModel.messageContent += " $it"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |