Skip to content

Commit 916cec1

Browse files
committed
Implement local and global timeline viewing
1 parent 108fca0 commit 916cec1

6 files changed

Lines changed: 149 additions & 21 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="960"
5+
android:viewportHeight="960"
6+
android:tint="?attr/colorControlNormal">
7+
<path
8+
android:fillColor="#000000"
9+
android:pathData="M600,840L360,756L174,828Q154,836 137,823.5Q120,811 120,790L120,230Q120,217 127.5,207Q135,197 148,192L360,120L600,204L786,132Q806,124 823,136.5Q840,149 840,170L840,730Q840,743 832.5,753Q825,763 812,768L600,840ZM560,742L560,274L400,218L400,686L560,742ZM640,742L760,702L760,228L640,274L640,742ZM200,732L320,686L320,218L200,258L200,732ZM640,274L640,274L640,742L640,742L640,274ZM320,218L320,218L320,686L320,686L320,218Z"/>
10+
</vector>

shared/src/commonMain/composeResources/values/strings.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,9 @@
104104
<string name="report">Report</string>
105105
<string name="edit">Edit</string>
106106
<string name="delete">Delete</string>
107+
108+
<string name="home">Home</string>
109+
<string name="local">Local</string>
110+
<string name="bubble">Bubble</string>
111+
<string name="global">Global</string>
107112
</resources>

