mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-05 22:34:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c29bf3f25 | ||
|
|
9642e45775 | ||
|
|
58788d2cd5 | ||
|
|
d7c18341cd | ||
|
|
ee16f6c143 | ||
|
|
d78b55d134 | ||
|
|
ff5e7359e2 | ||
|
|
4fd0f38402 | ||
|
|
5b516f4101 | ||
|
|
1fc390c215 | ||
|
|
2bb09f583b |
+4
@@ -37,6 +37,10 @@
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
|
||||
@@ -16,8 +16,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk libs.versions.android.minSdk.get().toInteger()
|
||||
targetSdk libs.versions.android.targetSdk.get().toInteger()
|
||||
versionCode 407
|
||||
versionName "0.92.2"
|
||||
versionCode 408
|
||||
versionName "0.92.3"
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"98f2acaf9b3eb70d87607942e576f9a086ac28adf37869da9a29f4815cecac86\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -26,6 +26,8 @@ import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
@@ -110,6 +112,7 @@ import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
|
||||
import com.vitorpamplona.amethyst.ui.note.types.EditState
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderReaction
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
@@ -135,6 +138,9 @@ import com.vitorpamplona.amethyst.ui.theme.Size28Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.theme.TinyBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.defaultTweenDuration
|
||||
import com.vitorpamplona.amethyst.ui.theme.defaultTweenFloatSpec
|
||||
import com.vitorpamplona.amethyst.ui.theme.defaultTweenIntOffsetSpec
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.reactionBox
|
||||
@@ -1014,20 +1020,26 @@ fun ZapReaction(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = ripple24dp,
|
||||
onClick = {
|
||||
zapClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } },
|
||||
onMultipleChoices = { wantsToZap = true },
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = showErrorMessageDialog + message
|
||||
}
|
||||
},
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
)
|
||||
scope.launch {
|
||||
zapClick(
|
||||
baseNote,
|
||||
accountViewModel,
|
||||
context,
|
||||
onZappingProgress = { progress: Float -> scope.launch { zappingProgress = progress } },
|
||||
onMultipleChoices = {
|
||||
scope.launch {
|
||||
wantsToZap = true
|
||||
}
|
||||
},
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
zappingProgress = 0f
|
||||
showErrorMessageDialog = showErrorMessageDialog + message
|
||||
}
|
||||
},
|
||||
onPayViaIntent = { wantsToPay = it },
|
||||
)
|
||||
}
|
||||
},
|
||||
onLongClick = { wantsToChangeZapAmount = true },
|
||||
onDoubleClick = { wantsToSetCustomZap = true },
|
||||
@@ -1043,8 +1055,10 @@ fun ZapReaction(
|
||||
zappingProgress = 0f
|
||||
},
|
||||
onChangeAmount = {
|
||||
wantsToZap = false
|
||||
wantsToChangeZapAmount = true
|
||||
scope.launch {
|
||||
wantsToZap = false
|
||||
wantsToChangeZapAmount = true
|
||||
}
|
||||
},
|
||||
onError = { _, message ->
|
||||
scope.launch {
|
||||
@@ -1249,7 +1263,6 @@ fun ObserveZapAmountText(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun BoostTypeChoicePopup(
|
||||
baseNote: Note,
|
||||
@@ -1259,62 +1272,120 @@ private fun BoostTypeChoicePopup(
|
||||
onQuote: () -> Unit,
|
||||
onRepost: () -> Unit,
|
||||
onFork: () -> Unit,
|
||||
) {
|
||||
val visibilityState = rememberVisibilityState(onDismiss)
|
||||
BoostTypeChoicePopup(
|
||||
baseNote,
|
||||
iconSize,
|
||||
accountViewModel,
|
||||
visibilityState,
|
||||
onQuote,
|
||||
onRepost,
|
||||
onFork,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun BoostTypeChoicePopup(
|
||||
baseNote: Note,
|
||||
iconSize: Dp,
|
||||
accountViewModel: AccountViewModel,
|
||||
visibilityState: MutableTransitionState<Boolean>,
|
||||
onQuote: () -> Unit,
|
||||
onRepost: () -> Unit,
|
||||
onFork: () -> Unit,
|
||||
) {
|
||||
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, iconSizePx),
|
||||
onDismissRequest = { onDismiss() },
|
||||
onDismissRequest = { visibilityState.targetState = false },
|
||||
) {
|
||||
FlowRow {
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
accountViewModel.boost(baseNote)
|
||||
onDismiss()
|
||||
} else {
|
||||
onRepost()
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.boost), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visibleState = visibilityState,
|
||||
enter = popupAnimationEnter,
|
||||
exit = popupAnimationExit,
|
||||
) {
|
||||
FlowRow {
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = {
|
||||
if (accountViewModel.isWriteable()) {
|
||||
accountViewModel.boost(baseNote)
|
||||
visibilityState.targetState = false
|
||||
} else {
|
||||
onRepost()
|
||||
visibilityState.targetState = false
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.boost), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onQuote,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onQuote,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onFork,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center)
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onFork,
|
||||
shape = ButtonBorder,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
) {
|
||||
Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val popupAnimationEnter: EnterTransition =
|
||||
slideInVertically(
|
||||
animationSpec = defaultTweenIntOffsetSpec,
|
||||
initialOffsetY = { it / 2 },
|
||||
) + fadeIn(animationSpec = defaultTweenFloatSpec)
|
||||
|
||||
val popupAnimationExit: ExitTransition =
|
||||
slideOutVertically(
|
||||
animationSpec = defaultTweenIntOffsetSpec,
|
||||
targetOffsetY = { it / 2 },
|
||||
) + fadeOut(animationSpec = defaultTweenFloatSpec)
|
||||
|
||||
@Composable
|
||||
fun rememberVisibilityState(onDismiss: () -> Unit): MutableTransitionState<Boolean> {
|
||||
// Prevent multiple calls to onDismiss()
|
||||
var dismissed by remember { mutableStateOf(false) }
|
||||
|
||||
val visibilityState = remember { MutableTransitionState(false).apply { targetState = true } }
|
||||
LaunchedEffect(visibilityState.targetState) {
|
||||
if (!visibilityState.targetState && !dismissed) {
|
||||
delay(defaultTweenDuration.toLong())
|
||||
dismissed = true
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
return visibilityState
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReactionChoicePopup(
|
||||
baseNote: Note,
|
||||
@@ -1322,6 +1393,18 @@ fun ReactionChoicePopup(
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
onChangeAmount: () -> Unit,
|
||||
) {
|
||||
val visibilityState = rememberVisibilityState(onDismiss)
|
||||
ReactionChoicePopup(baseNote, iconSize, accountViewModel, visibilityState, onChangeAmount)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReactionChoicePopup(
|
||||
baseNote: Note,
|
||||
iconSize: Dp,
|
||||
accountViewModel: AccountViewModel,
|
||||
visibilityState: MutableTransitionState<Boolean>,
|
||||
onChangeAmount: () -> Unit,
|
||||
) {
|
||||
val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() }
|
||||
|
||||
@@ -1329,22 +1412,6 @@ fun ReactionChoicePopup(
|
||||
.collectAsStateWithLifecycle()
|
||||
val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() }
|
||||
|
||||
// Define animation specs
|
||||
val animationDuration = 250
|
||||
val fadeAnimationSpec = tween<Float>(durationMillis = animationDuration)
|
||||
|
||||
// Prevent multiple calls to onDismiss()
|
||||
var dismissed by remember { mutableStateOf(false) }
|
||||
|
||||
val visibilityState = remember { MutableTransitionState(false).apply { targetState = true } }
|
||||
LaunchedEffect(visibilityState.targetState) {
|
||||
if (!visibilityState.targetState && !dismissed) {
|
||||
delay(animationDuration.toLong())
|
||||
dismissed = true
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, iconSizePx),
|
||||
@@ -1353,14 +1420,8 @@ fun ReactionChoicePopup(
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visibleState = visibilityState,
|
||||
enter =
|
||||
slideInVertically(
|
||||
initialOffsetY = { it / 2 },
|
||||
) + fadeIn(animationSpec = fadeAnimationSpec),
|
||||
exit =
|
||||
slideOutVertically(
|
||||
targetOffsetY = { it / 2 },
|
||||
) + fadeOut(animationSpec = fadeAnimationSpec),
|
||||
enter = popupAnimationEnter,
|
||||
exit = popupAnimationExit,
|
||||
) {
|
||||
ReactionChoicePopupContent(
|
||||
reactions,
|
||||
@@ -1518,7 +1579,6 @@ fun ZapAmountChoicePopup(
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, onDismiss, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ZapAmountChoicePopup(
|
||||
baseNote: Note,
|
||||
@@ -1530,28 +1590,29 @@ fun ZapAmountChoicePopup(
|
||||
onError: (title: String, text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
) {
|
||||
val visibilityState = rememberVisibilityState(onDismiss)
|
||||
ZapAmountChoicePopup(baseNote, zapAmountChoices, accountViewModel, popupYOffset, visibilityState, onChangeAmount, onError, onProgress, onPayViaIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ZapAmountChoicePopup(
|
||||
baseNote: Note,
|
||||
zapAmountChoices: ImmutableList<Long>,
|
||||
accountViewModel: AccountViewModel,
|
||||
popupYOffset: Dp,
|
||||
visibilityState: MutableTransitionState<Boolean>,
|
||||
onChangeAmount: () -> Unit,
|
||||
onError: (title: String, text: String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit,
|
||||
onPayViaIntent: (ImmutableList<ZapPaymentHandler.Payable>) -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val zapMessage = ""
|
||||
|
||||
val yOffset = with(LocalDensity.current) { -popupYOffset.toPx().toInt() }
|
||||
|
||||
// Define animation specs
|
||||
val animationDuration = 250
|
||||
val fadeAnimationSpec = tween<Float>(durationMillis = animationDuration)
|
||||
|
||||
// Prevent multiple calls to onDismiss()
|
||||
var dismissed by remember { mutableStateOf(false) }
|
||||
|
||||
val visibilityState = remember { MutableTransitionState(false).apply { targetState = true } }
|
||||
LaunchedEffect(visibilityState.targetState) {
|
||||
if (!visibilityState.targetState && !dismissed) {
|
||||
delay(animationDuration.toLong())
|
||||
dismissed = true
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.BottomCenter,
|
||||
offset = IntOffset(0, yOffset),
|
||||
@@ -1560,14 +1621,8 @@ fun ZapAmountChoicePopup(
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visibleState = visibilityState,
|
||||
enter =
|
||||
slideInVertically(
|
||||
initialOffsetY = { it / 2 },
|
||||
) + fadeIn(animationSpec = fadeAnimationSpec),
|
||||
exit =
|
||||
slideOutVertically(
|
||||
targetOffsetY = { it / 2 },
|
||||
) + fadeOut(animationSpec = fadeAnimationSpec),
|
||||
enter = popupAnimationEnter,
|
||||
exit = popupAnimationExit,
|
||||
) {
|
||||
FlowRow(horizontalArrangement = Arrangement.Center) {
|
||||
zapAmountChoices.forEach { amountInSats ->
|
||||
|
||||
+1
-1
@@ -1267,7 +1267,7 @@ class AccountViewModel(
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
Log.d(
|
||||
"Rendering Metrics",
|
||||
"Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()}",
|
||||
"Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()} with ${newNotes.size} new notes",
|
||||
)
|
||||
feedStates.updateFeedsWith(newNotes)
|
||||
upgradeAttestations()
|
||||
|
||||
+2
@@ -26,6 +26,7 @@ import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
@@ -43,6 +44,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
class SearchBarViewModel(
|
||||
val account: Account,
|
||||
) : ViewModel() {
|
||||
val focusRequester = FocusRequester()
|
||||
var searchValue by mutableStateOf("")
|
||||
|
||||
private var _searchResultsUsers = MutableStateFlow<List<User>>(emptyList())
|
||||
|
||||
+5
-12
@@ -52,7 +52,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
@@ -91,7 +90,6 @@ import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -148,6 +146,10 @@ fun SearchScreen(
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(searchBarViewModel.focusRequester) {
|
||||
searchBarViewModel.focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
DisappearingScaffold(
|
||||
isInvertedLayout = false,
|
||||
topBar = {
|
||||
@@ -234,15 +236,6 @@ private fun SearchTextField(
|
||||
modifier: Modifier,
|
||||
onTextChanges: (String) -> Unit,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
delay(100)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier.padding(10.dp).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -264,7 +257,7 @@ private fun SearchTextField(
|
||||
Modifier
|
||||
.weight(1f, true)
|
||||
.defaultMinSize(minHeight = 20.dp)
|
||||
.focusRequester(focusRequester),
|
||||
.focusRequester(searchBarViewModel.focusRequester),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(R.string.npub_hex_username),
|
||||
|
||||
+22
-15
@@ -27,6 +27,7 @@ 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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -196,9 +197,9 @@ import com.vitorpamplona.quartz.events.TorrentCommentEvent
|
||||
import com.vitorpamplona.quartz.events.TorrentEvent
|
||||
import com.vitorpamplona.quartz.events.VideoEvent
|
||||
import com.vitorpamplona.quartz.events.WikiNoteEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -235,26 +236,32 @@ fun RenderThreadFeed(
|
||||
nav: INav,
|
||||
) {
|
||||
val items by loaded.feed.collectAsStateWithLifecycle()
|
||||
val firstTimeScrolled = remember { TimeUtils.now() }
|
||||
|
||||
LaunchedEffect(noteId) {
|
||||
// waits to load the thread to scroll to item.
|
||||
delay(100)
|
||||
val noteForPosition =
|
||||
items.list
|
||||
.filter { it.idHex == noteId }
|
||||
.firstOrNull()
|
||||
var position = items.list.indexOf(noteForPosition)
|
||||
|
||||
if (position >= 0) {
|
||||
if (position >= 1 && position < items.list.size - 1) {
|
||||
position-- // show the replying note
|
||||
LaunchedEffect(noteId, items.list) {
|
||||
// hack to allow multiple scrolls to Item while posts on the screen load.
|
||||
// This is important when clicking on a reply of an older thread in Notifications
|
||||
// In that case, this screen will open with 0-1 items, and the scrollToItem below
|
||||
// will not change the state of the screen (too few items, scroll is not available)
|
||||
// as the app loads the reaming of the thread the position of the reply changes
|
||||
// and becuase there wasn't a possibility to scroll before and now there is one,
|
||||
// the screen stays at the top. Once the thread has enough replies, the lazy column
|
||||
// updates with new items correctly. It just needs a few items to start the scrool.
|
||||
//
|
||||
// This hack allows the list 1 second to fill up with more
|
||||
// records before setting up the position on the feed.
|
||||
//
|
||||
// It jumps around, but it is the best we can do.
|
||||
if (TimeUtils.now() - firstTimeScrolled < 1000) {
|
||||
val position = items.list.indexOfFirst { it.idHex == noteId }
|
||||
if (position >= 0) {
|
||||
listState.scrollToItem(position, -200)
|
||||
}
|
||||
|
||||
listState.scrollToItem(position)
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = FeedPadding,
|
||||
state = listState,
|
||||
) {
|
||||
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2024 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.video
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.compose.foundation.MutatePriority
|
||||
import androidx.compose.foundation.gestures.ScrollScope
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* This file only exists to fix an interference between the Disappearing Top
|
||||
* and Bottom Scaffold bars and the offsetFraction of the pager. The current
|
||||
* implementation ends the scroll at a state where some fraction is still present
|
||||
* which places videos away from their natural position in the page.
|
||||
*
|
||||
* The fix simply animates it back to the fraction = 0.
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun myRememberPagerState(
|
||||
initialPage: Int = 0,
|
||||
@FloatRange(from = -0.5, to = 0.5) initialPageOffsetFraction: Float = 0f,
|
||||
pageCount: () -> Int,
|
||||
): PagerState =
|
||||
rememberSaveable(saver = DefaultPagerState.Saver) {
|
||||
DefaultPagerState(
|
||||
initialPage,
|
||||
initialPageOffsetFraction,
|
||||
pageCount,
|
||||
)
|
||||
}.apply {
|
||||
pageCountState.value = pageCount
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun myRememberForeverPagerState(
|
||||
key: String,
|
||||
initialFirstVisibleItemIndex: Int = 0,
|
||||
initialFirstVisibleItemScrollOffset: Float = 0.0f,
|
||||
pageCount: () -> Int,
|
||||
): PagerState =
|
||||
rememberForeverPagerState(key, initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset, pageCount) { initialPage, initialPageOffsetFraction, pageCount ->
|
||||
myRememberPagerState(initialPage, initialPageOffsetFraction, pageCount)
|
||||
}
|
||||
|
||||
private class DefaultPagerState(
|
||||
currentPage: Int,
|
||||
currentPageOffsetFraction: Float,
|
||||
updatedPageCount: () -> Int,
|
||||
) : PagerState(currentPage, currentPageOffsetFraction) {
|
||||
var pageCountState = mutableStateOf(updatedPageCount)
|
||||
override val pageCount: Int get() = pageCountState.value.invoke()
|
||||
|
||||
override suspend fun scroll(
|
||||
scrollPriority: MutatePriority,
|
||||
block: suspend ScrollScope.() -> Unit,
|
||||
) {
|
||||
super.scroll(scrollPriority, block)
|
||||
if (abs(currentPageOffsetFraction) > 0) {
|
||||
animateScrollToPage(currentPage, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* To keep current page and current page offset saved
|
||||
*/
|
||||
val Saver: Saver<DefaultPagerState, *> =
|
||||
listSaver(
|
||||
save = {
|
||||
listOf(
|
||||
it.currentPage,
|
||||
(it.currentPageOffsetFraction).coerceIn(-0.5f, 0.5f),
|
||||
it.pageCount,
|
||||
)
|
||||
},
|
||||
restore = {
|
||||
DefaultPagerState(
|
||||
currentPage = it[0] as Int,
|
||||
currentPageOffsetFraction = it[1] as Float,
|
||||
updatedPageCount = { it[2] as Int },
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.VerticalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -67,6 +68,7 @@ import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop
|
||||
import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState
|
||||
import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
@@ -245,9 +247,9 @@ fun SlidingCarousel(
|
||||
|
||||
val pagerState =
|
||||
if (pagerStateKey != null) {
|
||||
myRememberForeverPagerState(pagerStateKey, items.list.size) { items.list.size }
|
||||
rememberForeverPagerState(pagerStateKey, items.list.size) { items.list.size }
|
||||
} else {
|
||||
myRememberPagerState(items.list.size) { items.list.size }
|
||||
rememberPagerState(items.list.size) { items.list.size }
|
||||
}
|
||||
|
||||
WatchScrollToTop(videoFeedContentState, pagerState)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.theme
|
||||
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -40,6 +41,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Shapes =
|
||||
@@ -287,3 +289,7 @@ val reactionBox =
|
||||
.padding(5.dp)
|
||||
|
||||
val ripple24dp = ripple(bounded = false, radius = Size24dp)
|
||||
|
||||
val defaultTweenDuration = 100
|
||||
val defaultTweenFloatSpec = tween<Float>(durationMillis = defaultTweenDuration)
|
||||
val defaultTweenIntOffsetSpec = tween<IntOffset>(durationMillis = defaultTweenDuration)
|
||||
|
||||
@@ -520,6 +520,7 @@
|
||||
<string name="light">Clair</string>
|
||||
<string name="dark">Sombre</string>
|
||||
<string name="application_preferences">Préférences de l\'application</string>
|
||||
<string name="wallet_connect">Wallet Connect</string>
|
||||
<string name="language">Langage</string>
|
||||
<string name="theme">Thème</string>
|
||||
<string name="automatically_load_images_gifs">Prévisualisation des images</string>
|
||||
|
||||
@@ -520,6 +520,7 @@
|
||||
<string name="light">प्रकाशवान</string>
|
||||
<string name="dark">अन्धकारमय</string>
|
||||
<string name="application_preferences">क्रमक आद्यताएँ</string>
|
||||
<string name="wallet_connect">धनकोष संयोजन</string>
|
||||
<string name="language">भाषा</string>
|
||||
<string name="theme">प्रदर्शनशैली</string>
|
||||
<string name="automatically_load_images_gifs">चित्र पूर्वीक्षण</string>
|
||||
|
||||
@@ -520,6 +520,7 @@
|
||||
<string name="light">Licht</string>
|
||||
<string name="dark">Donker</string>
|
||||
<string name="application_preferences">Application preferences</string>
|
||||
<string name="wallet_connect">Wallet Connect</string>
|
||||
<string name="language">Taal</string>
|
||||
<string name="theme">Thema</string>
|
||||
<string name="automatically_load_images_gifs">Automatisch afbeeldingen/gifs laden</string>
|
||||
|
||||
@@ -520,6 +520,7 @@
|
||||
<string name="light">Jasny</string>
|
||||
<string name="dark">Ciemny</string>
|
||||
<string name="application_preferences">Ustawienia Aplikacji</string>
|
||||
<string name="wallet_connect">Podłącz portfel</string>
|
||||
<string name="language">Język</string>
|
||||
<string name="theme">Motyw</string>
|
||||
<string name="automatically_load_images_gifs">Podgląd obrazu</string>
|
||||
|
||||
@@ -9,6 +9,7 @@ Prijavi se s privatnim ključem za všečkanje sporočila</string>
|
||||
<string name="public_chat_feed">Javni vir novic</string>
|
||||
<string name="add_a_relay">Dodaj rele</string>
|
||||
<string name="display_name">Prikazno ime</string>
|
||||
<string name="uploading">Nalaganje …</string>
|
||||
<string name="npub_hex_username">"npub, uporabniško ime, tekst"</string>
|
||||
<string name="quick_action_share_browser_link">Deli povezavo brskalnika</string>
|
||||
<string name="quick_action_request_deletion_gallery_title">Izbris iz galerije</string>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[versions]
|
||||
accompanistAdaptive = "0.34.0"
|
||||
activityCompose = "1.9.2"
|
||||
agp = "8.7.0"
|
||||
android-compileSdk = "34"
|
||||
agp = "8.7.1"
|
||||
android-compileSdk = "35"
|
||||
android-minSdk = "26"
|
||||
android-targetSdk = "34"
|
||||
android-targetSdk = "35"
|
||||
androidKotlinGeohash = "1.0"
|
||||
androidxJunit = "1.2.1"
|
||||
appcompat = "1.7.0"
|
||||
|
||||
Reference in New Issue
Block a user