@@ -54,6 +54,7 @@ import androidx.compose.material3.TooltipDefaults
5454import androidx.compose.material3.rememberModalBottomSheetState
5555import androidx.compose.material3.rememberTooltipState
5656import androidx.compose.runtime.Composable
57+ import androidx.compose.runtime.LaunchedEffect
5758import androidx.compose.runtime.getValue
5859import androidx.compose.runtime.mutableStateOf
5960import androidx.compose.runtime.remember
@@ -64,6 +65,8 @@ import androidx.compose.ui.Alignment
6465import androidx.compose.ui.Modifier
6566import androidx.compose.ui.platform.LocalClipboardManager
6667import androidx.compose.ui.platform.LocalContext
68+ import androidx.compose.ui.semantics.contentDescription
69+ import androidx.compose.ui.semantics.semantics
6770import androidx.compose.ui.text.AnnotatedString
6871import androidx.compose.ui.text.input.KeyboardType
6972import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -73,6 +76,7 @@ import androidx.compose.ui.unit.IntSize
7376import androidx.compose.ui.unit.dp
7477import androidx.compose.ui.unit.sp
7578import kotlinx.coroutines.launch
79+ import me.spoo.android.data.AliasStatus
7680import me.spoo.android.data.CreateLinkRequest
7781import me.spoo.android.data.EmojiCatalog
7882import me.spoo.android.data.ErrorField
@@ -90,6 +94,8 @@ fun CreateLinkSheet(
9094 initialUrl : String? ,
9195 state : CreateState ,
9296 emojiCatalog : EmojiCatalog ? ,
97+ aliasStatus : AliasStatus ,
98+ onAliasChanged : (String ) -> Unit ,
9399 onEmojiMode : () -> Unit ,
94100 onSubmit : (CreateLinkRequest ) -> Unit ,
95101 onDismiss : () -> Unit ,
@@ -134,6 +140,8 @@ fun CreateLinkSheet(
134140 submitting = state is CreateState .Submitting ,
135141 error = (state as ? CreateState .Failed )?.error,
136142 emojiCatalog = emojiCatalog,
143+ aliasStatus = aliasStatus,
144+ onAliasChanged = onAliasChanged,
137145 onEmojiMode = {
138146 onEmojiMode()
139147 // The picker deserves the room: pop the sheet open.
@@ -155,6 +163,8 @@ private fun FormPhase(
155163 submitting : Boolean ,
156164 error : FriendlyError ? ,
157165 emojiCatalog : EmojiCatalog ? ,
166+ aliasStatus : AliasStatus ,
167+ onAliasChanged : (String ) -> Unit ,
158168 onEmojiMode : () -> Unit ,
159169 onSubmit : (CreateLinkRequest ) -> Unit ,
160170) {
@@ -175,6 +185,19 @@ private fun FormPhase(
175185 val emojiCount = emojiPicks.codePointCount(0 , emojiPicks.length)
176186 val emojiMax = emojiCatalog?.maxGraphemes ? : Int .MAX_VALUE
177187
188+ // Live availability: report the effective alias upstream, debounced
189+ // and checked in the view model; "taken" comes back as [aliasTaken].
190+ val effectiveAlias = if (emojiAlias) emojiPicks else alias.trim()
191+ LaunchedEffect (effectiveAlias) { onAliasChanged(effectiveAlias) }
192+ val aliasBlocked = aliasStatus != AliasStatus .Free && aliasStatus != AliasStatus .Unknown
193+ val aliasMessage =
194+ when (aliasStatus) {
195+ AliasStatus .Taken -> " Already taken"
196+ AliasStatus .Reserved -> " Reserved by spoo.me"
197+ AliasStatus .Invalid -> " Not a usable alias"
198+ else -> null
199+ }
200+
178201 // Prevention first, server as backstop: local checks gate the button;
179202 // whatever still comes back lands on the field the server names.
180203 val urlOk = isLikelyUrl(url)
@@ -216,15 +239,40 @@ private fun FormPhase(
216239 // icon swaps back. In emoji mode the field is a read-only composed
217240 // display, so an invalid alias can't be typed.
218241 OutlinedTextField (
219- value = if (emojiAlias) emojiPicks.emojiPresentationAll() else alias,
242+ // Emoji live in the prefix slot as Fluent artwork. The text slot
243+ // carries a zero-width space when picks exist: an empty unfocused
244+ // field hides its prefix and floats the label back down.
245+ value =
246+ when {
247+ ! emojiAlias -> alias
248+ emojiPicks.isEmpty() -> " "
249+ else -> " \u200B "
250+ },
220251 // The API's alias charset, enforced at the keyboard: no error to show.
221252 onValueChange = { if (! emojiAlias) alias = it.filter(::isAliasChar) },
222253 modifier = Modifier .fillMaxWidth(),
223254 readOnly = emojiAlias,
224255 label = { Text (if (emojiAlias) " Emoji alias" else " Alias" ) },
225- placeholder = { Text (" Random if empty" , maxLines = 1 ) },
226- prefix = { Text (" spoo.me/" ) },
227- isError = error?.field == ErrorField .Alias ,
256+ placeholder =
257+ if (emojiAlias && emojiPicks.isNotEmpty()) {
258+ null
259+ } else {
260+ { Text (" Random if empty" , maxLines = 1 ) }
261+ },
262+ prefix = {
263+ Row (verticalAlignment = Alignment .CenterVertically ) {
264+ Text (" spoo.me/" )
265+ if (emojiAlias && emojiPicks.isNotEmpty()) {
266+ EmojiText (
267+ emojiPicks.emojiPresentationAll(),
268+ maxLines = 1 ,
269+ overflow = TextOverflow .Ellipsis ,
270+ modifier = Modifier .semantics { contentDescription = " Alias $emojiPicks " },
271+ )
272+ }
273+ }
274+ },
275+ isError = aliasBlocked || error?.field == ErrorField .Alias ,
228276 trailingIcon = {
229277 Row {
230278 if (emojiAlias && emojiPicks.isNotEmpty()) {
@@ -241,7 +289,10 @@ private fun FormPhase(
241289 IconButton (
242290 onClick = {
243291 emojiAlias = ! emojiAlias
244- if (emojiAlias) onEmojiMode()
292+ // Leaving emoji mode drops the picks: keeping them
293+ // hidden would submit a random code while the user
294+ // believes an alias is set.
295+ if (emojiAlias) onEmojiMode() else emojiPicks = " "
245296 },
246297 enabled = ! submitting,
247298 ) {
@@ -255,6 +306,7 @@ private fun FormPhase(
255306 supportingText =
256307 when {
257308 error?.field == ErrorField .Alias -> ({ Text (error.message) })
309+ aliasMessage != null -> ({ Text (aliasMessage) })
258310 emojiAlias && emojiCatalog != null && emojiCount > 0 ->
259311 ({ Text (" $emojiCount /${emojiCatalog.maxGraphemes} " ) })
260312 else -> null
@@ -410,7 +462,7 @@ private fun FormPhase(
410462 )
411463 },
412464 modifier = Modifier .fillMaxWidth(),
413- enabled = ! submitting && urlOk && passwordOk,
465+ enabled = ! submitting && urlOk && passwordOk && ! aliasBlocked ,
414466 ) {
415467 if (submitting) {
416468 LoadingIndicator (modifier = Modifier .height(24 .dp))
0 commit comments