shared/src/commonMain/kotlin/site/remlit/snowdrop/App.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import androidx.compose.runtime.getValue
4545
import androidx.compose.runtime.mutableStateOf
4646
import androidx.compose.runtime.remember
4747
import androidx.compose.runtime.setValue
48+
import androidx.compose.ui.Alignment
4849
import androidx.compose.ui.Modifier
4950
import androidx.compose.ui.draw.clip
5051
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
@@ -392,11 +393,12 @@ fun App() = safe {
392393
) {
393394
Row(
394395
modifier = Modifier.padding(10.dp),
395-
horizontalArrangement = Arrangement.spacedBy(10.dp)
396+
horizontalArrangement = Arrangement.spacedBy(10.dp),
397+
verticalAlignment = Alignment.CenterVertically
396398
) {
397399
Avatar(account!!)
398400

399-
Column(verticalArrangement = Arrangement.Center) {
401+
Column {
400402
Text(
401403
account!!.displayName ?: account!!.url,
402404
fontWeight = FontWeight.Medium

shared/src/commonMain/kotlin/site/remlit/snowdrop/model/Status.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import kotlin.time.Instant
77

88
@Serializable
99
data class Status(
10-
val id: String? = null,
10+
val id: String,
1111
val account: Account? = null,
1212

1313
@SerialName("spoiler_text")

shared/src/commonMain/kotlin/site/remlit/snowdrop/util/Features.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ suspend fun determineFeatures() {
6969
) putFeature("reactions", true)
7070
else putFeature("reactions", false)
7171

72+
// todo: detect bubble timeline support
73+
putFeature("bubble_timeline", false)
74+
7275
determiningFeatures = false
7376
}
7477

shared/src/commonMain/kotlin/site/remlit/snowdrop/view/TimelineView.kt

Lines changed: 126 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ package site.remlit.snowdrop.view
33
import androidx.compose.foundation.clickable
44
import androidx.compose.foundation.interaction.MutableInteractionSource
55
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Box
67
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.Row
79
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.padding
811
import androidx.compose.foundation.lazy.LazyColumn
912
import androidx.compose.foundation.lazy.LazyListState
1013
import androidx.compose.foundation.lazy.items
1114
import androidx.compose.foundation.lazy.rememberLazyListState
15+
import androidx.compose.material3.DropdownMenu
16+
import androidx.compose.material3.DropdownMenuItem
1217
import androidx.compose.material3.Icon
1318
import androidx.compose.material3.IconButton
1419
import androidx.compose.material3.Text
@@ -17,36 +22,59 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
1722
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
1823
import androidx.compose.runtime.Composable
1924
import androidx.compose.runtime.LaunchedEffect
25+
import androidx.compose.runtime.getValue
2026
import androidx.compose.runtime.mutableStateListOf
2127
import androidx.compose.runtime.mutableStateOf
2228
import androidx.compose.runtime.remember
2329
import androidx.compose.runtime.rememberCoroutineScope
30+
import androidx.compose.runtime.setValue
2431
import androidx.compose.runtime.snapshotFlow
2532
import androidx.compose.ui.Alignment
2633
import androidx.compose.ui.Modifier
2734
import androidx.compose.ui.geometry.Offset
2835
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
2936
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
3037
import androidx.compose.ui.input.nestedscroll.nestedScroll
38+
import androidx.compose.ui.unit.dp
39+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
40+
import com.russhwolf.settings.ExperimentalSettingsApi
3141
import kotlinx.coroutines.flow.collect
3242
import kotlinx.coroutines.flow.distinctUntilChanged
3343
import kotlinx.coroutines.flow.filter
3444
import kotlinx.coroutines.flow.map
3545
import kotlinx.coroutines.flow.onEach
3646
import kotlinx.coroutines.launch
47+
import kotlinx.coroutines.runBlocking
3748
import org.jetbrains.compose.resources.painterResource
3849
import org.jetbrains.compose.resources.stringResource
3950
import site.remlit.snowdrop.SettingsRoute
4051
import site.remlit.snowdrop.api.timeline.getHomeTimeline
52+
import site.remlit.snowdrop.api.timeline.getPublicTimeline
4153
import site.remlit.snowdrop.component.Status
4254
import site.remlit.snowdrop.component.ViewSurface
55+
import site.remlit.snowdrop.model.ApiResponse
4356
import site.remlit.snowdrop.model.Status
4457
import site.remlit.snowdrop.util.LocalNavController
58+
import site.remlit.snowdrop.util.bg
59+
import site.remlit.snowdrop.util.blockingSettings
60+
import site.remlit.snowdrop.util.getFeature
4561
import site.remlit.snowdrop.util.scrollingUpward
62+
import site.remlit.snowdrop.util.settings
4663
import snowdrop.shared.generated.resources.Res
4764
import snowdrop.shared.generated.resources.add_account
65+
import snowdrop.shared.generated.resources.bubble
66+
import snowdrop.shared.generated.resources.global
67+
import snowdrop.shared.generated.resources.home
68+
import snowdrop.shared.generated.resources.icon_bubble_chart_24px
69+
import snowdrop.shared.generated.resources.icon_globe_24px
70+
import snowdrop.shared.generated.resources.icon_home_24px
71+
import snowdrop.shared.generated.resources.icon_keyboard_arrow_down_24px
72+
import snowdrop.shared.generated.resources.icon_keyboard_arrow_up_24px
73+
import snowdrop.shared.generated.resources.icon_map_24px
4874
import snowdrop.shared.generated.resources.icon_settings_24px
75+
import snowdrop.shared.generated.resources.local
4976
import snowdrop.shared.generated.resources.timeline
77+
import kotlin.math.sin
5078

5179
@Composable
5280
inline fun LazyListState.ScrollEndCallback(crossinline callback: () -> Unit) {
@@ -63,6 +91,7 @@ inline fun LazyListState.ScrollEndCallback(crossinline callback: () -> Unit) {
6391
}
6492
}
6593

94+
@OptIn(ExperimentalSettingsApi::class)
6695
@Composable
6796
fun TimelineView() = ViewSurface {
6897
Column(
@@ -72,47 +101,131 @@ fun TimelineView() = ViewSurface {
72101
val navHandler = LocalNavController.current
73102
val coroutineScope = rememberCoroutineScope()
74103

104+
// 0 - home, 1 - local, 2 - bubble, 3 - global
105+
val timelineType by settings.getIntFlow("timeline", 0)
106+
.collectAsStateWithLifecycle(0)
107+
var timelinePickerOpen by remember { mutableStateOf(false) }
108+
109+
suspend fun getTimeline(
110+
maxId: String? = null,
111+
minId: String? = null,
112+
sinceId: String? = null
113+
): ApiResponse<List<Status>> {
114+
return when (timelineType) {
115+
0 -> getHomeTimeline(maxId = maxId, minId = minId, sinceId = sinceId)
116+
1 -> getPublicTimeline(maxId = maxId, minId = minId, sinceId = sinceId, local = true)
117+
2 -> getPublicTimeline(maxId = maxId, minId = minId, sinceId = sinceId, remote = true) // todo: fix bubble timeline
118+
else -> getPublicTimeline(maxId = maxId, minId = minId, sinceId = sinceId, remote = true) // else also 3
119+
}
120+
}
121+
75122
val timeline = remember { mutableStateListOf<Status>() }
76123
val refreshState = rememberPullToRefreshState()
77124
val listState = rememberLazyListState().also {
78125
it.ScrollEndCallback {
79126
coroutineScope.launch {
80-
val res = getHomeTimeline(maxId = timeline.last().id)
127+
val res = getTimeline(maxId = timeline.last().id)
81128
if (res.error) return@launch
82129
if (res.response == null) return@launch
83130
timeline.addAll(res.response)
84131
}
85132
}
86133
}
87-
val isRefreshing = remember { mutableStateOf(false) }
134+
var isRefreshing by remember { mutableStateOf(false) }
88135

89136
suspend fun addOrUpdateTimeline() {
90-
isRefreshing.value = true
91-
val res = getHomeTimeline()
137+
isRefreshing = true
138+
val res = getTimeline()
92139
if (res.error) return
93140
if (res.response == null) return
94141
timeline.clear()
95142
timeline.addAll(res.response)
96143
listState.scrollToItem(0)
97-
isRefreshing.value = false
144+
isRefreshing = false
145+
}
146+
147+
// whenever the timelineType changes, this function runs.
148+
LaunchedEffect(timelineType) { addOrUpdateTimeline() }
149+
150+
@Composable
151+
fun RenderTimelineTypeIcon(type: Int? = null) {
152+
when (type ?: timelineType) {
153+
0 -> Icon(painterResource(Res.drawable.icon_home_24px), null)
154+
1 -> Icon(painterResource(Res.drawable.icon_map_24px), null)
155+
2 -> Icon(painterResource(Res.drawable.icon_bubble_chart_24px), null)
156+
3 -> Icon(painterResource(Res.drawable.icon_globe_24px), null)
157+
}
98158
}
99159

100-
LaunchedEffect(Unit) {
101-
addOrUpdateTimeline()
160+
@Composable
161+
fun RenderTimelineSelectionDropdown() {
162+
DropdownMenu(
163+
expanded = timelinePickerOpen,
164+
onDismissRequest = { timelinePickerOpen = false },
165+
) {
166+
DropdownMenuItem(
167+
leadingIcon = { RenderTimelineTypeIcon(0) },
168+
text = { Text(stringResource(Res.string.home)) },
169+
onClick = { blockingSettings.putInt("timeline", 0); timelinePickerOpen = false }
170+
)
171+
DropdownMenuItem(
172+
leadingIcon = { RenderTimelineTypeIcon(1) },
173+
text = { Text(stringResource(Res.string.local)) },
174+
onClick = { blockingSettings.putInt("timeline", 1); timelinePickerOpen = false }
175+
)
176+
if (getFeature("bubble_timeline"))
177+
DropdownMenuItem(
178+
leadingIcon = { RenderTimelineTypeIcon(2) },
179+
text = { Text(stringResource(Res.string.bubble)) },
180+
onClick = { blockingSettings.putInt("timeline", 2); timelinePickerOpen = false }
181+
)
182+
DropdownMenuItem(
183+
leadingIcon = { RenderTimelineTypeIcon(3) },
184+
text = { Text(stringResource(Res.string.global)) },
185+
onClick = { blockingSettings.putInt("timeline", 3); timelinePickerOpen = false }
186+
)
187+
}
102188
}
103189

190+
191+
/*
192+
*
193+
* Actual Timeline View
194+
*
195+
*/
104196
TopAppBar(
105197
modifier = Modifier.clickable(
106198
interactionSource = MutableInteractionSource(),
107199
indication = null,
108200
onClick = {
201+
/*
109202
coroutineScope.launch {
110203
listState.animateScrollToItem(0)
111204
}
205+
*/
206+
207+
timelinePickerOpen = !timelinePickerOpen
112208
}
113209
),
114210
title = {
115-
Text(stringResource(Res.string.timeline))
211+
Row(
212+
horizontalArrangement = Arrangement.spacedBy(10.dp),
213+
verticalAlignment = Alignment.CenterVertically
214+
) {
215+
RenderTimelineTypeIcon()
216+
217+
when (timelineType) {
218+
0 -> Text(stringResource(Res.string.home))
219+
1 -> Text(stringResource(Res.string.local))
220+
2 -> Text(stringResource(Res.string.bubble))
221+
3 -> Text(stringResource(Res.string.global))
222+
}
223+
224+
if (timelinePickerOpen) Icon(painterResource(Res.drawable.icon_keyboard_arrow_up_24px), null)
225+
else Icon(painterResource(Res.drawable.icon_keyboard_arrow_down_24px), null)
226+
227+
RenderTimelineSelectionDropdown()
228+
}
116229
},
117230
actions = {
118231
IconButton(onClick = { navHandler.navigate(SettingsRoute) }) {
@@ -122,7 +235,7 @@ fun TimelineView() = ViewSurface {
122235
)
123236

124237
PullToRefreshBox(
125-
isRefreshing = isRefreshing.value,
238+
isRefreshing = isRefreshing,
126239
state = refreshState,
127240
onRefresh = {
128241
coroutineScope.launch {
@@ -131,6 +244,7 @@ fun TimelineView() = ViewSurface {
131244
}
132245
}
133246
) {
247+
// for determining if the compose FAB should be visible
134248
val nestedScrollConnection = remember {
135249
object : NestedScrollConnection {
136250
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
@@ -144,18 +258,12 @@ fun TimelineView() = ViewSurface {
144258

145259
LazyColumn(
146260
state = listState,
147-
modifier = Modifier
148-
.fillMaxSize()
149-
.nestedScroll(nestedScrollConnection),
261+
modifier = Modifier.fillMaxSize().nestedScroll(nestedScrollConnection),
150262
) {
151263
items(
152264
items = timeline,
153-
key = { status ->
154-
status.id!!
155-
}
156-
) { status ->
157-
Status(status)
158-
}
265+
key = { it.id }
266+
) { Status(it) }
159267
}
160268
}
161269
}

0 commit comments

Comments
 (0)