@@ -243,11 +243,16 @@ fun TimelineScreen(
|
|||||||
var replyingToStatus by remember { mutableStateOf<com.manalejandro.myactivitypub.data.models.Status?>(null) }
|
var replyingToStatus by remember { mutableStateOf<com.manalejandro.myactivitypub.data.models.Status?>(null) }
|
||||||
var selectedStatus by remember { mutableStateOf<com.manalejandro.myactivitypub.data.models.Status?>(null) }
|
var selectedStatus by remember { mutableStateOf<com.manalejandro.myactivitypub.data.models.Status?>(null) }
|
||||||
var selectedHashtag by remember { mutableStateOf<String?>(null) }
|
var selectedHashtag by remember { mutableStateOf<String?>(null) }
|
||||||
|
var showComposeScreen by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Remember scroll state to preserve position when navigating
|
// Remember scroll state to preserve position when navigating
|
||||||
val timelineScrollState = rememberLazyListState()
|
val timelineScrollState = rememberLazyListState()
|
||||||
|
|
||||||
// Handle back button for different states
|
// Handle back button for different states
|
||||||
|
BackHandler(enabled = showComposeScreen) {
|
||||||
|
showComposeScreen = false
|
||||||
|
}
|
||||||
|
|
||||||
BackHandler(enabled = replyingToStatus != null) {
|
BackHandler(enabled = replyingToStatus != null) {
|
||||||
replyingToStatus = null
|
replyingToStatus = null
|
||||||
}
|
}
|
||||||
@@ -271,6 +276,10 @@ fun TimelineScreen(
|
|||||||
replyingToStatus = null
|
replyingToStatus = null
|
||||||
viewModel.resetInteractionState()
|
viewModel.resetInteractionState()
|
||||||
}
|
}
|
||||||
|
is InteractionState.PostCreated -> {
|
||||||
|
showComposeScreen = false
|
||||||
|
viewModel.resetInteractionState()
|
||||||
|
}
|
||||||
is InteractionState.Success -> {
|
is InteractionState.Success -> {
|
||||||
// Auto-reset after short delay
|
// Auto-reset after short delay
|
||||||
kotlinx.coroutines.delay(500)
|
kotlinx.coroutines.delay(500)
|
||||||
@@ -376,10 +385,40 @@ fun TimelineScreen(
|
|||||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
// Show FAB only on timeline (not on detail, reply, etc.) and when authenticated
|
||||||
|
if (!showNotifications && selectedStatus == null && replyingToStatus == null &&
|
||||||
|
selectedHashtag == null && !showComposeScreen && userSession != null) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = { showComposeScreen = true },
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Edit, contentDescription = "New post")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
// Show compose reply screen if replying
|
// Show compose screen if creating new post
|
||||||
if (replyingToStatus != null) {
|
if (showComposeScreen) {
|
||||||
|
com.manalejandro.myactivitypub.ui.screens.ComposeScreen(
|
||||||
|
onBack = { showComposeScreen = false },
|
||||||
|
onPost = { content, cw, visibility, mediaIds, pollOptions, pollExpiresIn, pollMultiple ->
|
||||||
|
viewModel.postNewStatus(
|
||||||
|
content = content,
|
||||||
|
contentWarning = cw,
|
||||||
|
visibility = visibility,
|
||||||
|
mediaIds = mediaIds,
|
||||||
|
pollOptions = pollOptions,
|
||||||
|
pollExpiresIn = pollExpiresIn,
|
||||||
|
pollMultiple = pollMultiple
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isPosting = interactionState is InteractionState.Processing
|
||||||
|
)
|
||||||
|
} else if (replyingToStatus != null) {
|
||||||
|
// Show compose reply screen if replying
|
||||||
com.manalejandro.myactivitypub.ui.screens.ComposeReplyScreen(
|
com.manalejandro.myactivitypub.ui.screens.ComposeReplyScreen(
|
||||||
status = replyingToStatus!!,
|
status = replyingToStatus!!,
|
||||||
onBack = { replyingToStatus = null },
|
onBack = { replyingToStatus = null },
|
||||||
|
|||||||
@@ -127,7 +127,13 @@ interface MastodonApiService {
|
|||||||
suspend fun postStatus(
|
suspend fun postStatus(
|
||||||
@Field("status") status: String,
|
@Field("status") status: String,
|
||||||
@Field("in_reply_to_id") inReplyToId: String? = null,
|
@Field("in_reply_to_id") inReplyToId: String? = null,
|
||||||
@Field("visibility") visibility: String = "public"
|
@Field("visibility") visibility: String = "public",
|
||||||
|
@Field("spoiler_text") spoilerText: String? = null,
|
||||||
|
@Field("sensitive") sensitive: Boolean? = null,
|
||||||
|
@Field("media_ids[]") mediaIds: List<String>? = null,
|
||||||
|
@Field("poll[options][]") pollOptions: List<String>? = null,
|
||||||
|
@Field("poll[expires_in]") pollExpiresIn: Int? = null,
|
||||||
|
@Field("poll[multiple]") pollMultiple: Boolean? = null
|
||||||
): Response<Status>
|
): Response<Status>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -142,10 +142,30 @@ class MastodonRepository(private val apiService: MastodonApiService) {
|
|||||||
/**
|
/**
|
||||||
* Post a new status or reply
|
* Post a new status or reply
|
||||||
*/
|
*/
|
||||||
suspend fun postStatus(content: String, inReplyToId: String? = null): Result<Status> {
|
suspend fun postStatus(
|
||||||
|
content: String,
|
||||||
|
inReplyToId: String? = null,
|
||||||
|
visibility: String = "public",
|
||||||
|
spoilerText: String? = null,
|
||||||
|
sensitive: Boolean? = null,
|
||||||
|
mediaIds: List<String>? = null,
|
||||||
|
pollOptions: List<String>? = null,
|
||||||
|
pollExpiresIn: Int? = null,
|
||||||
|
pollMultiple: Boolean? = null
|
||||||
|
): Result<Status> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val response = apiService.postStatus(content, inReplyToId)
|
val response = apiService.postStatus(
|
||||||
|
status = content,
|
||||||
|
inReplyToId = inReplyToId,
|
||||||
|
visibility = visibility,
|
||||||
|
spoilerText = spoilerText,
|
||||||
|
sensitive = sensitive,
|
||||||
|
mediaIds = mediaIds,
|
||||||
|
pollOptions = pollOptions,
|
||||||
|
pollExpiresIn = pollExpiresIn,
|
||||||
|
pollMultiple = pollMultiple
|
||||||
|
)
|
||||||
if (response.isSuccessful && response.body() != null) {
|
if (response.isSuccessful && response.body() != null) {
|
||||||
Result.success(response.body()!!)
|
Result.success(response.body()!!)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ fun ComposeReplyScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
Button(
|
// Send button
|
||||||
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (replyText.isNotBlank() && replyText.length <= 500) {
|
if (replyText.isNotBlank() && replyText.length <= 500) {
|
||||||
onReply(
|
onReply(
|
||||||
@@ -90,22 +91,59 @@ fun ComposeReplyScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = replyText.isNotBlank() && replyText.length <= 500 && !isPosting,
|
enabled = replyText.isNotBlank() && replyText.length <= 500 && !isPosting
|
||||||
modifier = Modifier.padding(end = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
if (isPosting) {
|
if (isPosting) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
strokeWidth = 2.dp,
|
strokeWidth = 2.dp
|
||||||
color = MaterialTheme.colorScheme.onPrimary
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Text("Post")
|
Icon(
|
||||||
|
imageVector = Icons.Default.Send,
|
||||||
|
contentDescription = "Send reply",
|
||||||
|
tint = if (replyText.isNotBlank() && replyText.length <= 500) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
// Large visible send button
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
if (replyText.isNotBlank() && replyText.length <= 500) {
|
||||||
|
onReply(
|
||||||
|
replyText,
|
||||||
|
if (showCW && contentWarning.isNotBlank()) contentWarning else null,
|
||||||
|
visibility
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
modifier = Modifier.padding(bottom = 80.dp, end = 16.dp)
|
||||||
|
) {
|
||||||
|
if (isPosting) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Send,
|
||||||
|
contentDescription = "Send reply",
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
ComposeBottomBar(
|
ComposeBottomBar(
|
||||||
onImageClick = {
|
onImageClick = {
|
||||||
|
|||||||
340
app/src/main/java/com/manalejandro/myactivitypub/ui/screens/ComposeScreen.kt
Archivo normal
340
app/src/main/java/com/manalejandro/myactivitypub/ui/screens/ComposeScreen.kt
Archivo normal
@@ -0,0 +1,340 @@
|
|||||||
|
package com.manalejandro.myactivitypub.ui.screens
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.manalejandro.myactivitypub.ui.components.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-screen compose interface for creating new posts
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ComposeScreen(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onPost: (content: String, contentWarning: String?, visibility: String, mediaIds: List<String>?, pollOptions: List<String>?, pollExpiresIn: Int?, pollMultiple: Boolean?) -> Unit,
|
||||||
|
isPosting: Boolean = false
|
||||||
|
) {
|
||||||
|
var postText by remember { mutableStateOf("") }
|
||||||
|
var textFieldValue by remember { mutableStateOf(TextFieldValue("")) }
|
||||||
|
var showCW by remember { mutableStateOf(false) }
|
||||||
|
var contentWarning by remember { mutableStateOf("") }
|
||||||
|
var visibility by remember { mutableStateOf("public") }
|
||||||
|
var showVisibilityMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Media, emoji, and poll state
|
||||||
|
var selectedMedia by remember { mutableStateOf<List<Uri>>(emptyList()) }
|
||||||
|
var showEmojiPicker by remember { mutableStateOf(false) }
|
||||||
|
var showPollCreator by remember { mutableStateOf(false) }
|
||||||
|
var poll by remember { mutableStateOf<PollData?>(null) }
|
||||||
|
var mediaLauncherTrigger by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||||
|
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
// Show emoji picker dialog
|
||||||
|
if (showEmojiPicker) {
|
||||||
|
EmojiPickerDialog(
|
||||||
|
onDismiss = { showEmojiPicker = false },
|
||||||
|
onEmojiSelected = { emoji ->
|
||||||
|
// Insert emoji at cursor position
|
||||||
|
val currentText = textFieldValue.text
|
||||||
|
val selection = textFieldValue.selection
|
||||||
|
val newText = currentText.substring(0, selection.start) +
|
||||||
|
emoji +
|
||||||
|
currentText.substring(selection.end)
|
||||||
|
textFieldValue = TextFieldValue(
|
||||||
|
text = newText,
|
||||||
|
selection = TextRange(selection.start + emoji.length)
|
||||||
|
)
|
||||||
|
postText = newText
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show poll creator dialog
|
||||||
|
if (showPollCreator) {
|
||||||
|
PollCreatorDialog(
|
||||||
|
onDismiss = { showPollCreator = false },
|
||||||
|
onPollCreated = { newPoll ->
|
||||||
|
poll = newPoll
|
||||||
|
showPollCreator = false
|
||||||
|
},
|
||||||
|
initialPoll = poll
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("New Post") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack, enabled = !isPosting) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
// Send button
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (postText.isNotBlank() && postText.length <= 500) {
|
||||||
|
onPost(
|
||||||
|
postText,
|
||||||
|
if (showCW && contentWarning.isNotBlank()) contentWarning else null,
|
||||||
|
visibility,
|
||||||
|
null, // mediaIds - to be implemented
|
||||||
|
poll?.options,
|
||||||
|
poll?.expiresIn,
|
||||||
|
poll?.multiple
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = postText.isNotBlank() && postText.length <= 500 && !isPosting
|
||||||
|
) {
|
||||||
|
if (isPosting) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Send,
|
||||||
|
contentDescription = "Send post",
|
||||||
|
tint = if (postText.isNotBlank() && postText.length <= 500) {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
// Large visible send button
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
if (postText.isNotBlank() && postText.length <= 500) {
|
||||||
|
onPost(
|
||||||
|
postText,
|
||||||
|
if (showCW && contentWarning.isNotBlank()) contentWarning else null,
|
||||||
|
visibility,
|
||||||
|
null, // mediaIds
|
||||||
|
poll?.options,
|
||||||
|
poll?.expiresIn,
|
||||||
|
poll?.multiple
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
modifier = Modifier.padding(bottom = 80.dp, end = 16.dp)
|
||||||
|
) {
|
||||||
|
if (isPosting) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Send,
|
||||||
|
contentDescription = "Send post",
|
||||||
|
modifier = Modifier.size(28.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
ComposeBottomBar(
|
||||||
|
onImageClick = {
|
||||||
|
mediaLauncherTrigger?.invoke()
|
||||||
|
},
|
||||||
|
onEmojiClick = { showEmojiPicker = true },
|
||||||
|
onPollClick = {
|
||||||
|
if (poll == null) {
|
||||||
|
showPollCreator = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCWClick = { showCW = !showCW },
|
||||||
|
onVisibilityClick = { showVisibilityMenu = true },
|
||||||
|
visibility = visibility,
|
||||||
|
showCW = showCW,
|
||||||
|
enabled = !isPosting
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
|
) {
|
||||||
|
// Content Warning field
|
||||||
|
if (showCW) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = contentWarning,
|
||||||
|
onValueChange = { contentWarning = it },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
placeholder = { Text("Content warning (optional)") },
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
showCW = false
|
||||||
|
contentWarning = ""
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Default.Close, "Remove content warning")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = !isPosting
|
||||||
|
)
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Media picker
|
||||||
|
MediaPicker(
|
||||||
|
selectedMedia = selectedMedia,
|
||||||
|
onMediaSelected = { uris ->
|
||||||
|
selectedMedia = (selectedMedia + uris).take(4)
|
||||||
|
},
|
||||||
|
onMediaRemoved = { uri ->
|
||||||
|
selectedMedia = selectedMedia.filter { it != uri }
|
||||||
|
},
|
||||||
|
onLauncherReady = { trigger ->
|
||||||
|
mediaLauncherTrigger = trigger
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Poll preview
|
||||||
|
poll?.let { currentPoll ->
|
||||||
|
PollPreviewCard(
|
||||||
|
poll = currentPoll,
|
||||||
|
onRemove = { poll = null },
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main text field
|
||||||
|
OutlinedTextField(
|
||||||
|
value = textFieldValue,
|
||||||
|
onValueChange = {
|
||||||
|
textFieldValue = it
|
||||||
|
postText = it.text
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.heightIn(min = 300.dp),
|
||||||
|
placeholder = { Text("What's on your mind?") },
|
||||||
|
enabled = !isPosting
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Character counter and visibility
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${postText.length}/500",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = if (postText.length > 500)
|
||||||
|
MaterialTheme.colorScheme.error
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
when (visibility) {
|
||||||
|
"public" -> Icons.Default.Public
|
||||||
|
"unlisted" -> Icons.Default.Lock
|
||||||
|
"private" -> Icons.Default.LockOpen
|
||||||
|
"direct" -> Icons.Default.Mail
|
||||||
|
else -> Icons.Default.Public
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(
|
||||||
|
text = visibility.replaceFirstChar { it.uppercase() },
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postText.length > 500) {
|
||||||
|
Text(
|
||||||
|
text = "⚠️ Text is too long (${postText.length}/500)",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visibility menu
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showVisibilityMenu,
|
||||||
|
onDismissRequest = { showVisibilityMenu = false }
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Public") },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Public, null) },
|
||||||
|
onClick = {
|
||||||
|
visibility = "public"
|
||||||
|
showVisibilityMenu = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Unlisted") },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Lock, null) },
|
||||||
|
onClick = {
|
||||||
|
visibility = "unlisted"
|
||||||
|
showVisibilityMenu = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Followers only") },
|
||||||
|
leadingIcon = { Icon(Icons.Default.LockOpen, null) },
|
||||||
|
onClick = {
|
||||||
|
visibility = "private"
|
||||||
|
showVisibilityMenu = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Direct") },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Mail, null) },
|
||||||
|
onClick = {
|
||||||
|
visibility = "direct"
|
||||||
|
showVisibilityMenu = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -248,6 +248,45 @@ class TimelineViewModel(private val repository: MastodonRepository) : ViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post a new status
|
||||||
|
*/
|
||||||
|
fun postNewStatus(
|
||||||
|
content: String,
|
||||||
|
contentWarning: String? = null,
|
||||||
|
visibility: String = "public",
|
||||||
|
mediaIds: List<String>? = null,
|
||||||
|
pollOptions: List<String>? = null,
|
||||||
|
pollExpiresIn: Int? = null,
|
||||||
|
pollMultiple: Boolean? = null
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_interactionState.value = InteractionState.Processing("")
|
||||||
|
|
||||||
|
repository.postStatus(
|
||||||
|
content = content,
|
||||||
|
inReplyToId = null,
|
||||||
|
visibility = visibility,
|
||||||
|
spoilerText = contentWarning,
|
||||||
|
sensitive = false,
|
||||||
|
mediaIds = mediaIds,
|
||||||
|
pollOptions = pollOptions,
|
||||||
|
pollExpiresIn = pollExpiresIn,
|
||||||
|
pollMultiple = pollMultiple
|
||||||
|
).fold(
|
||||||
|
onSuccess = { newStatus ->
|
||||||
|
// Add new status to the beginning of the timeline
|
||||||
|
currentStatuses.add(0, newStatus)
|
||||||
|
_uiState.value = TimelineUiState.Success(currentStatuses.toList())
|
||||||
|
_interactionState.value = InteractionState.PostCreated
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
_interactionState.value = InteractionState.Error(error.message ?: "Failed to post")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a status in the current timeline
|
* Update a status in the current timeline
|
||||||
*/
|
*/
|
||||||
@@ -299,6 +338,7 @@ sealed class InteractionState {
|
|||||||
data class Processing(val statusId: String) : InteractionState()
|
data class Processing(val statusId: String) : InteractionState()
|
||||||
object Success : InteractionState()
|
object Success : InteractionState()
|
||||||
object ReplyPosted : InteractionState()
|
object ReplyPosted : InteractionState()
|
||||||
|
object PostCreated : InteractionState()
|
||||||
data class Error(val message: String) : InteractionState()
|
data class Error(val message: String) : InteractionState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user