gonna do settings now
This commit is contained in:
@@ -28,6 +28,7 @@ class PreferencesManager(private val context: Context) {
|
|||||||
private val KEY_PASSWORD = stringPreferencesKey("password")
|
private val KEY_PASSWORD = stringPreferencesKey("password")
|
||||||
private val KEY_IS_LOGGED_IN = booleanPreferencesKey("is_logged_in")
|
private val KEY_IS_LOGGED_IN = booleanPreferencesKey("is_logged_in")
|
||||||
private val KEY_CHANNEL_ORDER = stringPreferencesKey("channel_order")
|
private val KEY_CHANNEL_ORDER = stringPreferencesKey("channel_order")
|
||||||
|
private val KEY_CHILD_SPACE_ORDER = stringPreferencesKey("child_space_order")
|
||||||
}
|
}
|
||||||
|
|
||||||
val isLoggedIn: Flow<Boolean> = context.dataStore.data.map { prefs ->
|
val isLoggedIn: Flow<Boolean> = context.dataStore.data.map { prefs ->
|
||||||
@@ -98,6 +99,24 @@ class PreferencesManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val childSpaceOrder: Flow<Map<String, List<String>>> = context.dataStore.data.map { prefs ->
|
||||||
|
val raw = prefs[KEY_CHILD_SPACE_ORDER] ?: return@map emptyMap()
|
||||||
|
try {
|
||||||
|
Json.decodeFromString<Map<String, List<String>>>(raw)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
emptyMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveChildSpaceOrder(parentSpaceId: String, childSpaceIds: List<String>) {
|
||||||
|
context.dataStore.edit { prefs ->
|
||||||
|
val existing = prefs[KEY_CHILD_SPACE_ORDER]?.let {
|
||||||
|
try { Json.decodeFromString<Map<String, List<String>>>(it) } catch (_: Exception) { emptyMap() }
|
||||||
|
} ?: emptyMap()
|
||||||
|
prefs[KEY_CHILD_SPACE_ORDER] = Json.encodeToString(existing + (parentSpaceId to childSpaceIds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun clearSession() {
|
suspend fun clearSession() {
|
||||||
context.dataStore.edit { it.clear() }
|
context.dataStore.edit { it.clear() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ fun MainScreen(
|
|||||||
val homeUnreadStatus by viewModel.homeUnreadStatus.collectAsState()
|
val homeUnreadStatus by viewModel.homeUnreadStatus.collectAsState()
|
||||||
val channelSections by viewModel.channelSections.collectAsState()
|
val channelSections by viewModel.channelSections.collectAsState()
|
||||||
val unreadMarkerIndex by viewModel.unreadMarkerIndex.collectAsState()
|
val unreadMarkerIndex by viewModel.unreadMarkerIndex.collectAsState()
|
||||||
|
val listState = viewModel.channelListState
|
||||||
|
|
||||||
// Back button opens channel list when in a chat, or does nothing if already open
|
// Back button opens channel list when in a chat, or does nothing if already open
|
||||||
BackHandler(enabled = selectedChannel != null && !showChannelList) {
|
BackHandler(enabled = selectedChannel != null && !showChannelList) {
|
||||||
@@ -124,9 +125,11 @@ fun MainScreen(
|
|||||||
onLogout()
|
onLogout()
|
||||||
},
|
},
|
||||||
contentPadding = padding,
|
contentPadding = padding,
|
||||||
|
listState = listState,
|
||||||
isReorderMode = isReorderMode,
|
isReorderMode = isReorderMode,
|
||||||
onToggleReorderMode = { viewModel.toggleReorderMode() },
|
onToggleReorderMode = { viewModel.toggleReorderMode() },
|
||||||
onMoveChannel = { from, to -> viewModel.moveChannel(from, to) },
|
onMoveChannel = { from, to -> viewModel.moveChannel(from, to) },
|
||||||
|
onMoveChildSpace = { from, to -> viewModel.moveChildSpace(from, to) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import com.example.fluffytrix.data.repository.AuthRepository
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -114,10 +116,13 @@ class MainViewModel(
|
|||||||
private val _channelName = MutableStateFlow<String?>(null)
|
private val _channelName = MutableStateFlow<String?>(null)
|
||||||
val channelName: StateFlow<String?> = _channelName
|
val channelName: StateFlow<String?> = _channelName
|
||||||
|
|
||||||
|
val channelListState = LazyListState()
|
||||||
|
|
||||||
private val _isReorderMode = MutableStateFlow(false)
|
private val _isReorderMode = MutableStateFlow(false)
|
||||||
val isReorderMode: StateFlow<Boolean> = _isReorderMode
|
val isReorderMode: StateFlow<Boolean> = _isReorderMode
|
||||||
|
|
||||||
private val _channelOrderMap = MutableStateFlow<Map<String, List<String>>>(emptyMap())
|
private val _channelOrderMap = MutableStateFlow<Map<String, List<String>>>(emptyMap())
|
||||||
|
private val _childSpaceOrderMap = MutableStateFlow<Map<String, List<String>>>(emptyMap())
|
||||||
|
|
||||||
private val _allChannelRooms = MutableStateFlow<List<ChannelItem>>(emptyList())
|
private val _allChannelRooms = MutableStateFlow<List<ChannelItem>>(emptyList())
|
||||||
private val _spaceChildren = MutableStateFlow<Set<String>?>(emptySet())
|
private val _spaceChildren = MutableStateFlow<Set<String>?>(emptySet())
|
||||||
@@ -175,6 +180,9 @@ class MainViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
preferencesManager.channelOrder.collect { _channelOrderMap.value = it }
|
preferencesManager.channelOrder.collect { _channelOrderMap.value = it }
|
||||||
}
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
preferencesManager.childSpaceOrder.collect { _childSpaceOrderMap.value = it }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadRooms() {
|
private fun loadRooms() {
|
||||||
@@ -289,7 +297,8 @@ class MainViewModel(
|
|||||||
combine(
|
combine(
|
||||||
combine(_allChannelRooms, _spaceChildren, _selectedSpace) { a, b, c -> Triple(a, b, c) },
|
combine(_allChannelRooms, _spaceChildren, _selectedSpace) { a, b, c -> Triple(a, b, c) },
|
||||||
combine(_channelOrderMap, _roomUnreadStatus, _directChildSpaces) { a, b, c -> Triple(a, b, c) },
|
combine(_channelOrderMap, _roomUnreadStatus, _directChildSpaces) { a, b, c -> Triple(a, b, c) },
|
||||||
) { (allChannels, children, spaceId), (orderMap, unreadMap, directChildren) ->
|
_childSpaceOrderMap,
|
||||||
|
) { (allChannels, children, spaceId), (orderMap, unreadMap, directChildren), childSpaceOrderMap ->
|
||||||
val filtered = if (children == null) allChannels
|
val filtered = if (children == null) allChannels
|
||||||
else allChannels.filter { it.id in children }
|
else allChannels.filter { it.id in children }
|
||||||
val withUnread = filtered.map { ch ->
|
val withUnread = filtered.map { ch ->
|
||||||
@@ -303,7 +312,14 @@ class MainViewModel(
|
|||||||
} else withUnread
|
} else withUnread
|
||||||
|
|
||||||
// Build sections
|
// Build sections
|
||||||
val childSpaces = spaceId?.let { directChildren[it] }
|
val rawChildSpaces = spaceId?.let { directChildren[it] }
|
||||||
|
val childSpaces = if (!rawChildSpaces.isNullOrEmpty() && spaceId != null) {
|
||||||
|
val savedCsOrder = childSpaceOrderMap[spaceId]
|
||||||
|
if (savedCsOrder != null) {
|
||||||
|
val csIndexMap = savedCsOrder.withIndex().associate { (i, id) -> id to i }
|
||||||
|
rawChildSpaces.sortedBy { csIndexMap[it.first] ?: Int.MAX_VALUE }
|
||||||
|
} else rawChildSpaces
|
||||||
|
} else rawChildSpaces
|
||||||
val sections = if (childSpaces.isNullOrEmpty()) {
|
val sections = if (childSpaces.isNullOrEmpty()) {
|
||||||
// Home view or space with no child spaces — single flat section
|
// Home view or space with no child spaces — single flat section
|
||||||
listOf(ChannelSection(channels = sorted))
|
listOf(ChannelSection(channels = sorted))
|
||||||
@@ -393,59 +409,66 @@ class MainViewModel(
|
|||||||
val cached = messageCache.getOrPut(roomId) { mutableListOf() }
|
val cached = messageCache.getOrPut(roomId) { mutableListOf() }
|
||||||
val ids = messageIds.getOrPut(roomId) { mutableSetOf() }
|
val ids = messageIds.getOrPut(roomId) { mutableSetOf() }
|
||||||
|
|
||||||
|
// SDK timeline items in SDK order — we apply diffs to this, then rebuild our cache
|
||||||
|
val sdkItems = mutableListOf<org.matrix.rustcomponents.sdk.TimelineItem>()
|
||||||
|
val mutex = kotlinx.coroutines.sync.Mutex()
|
||||||
|
|
||||||
// Register listener for live updates — must keep handle to prevent GC
|
// Register listener for live updates — must keep handle to prevent GC
|
||||||
timelineListenerHandle = timeline.addListener(object : TimelineListener {
|
timelineListenerHandle = timeline.addListener(object : TimelineListener {
|
||||||
override fun onUpdate(diff: List<TimelineDiff>) {
|
override fun onUpdate(diff: List<TimelineDiff>) {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
var dirty = false
|
mutex.withLock {
|
||||||
for (d in diff) {
|
for (d in diff) {
|
||||||
when (d) {
|
when (d) {
|
||||||
is TimelineDiff.Reset -> {
|
is TimelineDiff.Reset -> {
|
||||||
// Incrementally merge rather than full replace to avoid UI flash
|
sdkItems.clear()
|
||||||
val newIds = mutableSetOf<String>()
|
sdkItems.addAll(d.values)
|
||||||
for (item in d.values) {
|
|
||||||
val eventItem = item.asEvent() ?: continue
|
|
||||||
val eid = when (val eot = eventItem.eventOrTransactionId) {
|
|
||||||
is EventOrTransactionId.EventId -> eot.eventId
|
|
||||||
is EventOrTransactionId.TransactionId -> eot.transactionId
|
|
||||||
}
|
|
||||||
newIds.add(eid)
|
|
||||||
if (eid !in ids) {
|
|
||||||
processEventItem(roomId, eventItem, cached, ids)
|
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove messages no longer in timeline
|
|
||||||
val removed = ids.filter { it !in newIds }
|
|
||||||
if (removed.isNotEmpty()) {
|
|
||||||
for (rid in removed) ids.remove(rid)
|
|
||||||
cached.removeAll { it.eventId in removed.toSet() }
|
|
||||||
dirty = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is TimelineDiff.Clear -> {
|
is TimelineDiff.Clear -> sdkItems.clear()
|
||||||
cached.clear()
|
is TimelineDiff.Append -> sdkItems.addAll(d.values)
|
||||||
ids.clear()
|
is TimelineDiff.PushBack -> sdkItems.add(d.value)
|
||||||
dirty = true
|
is TimelineDiff.PushFront -> sdkItems.add(0, d.value)
|
||||||
|
is TimelineDiff.Insert -> {
|
||||||
|
val idx = d.index.toInt()
|
||||||
|
if (idx in 0..sdkItems.size) sdkItems.add(idx, d.value)
|
||||||
}
|
}
|
||||||
else -> {
|
is TimelineDiff.Set -> {
|
||||||
val items = when (d) {
|
val idx = d.index.toInt()
|
||||||
is TimelineDiff.Append -> d.values
|
if (idx in sdkItems.indices) sdkItems[idx] = d.value
|
||||||
is TimelineDiff.PushBack -> listOf(d.value)
|
}
|
||||||
is TimelineDiff.PushFront -> listOf(d.value)
|
is TimelineDiff.Remove -> {
|
||||||
is TimelineDiff.Insert -> listOf(d.value)
|
val idx = d.index.toInt()
|
||||||
is TimelineDiff.Set -> listOf(d.value)
|
if (idx in sdkItems.indices) sdkItems.removeAt(idx)
|
||||||
else -> emptyList()
|
}
|
||||||
}
|
is TimelineDiff.Truncate -> {
|
||||||
for (item in items) {
|
val len = d.length.toInt()
|
||||||
val eventItem = item.asEvent() ?: continue
|
while (sdkItems.size > len) sdkItems.removeAt(sdkItems.lastIndex)
|
||||||
if (processEventItem(roomId, eventItem, cached, ids)) dirty = true
|
}
|
||||||
}
|
is TimelineDiff.PopBack -> {
|
||||||
|
if (sdkItems.isNotEmpty()) sdkItems.removeAt(sdkItems.lastIndex)
|
||||||
|
}
|
||||||
|
is TimelineDiff.PopFront -> {
|
||||||
|
if (sdkItems.isNotEmpty()) sdkItems.removeAt(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dirty && _selectedChannel.value == roomId) {
|
|
||||||
|
// Rebuild cache from SDK items
|
||||||
|
cached.clear()
|
||||||
|
ids.clear()
|
||||||
|
for (item in sdkItems) {
|
||||||
|
val eventItem = item.asEvent() ?: continue
|
||||||
|
processEventItem(roomId, eventItem, cached, ids)
|
||||||
|
}
|
||||||
|
if (_selectedChannel.value == roomId) {
|
||||||
_messages.value = ArrayList(cached)
|
_messages.value = ArrayList(cached)
|
||||||
|
// Clamp unread marker to valid range — hide if it would be at/beyond the end
|
||||||
|
val marker = _unreadMarkerIndex.value
|
||||||
|
if (marker >= 0 && marker >= cached.size - 1) {
|
||||||
|
_unreadMarkerIndex.value = -1
|
||||||
|
}
|
||||||
|
sendReadReceipt(roomId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -467,6 +490,7 @@ class MainViewModel(
|
|||||||
is EventOrTransactionId.EventId -> eot.eventId
|
is EventOrTransactionId.EventId -> eot.eventId
|
||||||
is EventOrTransactionId.TransactionId -> eot.transactionId
|
is EventOrTransactionId.TransactionId -> eot.transactionId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventId in ids) return false
|
if (eventId in ids) return false
|
||||||
|
|
||||||
val content = eventItem.content
|
val content = eventItem.content
|
||||||
@@ -516,7 +540,7 @@ class MainViewModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
ids.add(eventId)
|
ids.add(eventId)
|
||||||
// Descending order (newest first at index 0) for reverse layout
|
// Descending order (newest at index 0) — reverseLayout shows index 0 at bottom
|
||||||
val insertIdx = cached.binarySearch {
|
val insertIdx = cached.binarySearch {
|
||||||
msg.timestamp.compareTo(it.timestamp)
|
msg.timestamp.compareTo(it.timestamp)
|
||||||
}.let { if (it < 0) -(it + 1) else it }
|
}.let { if (it < 0) -(it + 1) else it }
|
||||||
@@ -657,20 +681,16 @@ class MainViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendMessage(body: String) {
|
fun sendMessage(body: String) {
|
||||||
val roomId = _selectedChannel.value ?: return
|
val timeline = activeTimeline ?: return
|
||||||
val client = authRepository.getClient() ?: return
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val room = client.getRoom(roomId) ?: return@launch
|
|
||||||
val timeline = room.timeline()
|
|
||||||
timeline.send(messageEventContentFromMarkdown(body))
|
timeline.send(messageEventContentFromMarkdown(body))
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendFile(uri: Uri) {
|
fun sendFile(uri: Uri) {
|
||||||
val roomId = _selectedChannel.value ?: return
|
val timeline = activeTimeline ?: return
|
||||||
val client = authRepository.getClient() ?: return
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val contentResolver = application.contentResolver
|
val contentResolver = application.contentResolver
|
||||||
@@ -682,9 +702,6 @@ class MainViewModel(
|
|||||||
} else null
|
} else null
|
||||||
} ?: "file"
|
} ?: "file"
|
||||||
|
|
||||||
val room = client.getRoom(roomId) ?: return@launch
|
|
||||||
val timeline = room.timeline()
|
|
||||||
|
|
||||||
// Copy file to a temp path
|
// Copy file to a temp path
|
||||||
val tempFile = java.io.File(application.cacheDir, "upload_$fileName")
|
val tempFile = java.io.File(application.cacheDir, "upload_$fileName")
|
||||||
contentResolver.openInputStream(uri)?.use { input ->
|
contentResolver.openInputStream(uri)?.use { input ->
|
||||||
@@ -986,8 +1003,10 @@ class MainViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun selectChannel(channelId: String) {
|
fun selectChannel(channelId: String) {
|
||||||
// Capture unread count before clearing
|
// Place unread marker: in descending list (newest=0), marker at index (count-1)
|
||||||
|
// means it appears visually above the block of unread messages
|
||||||
val unreadCount = _roomUnreadCount.value[channelId]?.toInt() ?: 0
|
val unreadCount = _roomUnreadCount.value[channelId]?.toInt() ?: 0
|
||||||
|
// Marker goes at the last unread message, divider renders above it
|
||||||
_unreadMarkerIndex.value = if (unreadCount > 0) unreadCount - 1 else -1
|
_unreadMarkerIndex.value = if (unreadCount > 0) unreadCount - 1 else -1
|
||||||
|
|
||||||
_selectedChannel.value = channelId
|
_selectedChannel.value = channelId
|
||||||
@@ -1074,6 +1093,20 @@ class MainViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun moveChildSpace(from: Int, to: Int) {
|
||||||
|
val spaceId = _selectedSpace.value ?: return
|
||||||
|
val current = _directChildSpaces.value[spaceId]?.toMutableList() ?: return
|
||||||
|
if (from !in current.indices || to !in current.indices) return
|
||||||
|
val item = current.removeAt(from)
|
||||||
|
current.add(to, item)
|
||||||
|
_directChildSpaces.value = _directChildSpaces.value + (spaceId to current)
|
||||||
|
val childSpaceIds = current.map { it.first }
|
||||||
|
_childSpaceOrderMap.value = _childSpaceOrderMap.value + (spaceId to childSpaceIds)
|
||||||
|
viewModelScope.launch {
|
||||||
|
preferencesManager.saveChildSpaceOrder(spaceId, childSpaceIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun logout() {
|
fun logout() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try { syncService?.stop() } catch (_: Exception) { }
|
try { syncService?.stop() } catch (_: Exception) { }
|
||||||
|
|||||||
@@ -70,17 +70,22 @@ fun ChannelList(
|
|||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
onDismiss: () -> Unit = {},
|
onDismiss: () -> Unit = {},
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
|
listState: androidx.compose.foundation.lazy.LazyListState,
|
||||||
isReorderMode: Boolean = false,
|
isReorderMode: Boolean = false,
|
||||||
onToggleReorderMode: () -> Unit = {},
|
onToggleReorderMode: () -> Unit = {},
|
||||||
onMoveChannel: (from: Int, to: Int) -> Unit = { _, _ -> },
|
onMoveChannel: (from: Int, to: Int) -> Unit = { _, _ -> },
|
||||||
|
onMoveChildSpace: (from: Int, to: Int) -> Unit = { _, _ -> },
|
||||||
) {
|
) {
|
||||||
var showLogoutDialog by remember { mutableStateOf(false) }
|
var showLogoutDialog by remember { mutableStateOf(false) }
|
||||||
val collapsedSections = remember { mutableStateMapOf<String, Boolean>() }
|
val collapsedSections = remember { mutableStateMapOf<String, Boolean>() }
|
||||||
|
|
||||||
// Drag state
|
// Channel drag state
|
||||||
var draggingIndex by remember { mutableIntStateOf(-1) }
|
var draggingIndex by remember { mutableIntStateOf(-1) }
|
||||||
var dragOffsetY by remember { mutableFloatStateOf(0f) }
|
var dragOffsetY by remember { mutableFloatStateOf(0f) }
|
||||||
val listState = rememberLazyListState()
|
|
||||||
|
// Section drag state
|
||||||
|
var draggingSectionIndex by remember { mutableIntStateOf(-1) }
|
||||||
|
var sectionDragOffsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
if (showLogoutDialog) {
|
if (showLogoutDialog) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
@@ -157,12 +162,20 @@ fun ChannelList(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
|
||||||
) {
|
) {
|
||||||
|
// Build index of child space sections (excluding the direct-rooms section which has no spaceId)
|
||||||
|
val childSpaceSections = sections.withIndex().filter { it.value.spaceId != null }
|
||||||
|
val childSpaceSectionIndexMap = childSpaceSections.withIndex().associate { (csIdx, iv) -> iv.index to csIdx }
|
||||||
|
|
||||||
for ((sectionIdx, section) in sections.withIndex()) {
|
for ((sectionIdx, section) in sections.withIndex()) {
|
||||||
// Section header for child spaces
|
// Section header for child spaces
|
||||||
if (section.spaceName != null && section.spaceId != null) {
|
if (section.spaceName != null && section.spaceId != null) {
|
||||||
item(key = "section_${section.spaceId}") {
|
item(key = "section_${section.spaceId}") {
|
||||||
val isCollapsed = collapsedSections[section.spaceId] ?: false
|
val isCollapsed = collapsedSections[section.spaceId] ?: false
|
||||||
if (sectionIdx > 0) {
|
val csIndex = childSpaceSectionIndexMap[sectionIdx] ?: -1
|
||||||
|
val isSectionDragging = draggingSectionIndex == csIndex
|
||||||
|
val sectionElevation by animateDpAsState(if (isSectionDragging) 8.dp else 0.dp, label = "sectionElevation")
|
||||||
|
|
||||||
|
if (sectionIdx > 0 && !isSectionDragging) {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f),
|
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f),
|
||||||
@@ -171,11 +184,58 @@ fun ChannelList(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.then(
|
||||||
|
if (isSectionDragging) Modifier
|
||||||
|
.zIndex(1f)
|
||||||
|
.offset { IntOffset(0, sectionDragOffsetY.roundToInt()) }
|
||||||
|
.shadow(sectionElevation, RoundedCornerShape(8.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.clickable { collapsedSections[section.spaceId] = !isCollapsed }
|
.clickable { collapsedSections[section.spaceId] = !isCollapsed }
|
||||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
if (isReorderMode) {
|
||||||
|
val currentCsIndex by rememberUpdatedState(csIndex)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.DragHandle,
|
||||||
|
contentDescription = "Drag to reorder section",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectDragGesturesAfterLongPress(
|
||||||
|
onDragStart = {
|
||||||
|
draggingSectionIndex = currentCsIndex
|
||||||
|
sectionDragOffsetY = 0f
|
||||||
|
},
|
||||||
|
onDrag = { change, dragAmount ->
|
||||||
|
change.consume()
|
||||||
|
sectionDragOffsetY += dragAmount.y
|
||||||
|
val itemHeight = 40.dp.toPx()
|
||||||
|
val draggedPositions = (sectionDragOffsetY / itemHeight).roundToInt()
|
||||||
|
val targetIndex = (draggingSectionIndex + draggedPositions).coerceIn(0, childSpaceSections.lastIndex)
|
||||||
|
if (targetIndex != draggingSectionIndex) {
|
||||||
|
onMoveChildSpace(draggingSectionIndex, targetIndex)
|
||||||
|
sectionDragOffsetY -= (targetIndex - draggingSectionIndex) * itemHeight
|
||||||
|
draggingSectionIndex = targetIndex
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
draggingSectionIndex = -1
|
||||||
|
sectionDragOffsetY = 0f
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
draggingSectionIndex = -1
|
||||||
|
sectionDragOffsetY = 0f
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(6.dp))
|
||||||
|
}
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isCollapsed) Icons.Default.ExpandMore else Icons.Default.ExpandLess,
|
imageVector = if (isCollapsed) Icons.Default.ExpandMore else Icons.Default.ExpandLess,
|
||||||
contentDescription = if (isCollapsed) "Expand" else "Collapse",
|
contentDescription = if (isCollapsed) "Expand" else "Collapse",
|
||||||
|
|||||||
@@ -219,12 +219,6 @@ fun MessageTimeline(
|
|||||||
// The divider goes after index unreadMarkerIndex (visually above unread block)
|
// The divider goes after index unreadMarkerIndex (visually above unread block)
|
||||||
if (index == unreadMarkerIndex) {
|
if (index == unreadMarkerIndex) {
|
||||||
Column {
|
Column {
|
||||||
if (isFirstInGroup) {
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
FullMessage(message)
|
|
||||||
} else {
|
|
||||||
CompactMessage(message.content)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -247,6 +241,12 @@ fun MessageTimeline(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
if (isFirstInGroup) {
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
FullMessage(message)
|
||||||
|
} else {
|
||||||
|
CompactMessage(message.content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isFirstInGroup) {
|
if (isFirstInGroup) {
|
||||||
|
|||||||
Reference in New Issue
Block a user