feat: make revolt a share target (rough around edges)
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
35dc87ed7e
commit
1bf9610d7d
|
|
@ -86,6 +86,22 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activities.ShareTargetActivity"
|
||||||
|
android:theme="@style/Theme.Revolt"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="*/*" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.media.ImageViewActivity"
|
android:name=".activities.media.ImageViewActivity"
|
||||||
android:theme="@style/Theme.Revolt" />
|
android:theme="@style/Theme.Revolt" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,461 @@
|
||||||
|
package chat.revolt.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.material3.surfaceColorAtElevation
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import chat.revolt.R
|
||||||
|
import chat.revolt.api.RevoltAPI
|
||||||
|
import chat.revolt.api.internals.ChannelUtils
|
||||||
|
import chat.revolt.api.routes.channel.sendMessage
|
||||||
|
import chat.revolt.api.routes.microservices.autumn.FileArgs
|
||||||
|
import chat.revolt.api.routes.microservices.autumn.MAX_ATTACHMENTS_PER_MESSAGE
|
||||||
|
import chat.revolt.api.routes.microservices.autumn.uploadToAutumn
|
||||||
|
import chat.revolt.api.schemas.ChannelType
|
||||||
|
import chat.revolt.api.settings.GlobalState
|
||||||
|
import chat.revolt.api.settings.SyncedSettings
|
||||||
|
import chat.revolt.components.chat.NativeMessageField
|
||||||
|
import chat.revolt.components.emoji.EmojiPicker
|
||||||
|
import chat.revolt.components.generic.presenceFromStatus
|
||||||
|
import chat.revolt.components.screens.chat.AttachmentManager
|
||||||
|
import chat.revolt.components.screens.chat.drawer.server.DrawerChannel
|
||||||
|
import chat.revolt.components.screens.chat.drawer.server.DrawerChannelIconType
|
||||||
|
import chat.revolt.persistence.KVStorage
|
||||||
|
import chat.revolt.screens.chat.views.channel.BottomPane
|
||||||
|
import chat.revolt.ui.theme.RevoltTheme
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
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 {
|
||||||
|
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() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ShareTargetScreenViewModel @Inject constructor(
|
||||||
|
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<BottomPane>(BottomPane.None)
|
||||||
|
|
||||||
|
suspend fun isLoggedIn(): Boolean {
|
||||||
|
return kvStorage.get("sessionToken") != null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun initialiseAPI() {
|
||||||
|
if (!RevoltAPI.isLoggedIn()) {
|
||||||
|
val token = kvStorage.get("sessionToken") ?: return
|
||||||
|
RevoltAPI.loginAs(token)
|
||||||
|
RevoltAPI.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 = hiltViewModel()
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (!viewModel.isLoggedIn()) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.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) }
|
||||||
|
|
||||||
|
RevoltTheme(
|
||||||
|
requestedTheme = GlobalState.theme,
|
||||||
|
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 = RevoltAPI.channelCache.values.asSequence().filter {
|
||||||
|
it.name?.contains(
|
||||||
|
channelSearchContent,
|
||||||
|
ignoreCase = true
|
||||||
|
) == true
|
||||||
|
|| ChannelUtils.resolveDMName(it)
|
||||||
|
?.contains(
|
||||||
|
channelSearchContent,
|
||||||
|
ignoreCase = true
|
||||||
|
) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn {
|
||||||
|
items(filteredChannels.count()) {
|
||||||
|
val channel = filteredChannels.elementAt(it)
|
||||||
|
|
||||||
|
DrawerChannel(
|
||||||
|
iconType = DrawerChannelIconType.Channel(
|
||||||
|
channel.channelType ?: ChannelType.TextChannel
|
||||||
|
),
|
||||||
|
name = (if (channel.server != null) "${channel.name} (${RevoltAPI.serverCache[channel.server]?.name})" else channel.name)
|
||||||
|
?: ChannelUtils.resolveDMName(channel)
|
||||||
|
?: stringResource(R.string.unknown),
|
||||||
|
selected = selectedChannel == channel.id,
|
||||||
|
hasUnread = false,
|
||||||
|
onClick = {
|
||||||
|
selectedChannel = channel.id
|
||||||
|
},
|
||||||
|
dmPartnerIcon = ChannelUtils.resolveDMPartner(
|
||||||
|
channel
|
||||||
|
)?.let { u -> RevoltAPI.userCache[u] }?.avatar,
|
||||||
|
dmPartnerName = ChannelUtils.resolveDMName(
|
||||||
|
channel
|
||||||
|
),
|
||||||
|
dmPartnerStatus = ChannelUtils.resolveDMPartner(
|
||||||
|
channel
|
||||||
|
)
|
||||||
|
?.let { u -> RevoltAPI.userCache[u] }?.status?.presence?.let { p ->
|
||||||
|
presenceFromStatus(
|
||||||
|
p,
|
||||||
|
RevoltAPI.userCache[ChannelUtils.resolveDMPartner(
|
||||||
|
channel
|
||||||
|
)]?.online ?: false
|
||||||
|
)
|
||||||
|
},
|
||||||
|
dmPartnerId = ChannelUtils.resolveDMPartner(
|
||||||
|
channel
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
bottom = 16.dp,
|
||||||
|
top = 8.dp
|
||||||
|
)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(viewModel.attachments.isNotEmpty()) {
|
||||||
|
AttachmentManager(
|
||||||
|
attachments = viewModel.attachments,
|
||||||
|
uploading = viewModel.attachmentsUploading,
|
||||||
|
uploadProgress = viewModel.attachmentProgress,
|
||||||
|
onRemove = {},
|
||||||
|
canRemove = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeMessageField(
|
||||||
|
value = viewModel.messageContent,
|
||||||
|
onValueChange = { viewModel.messageContent = it },
|
||||||
|
canAttach = false,
|
||||||
|
forceSendButton = viewModel.attachments.isNotEmpty(),
|
||||||
|
disabled = viewModel.attachmentsUploading,
|
||||||
|
onAddAttachment = {},
|
||||||
|
onCommitAttachment = {},
|
||||||
|
onPickEmoji = {
|
||||||
|
if (viewModel.activeBottomPane is BottomPane.EmojiPicker) {
|
||||||
|
viewModel.activeBottomPane = BottomPane.None
|
||||||
|
} else {
|
||||||
|
viewModel.activeBottomPane = BottomPane.EmojiPicker
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSendMessage = {
|
||||||
|
if (selectedChannel == null) {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.share_target_select_channel),
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
return@NativeMessageField
|
||||||
|
} else {
|
||||||
|
viewModel.send(selectedChannel!!) {
|
||||||
|
onFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
channelType = RevoltAPI.channelCache[selectedChannel]?.channelType
|
||||||
|
?: ChannelType.TextChannel,
|
||||||
|
channelName = RevoltAPI.channelCache[selectedChannel]?.name ?: "",
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(viewModel.activeBottomPane is BottomPane.EmojiPicker) {
|
||||||
|
BackHandler(enabled = viewModel.activeBottomPane == BottomPane.EmojiPicker) {
|
||||||
|
viewModel.activeBottomPane = BottomPane.None
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fillMaxHeight(0.5f)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
|
||||||
|
.padding(4.dp)
|
||||||
|
) {
|
||||||
|
EmojiPicker {
|
||||||
|
viewModel.messageContent += " $it"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ object ChannelUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resolveDMPartner(channel: Channel): String? {
|
fun resolveDMPartner(channel: Channel): String? {
|
||||||
return channel.recipients?.first { u -> u != RevoltAPI.selfId }
|
return channel.recipients?.firstOrNull { u -> u != RevoltAPI.selfId }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun categoriseServerFlat(server: Server): List<CategorisedChannelList> {
|
fun categoriseServerFlat(server: Server): List<CategorisedChannelList> {
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,7 @@ fun NativeMessageField(
|
||||||
channelName: String,
|
channelName: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
forceSendButton: Boolean = false,
|
forceSendButton: Boolean = false,
|
||||||
|
canAttach: Boolean = true,
|
||||||
disabled: Boolean = false,
|
disabled: Boolean = false,
|
||||||
serverId: String? = null,
|
serverId: String? = null,
|
||||||
channelId: String? = null,
|
channelId: String? = null,
|
||||||
|
|
@ -341,29 +342,31 @@ fun NativeMessageField(
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
|
||||||
Icon(
|
if (canAttach) {
|
||||||
when {
|
Icon(
|
||||||
editMode -> Icons.Default.Close
|
when {
|
||||||
else -> Icons.Default.Add
|
editMode -> Icons.Default.Close
|
||||||
},
|
else -> Icons.Default.Add
|
||||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
},
|
||||||
contentDescription = stringResource(id = R.string.add_attachment_alt),
|
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
|
||||||
modifier = Modifier
|
contentDescription = stringResource(id = R.string.add_attachment_alt),
|
||||||
.clip(CircleShape)
|
modifier = Modifier
|
||||||
.size(32.dp)
|
.clip(CircleShape)
|
||||||
.clickable {
|
.size(32.dp)
|
||||||
when {
|
.clickable {
|
||||||
editMode -> cancelEdit()
|
when {
|
||||||
else -> {
|
editMode -> cancelEdit()
|
||||||
// hide keyboard because it's annoying
|
else -> {
|
||||||
clearFocus()
|
// hide keyboard because it's annoying
|
||||||
onAddAttachment()
|
clearFocus()
|
||||||
|
onAddAttachment()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.padding(4.dp)
|
||||||
.padding(4.dp)
|
.testTag("add_attachment")
|
||||||
.testTag("add_attachment")
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
AndroidView(
|
AndroidView(
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
|
|
@ -599,6 +602,7 @@ fun NativeMessageFieldPreview() {
|
||||||
channelName = "Test",
|
channelName = "Test",
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
forceSendButton = false,
|
forceSendButton = false,
|
||||||
|
canAttach = true,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
editMode = false,
|
editMode = false,
|
||||||
cancelEdit = {},
|
cancelEdit = {},
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,8 @@ fun AttachmentManager(
|
||||||
attachments: List<FileArgs>,
|
attachments: List<FileArgs>,
|
||||||
uploading: Boolean,
|
uploading: Boolean,
|
||||||
uploadProgress: Float = 0f,
|
uploadProgress: Float = 0f,
|
||||||
onRemove: (FileArgs) -> Unit
|
onRemove: (FileArgs) -> Unit,
|
||||||
|
canRemove: Boolean = true
|
||||||
) {
|
) {
|
||||||
val animatedProgress by animateFloatAsState(
|
val animatedProgress by animateFloatAsState(
|
||||||
targetValue = uploadProgress,
|
targetValue = uploadProgress,
|
||||||
|
|
@ -69,11 +70,13 @@ fun AttachmentManager(
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
Text(attachment.filename, maxLines = 1)
|
Text(attachment.filename, maxLines = 1)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
if (canRemove) {
|
||||||
Icon(
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
Icons.Default.Close,
|
Icon(
|
||||||
contentDescription = stringResource(R.string.remove_attachment_alt)
|
Icons.Default.Close,
|
||||||
)
|
contentDescription = stringResource(R.string.remove_attachment_alt)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -529,4 +529,11 @@
|
||||||
<string name="settings_changelogs_new_header">What\'s been cooking ✨</string>
|
<string name="settings_changelogs_new_header">What\'s been cooking ✨</string>
|
||||||
<string name="settings_changelogs_historical_version_header">Changelog for %1$s</string>
|
<string name="settings_changelogs_historical_version_header">Changelog for %1$s</string>
|
||||||
<string name="settings_changelogs_historical_version_header_placeholder">that version</string>
|
<string name="settings_changelogs_historical_version_header_placeholder">that version</string>
|
||||||
|
|
||||||
|
<string name="share_target_heading">Share to Revolt</string>
|
||||||
|
<string name="share_target_login_first">Please log in before sharing to Revolt.</string>
|
||||||
|
<string name="share_target_invalid_intent">This is not a valid share intent.</string>
|
||||||
|
<string name="share_target_attachment_too_large">This attachment is too large for Revolt (max. $1$s).</string>
|
||||||
|
<string name="share_target_search_channels">Search channels</string>
|
||||||
|
<string name="share_target_select_channel">Please select a channel to share to.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue