feat: inbuilt media picker
Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
parent
de37d14139
commit
df57124ab4
|
|
@ -4,6 +4,9 @@
|
|||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
|
|
|||
|
|
@ -5,11 +5,15 @@ import chat.revolt.api.RevoltHttp
|
|||
import chat.revolt.api.RevoltJson
|
||||
import chat.revolt.api.schemas.AutumnError
|
||||
import chat.revolt.api.schemas.AutumnId
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.client.plugins.onUpload
|
||||
import io.ktor.client.request.forms.MultiPartFormDataContent
|
||||
import io.ktor.client.request.forms.formData
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.Headers
|
||||
import io.ktor.http.HttpHeaders
|
||||
import java.io.File
|
||||
|
||||
const val MAX_ATTACHMENTS_PER_MESSAGE = 5
|
||||
|
|
@ -18,6 +22,7 @@ data class FileArgs(
|
|||
val file: File,
|
||||
val filename: String,
|
||||
val contentType: String,
|
||||
val pickerIdentifier: String? = null
|
||||
)
|
||||
|
||||
suspend fun uploadToAutumn(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package chat.revolt.api.settings
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
|
||||
annotation class FeatureFlag(val name: String)
|
||||
annotation class Treatment(val description: String)
|
||||
|
||||
@FeatureFlag("TiramisuFilePicker")
|
||||
enum class FilePickerFeatureFlagVariates {
|
||||
@Treatment("Use the READ_MEDIA_IMAGES or READ_MEDIA_VIDEO permissions introduced in Android Tiramisu")
|
||||
TiramisuMediaPermissions,
|
||||
|
||||
@Treatment("Use the DocumentsUI picker introduced in Android KitKat")
|
||||
DocumentsUI
|
||||
}
|
||||
|
||||
object FeatureFlags {
|
||||
@FeatureFlag("TiramisuFilePicker")
|
||||
var filePickerType by mutableStateOf(FilePickerFeatureFlagVariates.TiramisuMediaPermissions)
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.testTag
|
||||
|
|
@ -61,6 +62,7 @@ fun MessageField(
|
|||
disabled: Boolean = false,
|
||||
editMode: Boolean = false,
|
||||
cancelEdit: () -> Unit = {},
|
||||
onFocusChange: (Boolean) -> Unit = {},
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val placeholderResource = when (channelType) {
|
||||
|
|
@ -74,7 +76,7 @@ fun MessageField(
|
|||
val sendButtonVisible = (messageContent.isNotBlank() || forceSendButton) && !disabled
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp))
|
||||
) {
|
||||
|
||||
|
|
@ -85,9 +87,12 @@ fun MessageField(
|
|||
enabled = !disabled,
|
||||
textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.weight(1f)
|
||||
.focusRequester(focusRequester),
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { state ->
|
||||
onFocusChange(state.isFocused)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions = KeyboardActions.Default,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,269 @@
|
|||
package chat.revolt.components.media
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
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.width
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.revolt.R
|
||||
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||
import com.bumptech.glide.integration.compose.GlideImage
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
|
||||
data class Media(
|
||||
val uri: Uri,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val aspectRatio: Float = width.toFloat() / height.toFloat()
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class, ExperimentalGlideComposeApi::class)
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
@Composable
|
||||
fun InbuiltMediaPicker(
|
||||
onOpenDocumentsUi: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
onMediaSelected: (Media) -> Unit,
|
||||
pendingMedia: List<String>,
|
||||
disabled: Boolean = false,
|
||||
) {
|
||||
val mediaPermissionState = rememberMultiplePermissionsState(
|
||||
listOf(
|
||||
android.Manifest.permission.READ_MEDIA_IMAGES,
|
||||
android.Manifest.permission.READ_MEDIA_VIDEO
|
||||
)
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val images = remember { mutableStateListOf<Media>() }
|
||||
|
||||
BackHandler {
|
||||
onClose()
|
||||
}
|
||||
|
||||
LaunchedEffect(mediaPermissionState.allPermissionsGranted) {
|
||||
if (mediaPermissionState.allPermissionsGranted) {
|
||||
val projection = arrayOf(
|
||||
MediaStore.Images.ImageColumns._ID,
|
||||
MediaStore.Images.ImageColumns.RESOLUTION
|
||||
)
|
||||
|
||||
val selection: String? = null
|
||||
val selectionArgs: Array<String>? = null
|
||||
val sortOrder = MediaStore.Images.ImageColumns.DATE_ADDED + " DESC"
|
||||
|
||||
val queryUri = MediaStore.Files.getContentUri("external")
|
||||
|
||||
val cursor: Cursor? = context.contentResolver.query(
|
||||
queryUri,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
sortOrder
|
||||
)
|
||||
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
val id =
|
||||
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID))
|
||||
val resolution =
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.RESOLUTION))
|
||||
val contentUri =
|
||||
ContentUris.withAppendedId(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
id
|
||||
)
|
||||
|
||||
if (resolution == null) continue
|
||||
|
||||
images.add(
|
||||
Media(
|
||||
uri = contentUri,
|
||||
width = resolution.split("×")[0].toInt(),
|
||||
height = resolution.split("×")[1].toInt()
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
cursor.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.5f)
|
||||
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Crossfade(
|
||||
targetState = mediaPermissionState.allPermissionsGranted,
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
label = "Media picker permission dialog"
|
||||
) { state ->
|
||||
if (!state) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ux_file_request),
|
||||
modifier = Modifier
|
||||
.width(128.dp)
|
||||
.height(128.dp),
|
||||
contentDescription = null // decorative
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.file_picker_permission_request_header),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.file_picker_permission_request_body),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
color = LocalContentColor.current.copy(
|
||||
alpha = 0.5f
|
||||
)
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Button(onClick = {
|
||||
mediaPermissionState.launchMultiplePermissionRequest()
|
||||
}) {
|
||||
Text(text = stringResource(id = R.string.file_picker_permission_request_cta))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = 16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
) {
|
||||
AssistChip(
|
||||
onClick = {
|
||||
onOpenDocumentsUi()
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(id = R.string.file_picker_chip_documents))
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_paperclip_24dp),
|
||||
contentDescription = null // see label
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LazyVerticalStaggeredGrid(
|
||||
columns = StaggeredGridCells.Adaptive(100.dp),
|
||||
verticalItemSpacing = 8.dp,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
content = {
|
||||
items(images.size) { image ->
|
||||
GlideImage(
|
||||
model = images[image].uri.toString(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.then(
|
||||
if (images[image].uri.lastPathSegment in pendingMedia) {
|
||||
Modifier.border(
|
||||
width = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.then(
|
||||
if (disabled) {
|
||||
Modifier.alpha(0.5f)
|
||||
} else {
|
||||
Modifier.clickable {
|
||||
onMediaSelected(images[image])
|
||||
}
|
||||
}
|
||||
)
|
||||
.width(100.dp)
|
||||
.aspectRatio(images[image].aspectRatio),
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package chat.revolt.screens.chat.views.channel
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
|
@ -45,6 +47,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -58,9 +61,13 @@ import chat.revolt.activities.RevoltTweenInt
|
|||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.internals.ChannelUtils
|
||||
import chat.revolt.api.routes.microservices.autumn.FileArgs
|
||||
import chat.revolt.api.settings.FeatureFlag
|
||||
import chat.revolt.api.settings.FeatureFlags
|
||||
import chat.revolt.api.settings.FilePickerFeatureFlagVariates
|
||||
import chat.revolt.components.chat.Message
|
||||
import chat.revolt.components.chat.MessageField
|
||||
import chat.revolt.components.chat.SystemMessage
|
||||
import chat.revolt.components.media.InbuiltMediaPicker
|
||||
import chat.revolt.components.screens.chat.AttachmentManager
|
||||
import chat.revolt.components.screens.chat.ChannelHeader
|
||||
import chat.revolt.components.screens.chat.ReplyManager
|
||||
|
|
@ -81,6 +88,7 @@ import com.discord.simpleast.core.simple.SimpleRenderer
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -104,27 +112,43 @@ fun ChannelScreen(
|
|||
var messageContextSheetShown by remember { mutableStateOf(false) }
|
||||
var messageContextSheetTarget by remember { mutableStateOf("") }
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
fun processFileUri(uri: Uri, pickerIdentifier: String? = null) {
|
||||
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)
|
||||
}
|
||||
|
||||
// If the file is already pending and was picked from the inbuilt picker, remove it.
|
||||
// This is so you can "toggle" the file in the picker.
|
||||
// If the file was picked via DocumentsUI we don't want toggling functionality as
|
||||
// if you specifically opened it from DocumentsUI you probably want to send it anyway.
|
||||
if (viewModel.pendingAttachments.any { it.pickerIdentifier == pickerIdentifier }) {
|
||||
viewModel.pendingAttachments.removeIf { it.pickerIdentifier == pickerIdentifier }
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.pendingAttachments.add(
|
||||
FileArgs(
|
||||
file = mFile,
|
||||
contentType = file.type ?: "application/octet-stream",
|
||||
filename = file.name ?: "attachment",
|
||||
pickerIdentifier = pickerIdentifier
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val pickFileLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenMultipleDocuments()
|
||||
) { uriList ->
|
||||
uriList.let { uris ->
|
||||
uris.forEach {
|
||||
DocumentFile.fromSingleUri(context, it)?.let { file ->
|
||||
val mFile = File(context.cacheDir, file.name ?: "attachment")
|
||||
|
||||
mFile.outputStream().use { output ->
|
||||
@Suppress("Recycle")
|
||||
context.contentResolver.openInputStream(it)?.copyTo(output)
|
||||
}
|
||||
|
||||
viewModel.pendingAttachments.add(
|
||||
FileArgs(
|
||||
file = mFile,
|
||||
contentType = file.type ?: "application/octet-stream",
|
||||
filename = file.name ?: "attachment"
|
||||
)
|
||||
)
|
||||
}
|
||||
uriList.let { list ->
|
||||
list.forEach { uri ->
|
||||
processFileUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -409,7 +433,21 @@ fun ChannelScreen(
|
|||
},
|
||||
onSendMessage = viewModel::sendPendingMessage,
|
||||
onAddAttachment = {
|
||||
pickFileLauncher.launch(arrayOf("*/*"))
|
||||
val isTiramisu = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||
|
||||
@FeatureFlag("TiramisuFilePicker")
|
||||
when {
|
||||
FeatureFlags.filePickerType == FilePickerFeatureFlagVariates.TiramisuMediaPermissions
|
||||
&& isTiramisu -> {
|
||||
focusManager.clearFocus()
|
||||
viewModel.inbuiltFilePickerOpen = !viewModel.inbuiltFilePickerOpen
|
||||
}
|
||||
|
||||
FeatureFlags.filePickerType == FilePickerFeatureFlagVariates.DocumentsUI
|
||||
|| !isTiramisu -> {
|
||||
pickFileLauncher.launch(arrayOf("*/*"))
|
||||
}
|
||||
}
|
||||
},
|
||||
channelType = channel.channelType,
|
||||
channelName = channel.name ?: ChannelUtils.resolveDMName(channel) ?: stringResource(
|
||||
|
|
@ -419,7 +457,46 @@ fun ChannelScreen(
|
|||
disabled = viewModel.pendingAttachments.isNotEmpty() && viewModel.isSendingMessage,
|
||||
editMode = viewModel.editingMessage != null,
|
||||
cancelEdit = viewModel::cancelEditingMessage,
|
||||
onFocusChange = { nowFocused ->
|
||||
if (nowFocused && viewModel.inbuiltFilePickerOpen) {
|
||||
viewModel.inbuiltFilePickerOpen = false
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
AnimatedVisibility(visible = viewModel.inbuiltFilePickerOpen) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
InbuiltMediaPicker(
|
||||
onOpenDocumentsUi = {
|
||||
pickFileLauncher.launch(arrayOf("*/*"))
|
||||
viewModel.inbuiltFilePickerOpen = false
|
||||
},
|
||||
onClose = {
|
||||
viewModel.inbuiltFilePickerOpen = false
|
||||
},
|
||||
onMediaSelected = { media ->
|
||||
try {
|
||||
processFileUri(
|
||||
media.uri,
|
||||
pickerIdentifier = media.uri.lastPathSegment
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is FileNotFoundException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.file_picker_cannot_attach_file_invalid),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
pendingMedia = viewModel.pendingAttachments
|
||||
.filterNot { it.pickerIdentifier == null }
|
||||
.map { it.pickerIdentifier!! },
|
||||
disabled = viewModel.isSendingMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import chat.revolt.api.routes.server.fetchMember
|
|||
import chat.revolt.api.routes.user.addUserIfUnknown
|
||||
import chat.revolt.api.schemas.Channel
|
||||
import chat.revolt.api.schemas.Message
|
||||
import chat.revolt.api.settings.FeatureFlag
|
||||
import chat.revolt.callbacks.UiCallback
|
||||
import chat.revolt.callbacks.UiCallbacks
|
||||
import io.ktor.http.ContentType
|
||||
|
|
@ -58,6 +59,9 @@ class ChannelScreenViewModel : ViewModel() {
|
|||
var pendingReplies = mutableStateListOf<SendMessageReply>()
|
||||
var pendingAttachments = mutableStateListOf<FileArgs>()
|
||||
|
||||
@FeatureFlag("TiramisuFilePicker")
|
||||
var inbuiltFilePickerOpen by mutableStateOf(false)
|
||||
|
||||
var pendingUploadProgress by mutableFloatStateOf(0f)
|
||||
|
||||
var editingMessage by mutableStateOf<String?>(null)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M16.5,6V17.5A4,4 0 0,1 12.5,21.5A4,4 0 0,1 8.5,17.5V5A2.5,2.5 0 0,1 11,2.5A2.5,2.5 0 0,1 13.5,5V15.5A1,1 0 0,1 12.5,16.5A1,1 0 0,1 11.5,15.5V6H10V15.5A2.5,2.5 0 0,0 12.5,18A2.5,2.5 0 0,0 15,15.5V5A4,4 0 0,0 11,1A4,4 0 0,0 7,5V17.5A5.5,5.5 0 0,0 12.5,23A5.5,5.5 0 0,0 18,17.5V6H16.5Z" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="95.914dp"
|
||||
android:height="80.251dp"
|
||||
android:viewportWidth="95.914"
|
||||
android:viewportHeight="80.251">
|
||||
<path
|
||||
android:pathData="M4.557,40.125a38.721,40.125 0,1 0,77.443 0a38.721,40.125 0,1 0,-77.443 0z"
|
||||
android:strokeWidth="7.76499"
|
||||
android:fillColor="@color/material_dynamic_primary100"
|
||||
android:fillAlpha="0.196078"/>
|
||||
<path
|
||||
android:pathData="M13.333,40.125a29.945,31.031 0,1 0,59.891 0a29.945,31.031 0,1 0,-59.891 0z"
|
||||
android:strokeWidth="6.00512"
|
||||
android:fillColor="@color/material_dynamic_primary100"
|
||||
android:fillAlpha="0.196078"/>
|
||||
<path
|
||||
android:pathData="M24.1,40.125a19.179,19.874 0,1 0,38.358 0a19.179,19.874 0,1 0,-38.358 0z"
|
||||
android:strokeWidth="3.84605"
|
||||
android:fillColor="@color/material_dynamic_primary100"
|
||||
android:fillAlpha="0.196078"/>
|
||||
<path
|
||||
android:pathData="m70.658,10.102 l-13.704,-3.441c-2.535,-0.637 -5.078,0.886 -5.715,3.421L44.356,37.489a4.71,4.71 0,0 0,3.421 5.715l36.543,9.177a4.71,4.71 0,0 0,5.715 -3.421l5.736,-22.84c0.637,-2.535 -0.908,-5.084 -3.421,-5.715l-18.272,-4.589z"
|
||||
android:strokeWidth="2.35489"
|
||||
android:fillColor="#db4e5b"/>
|
||||
<path
|
||||
android:pathData="m11.386,55.62 l4.774,3.257 3.159,-7.823 8.836,6.717 -19.927,5.672m23.179,-5.059 l-5.672,-19.927c-0.45,-1.58 -2.091,-2.482 -3.657,-2.036l-19.927,5.672a2.96,2.96 0,0 0,-2.036 3.657l5.672,19.927a2.96,2.96 0,0 0,3.657 2.036l19.927,-5.672a2.96,2.96 0,0 0,2.036 -3.657z"
|
||||
android:strokeWidth="1.47992"
|
||||
android:fillColor="@color/foreground"/>
|
||||
</vector>
|
||||
|
|
@ -285,6 +285,13 @@
|
|||
<string name="media_viewer_share_video">Share video</string>
|
||||
<string name="media_viewer_share_image">Share image</string>
|
||||
|
||||
<string name="file_picker_cannot_attach_file_invalid">This file is invalid and cannot be attached.</string>
|
||||
<string name="file_picker_permission_request_header">We need your permission to access photos and videos</string>
|
||||
<string name="file_picker_permission_request_body">You will be able to attach photos and videos to your messages afterwards.</string>
|
||||
<string name="file_picker_permission_request_cta">Allow access</string>
|
||||
<string name="file_picker_chip_documents">Attach a file</string>
|
||||
<string name="file_picker_chip_camera">Take a photo</string>
|
||||
|
||||
<string name="spark_sidebar_settings_tutorial">The settings are in the sidebar</string>
|
||||
<string name="spark_sidebar_settings_tutorial_description_1">You can open the sidebar by swiping from the left edge of the screen.</string>
|
||||
<string name="spark_sidebar_settings_tutorial_description_2">Then long tap your profile picture to open the settings.</string>
|
||||
|
|
|
|||
Loading…
Reference in New Issue