DMs baseline
This commit is contained in:
13
.idea/deviceManager.xml
generated
Normal file
13
.idea/deviceManager.xml
generated
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -15,7 +15,9 @@ import androidx.compose.foundation.layout.ime
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
@@ -28,6 +30,7 @@ import com.example.fluffytrix.ui.screens.main.components.ChannelList
|
|||||||
import com.example.fluffytrix.ui.screens.main.components.MemberList
|
import com.example.fluffytrix.ui.screens.main.components.MemberList
|
||||||
import com.example.fluffytrix.ui.screens.main.components.MessageTimeline
|
import com.example.fluffytrix.ui.screens.main.components.MessageTimeline
|
||||||
import com.example.fluffytrix.ui.screens.main.components.SpaceList
|
import com.example.fluffytrix.ui.screens.main.components.SpaceList
|
||||||
|
import com.example.fluffytrix.ui.screens.main.components.UserProfileSheet
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@@ -62,6 +65,23 @@ fun MainScreen(
|
|||||||
val preferencesManager: PreferencesManager = koinInject()
|
val preferencesManager: PreferencesManager = koinInject()
|
||||||
val hideSpacesWhenClosed by preferencesManager.hideSpacesWhenClosed.collectAsStateWithLifecycle(initialValue = false)
|
val hideSpacesWhenClosed by preferencesManager.hideSpacesWhenClosed.collectAsStateWithLifecycle(initialValue = false)
|
||||||
|
|
||||||
|
data class ProfileSheetState(val userId: String, val displayName: String, val avatarUrl: String?)
|
||||||
|
var profileSheet by remember { mutableStateOf<ProfileSheetState?>(null) }
|
||||||
|
|
||||||
|
profileSheet?.let { sheet ->
|
||||||
|
UserProfileSheet(
|
||||||
|
userId = sheet.userId,
|
||||||
|
displayName = sheet.displayName,
|
||||||
|
avatarUrl = sheet.avatarUrl,
|
||||||
|
currentUserId = currentUserId,
|
||||||
|
onDismiss = { profileSheet = null },
|
||||||
|
onStartDm = {
|
||||||
|
profileSheet = null
|
||||||
|
viewModel.startDm(sheet.userId)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val imeInsets = WindowInsets.ime
|
val imeInsets = WindowInsets.ime
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -156,12 +176,17 @@ fun MainScreen(
|
|||||||
onSendReaction = { eventId, emoji -> viewModel.sendReaction(eventId, emoji) },
|
onSendReaction = { eventId, emoji -> viewModel.sendReaction(eventId, emoji) },
|
||||||
onSendThreadReaction = { eventId, emoji -> viewModel.sendThreadReaction(eventId, emoji) },
|
onSendThreadReaction = { eventId, emoji -> viewModel.sendThreadReaction(eventId, emoji) },
|
||||||
emojiPacks = emojiPacks,
|
emojiPacks = emojiPacks,
|
||||||
|
onStartDm = { userId -> viewModel.startDm(userId) },
|
||||||
|
memberNames = remember(members) { members.associate { it.userId to it.displayName } },
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimatedVisibility(visible = showMemberList) {
|
AnimatedVisibility(visible = showMemberList) {
|
||||||
MemberList(
|
MemberList(
|
||||||
members = members,
|
members = members,
|
||||||
contentPadding = padding,
|
contentPadding = padding,
|
||||||
|
onMemberClick = { member ->
|
||||||
|
profileSheet = ProfileSheetState(member.userId, member.displayName, member.avatarUrl)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.matrix.rustcomponents.sdk.CreateRoomParameters
|
||||||
import org.matrix.rustcomponents.sdk.EditedContent
|
import org.matrix.rustcomponents.sdk.EditedContent
|
||||||
import org.matrix.rustcomponents.sdk.EventOrTransactionId
|
import org.matrix.rustcomponents.sdk.EventOrTransactionId
|
||||||
import org.matrix.rustcomponents.sdk.Membership
|
import org.matrix.rustcomponents.sdk.Membership
|
||||||
@@ -32,6 +33,8 @@ import org.matrix.rustcomponents.sdk.MembershipState
|
|||||||
import org.matrix.rustcomponents.sdk.MessageType
|
import org.matrix.rustcomponents.sdk.MessageType
|
||||||
import org.matrix.rustcomponents.sdk.MsgLikeKind
|
import org.matrix.rustcomponents.sdk.MsgLikeKind
|
||||||
import org.matrix.rustcomponents.sdk.ProfileDetails
|
import org.matrix.rustcomponents.sdk.ProfileDetails
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomPreset
|
||||||
|
import org.matrix.rustcomponents.sdk.RoomVisibility
|
||||||
import org.matrix.rustcomponents.sdk.SyncService
|
import org.matrix.rustcomponents.sdk.SyncService
|
||||||
import org.matrix.rustcomponents.sdk.DateDividerMode
|
import org.matrix.rustcomponents.sdk.DateDividerMode
|
||||||
import org.matrix.rustcomponents.sdk.TimelineConfiguration
|
import org.matrix.rustcomponents.sdk.TimelineConfiguration
|
||||||
@@ -522,9 +525,12 @@ class MainViewModel(
|
|||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val room = client.getRoom(roomId)
|
val room = client.getRoom(roomId)
|
||||||
val name = room?.displayName() ?: roomId
|
val sdkName = room?.displayName()
|
||||||
|
// Don't overwrite a real seeded name with a generic SDK placeholder
|
||||||
|
val isGeneric = sdkName == null || sdkName == "Empty Room" || sdkName.startsWith("!")
|
||||||
|
val name = if (isGeneric) channelNameCache[roomId] ?: sdkName ?: roomId else sdkName
|
||||||
channelNameCache[roomId] = name
|
channelNameCache[roomId] = name
|
||||||
_channelName.value = name
|
if (_selectedChannel.value == roomId) _channelName.value = name
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -756,12 +762,12 @@ class MainViewModel(
|
|||||||
senderAvatar = avatarUrl(profile.avatarUrl)
|
senderAvatar = avatarUrl(profile.avatarUrl)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
senderName = senderNameCache[localpart] ?: localpart
|
senderName = senderNameCache[sender] ?: localpart
|
||||||
senderAvatar = senderAvatarCache[localpart]
|
senderAvatar = senderAvatarCache[sender]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
senderNameCache[localpart] = senderName
|
senderNameCache[sender] = senderName
|
||||||
if (senderAvatar != null) senderAvatarCache[localpart] = senderAvatar
|
if (senderAvatar != null) senderAvatarCache[sender] = senderAvatar
|
||||||
|
|
||||||
val reactions = if (content is TimelineItemContent.MsgLike) {
|
val reactions = if (content is TimelineItemContent.MsgLike) {
|
||||||
content.content.reactions
|
content.content.reactions
|
||||||
@@ -771,7 +777,7 @@ class MainViewModel(
|
|||||||
|
|
||||||
val msg = MessageItem(
|
val msg = MessageItem(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
senderId = localpart,
|
senderId = sender,
|
||||||
senderName = senderName,
|
senderName = senderName,
|
||||||
senderAvatarUrl = senderAvatar,
|
senderAvatarUrl = senderAvatar,
|
||||||
content = msgContent,
|
content = msgContent,
|
||||||
@@ -934,9 +940,8 @@ class MainViewModel(
|
|||||||
}
|
}
|
||||||
memberCache[roomId] = memberList
|
memberCache[roomId] = memberList
|
||||||
memberList.forEach { m ->
|
memberList.forEach { m ->
|
||||||
val localpart = m.userId.removePrefix("@").substringBefore(":")
|
senderAvatarCache[m.userId] = avatarUrl(m.avatarUrl)
|
||||||
senderAvatarCache[localpart] = avatarUrl(m.avatarUrl)
|
senderNameCache[m.userId] = m.displayName
|
||||||
senderNameCache[localpart] = m.displayName
|
|
||||||
}
|
}
|
||||||
// Backfill avatars into cached messages
|
// Backfill avatars into cached messages
|
||||||
messageCache[roomId]?.let { cached ->
|
messageCache[roomId]?.let { cached ->
|
||||||
@@ -1672,6 +1677,76 @@ class MainViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDm(userId: String) {
|
||||||
|
val client = authRepository.getClient() ?: return
|
||||||
|
val normalizedUserId = if (userId.startsWith("@")) userId else "@$userId"
|
||||||
|
android.util.Log.d("MainVM", "startDm called with userId='$userId' normalized='$normalizedUserId'")
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
// Reuse existing DM if one already exists
|
||||||
|
val existingRoom = try { client.getDmRoom(normalizedUserId) } catch (_: Exception) { null }
|
||||||
|
val roomId: String
|
||||||
|
if (existingRoom != null) {
|
||||||
|
roomId = existingRoom.id()
|
||||||
|
} else {
|
||||||
|
val newRoomId = client.createRoom(
|
||||||
|
CreateRoomParameters(
|
||||||
|
name = null,
|
||||||
|
topic = null,
|
||||||
|
isEncrypted = true,
|
||||||
|
isDirect = true,
|
||||||
|
visibility = RoomVisibility.Private,
|
||||||
|
preset = RoomPreset.TRUSTED_PRIVATE_CHAT,
|
||||||
|
invite = emptyList(),
|
||||||
|
avatar = null,
|
||||||
|
powerLevelContentOverride = null,
|
||||||
|
joinRuleOverride = null,
|
||||||
|
historyVisibilityOverride = null,
|
||||||
|
canonicalAlias = null,
|
||||||
|
isSpace = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
// Poll until the room appears in the local store, then invite
|
||||||
|
var room = client.getRoom(newRoomId)
|
||||||
|
if (room == null) {
|
||||||
|
repeat(10) {
|
||||||
|
kotlinx.coroutines.delay(500)
|
||||||
|
room = client.getRoom(newRoomId)
|
||||||
|
if (room != null) return@repeat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (room != null) {
|
||||||
|
try {
|
||||||
|
room!!.inviteUserById(normalizedUserId)
|
||||||
|
android.util.Log.d("MainVM", "Invited $normalizedUserId to $newRoomId")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("MainVM", "inviteUserById failed: ${e.message}", e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
android.util.Log.e("MainVM", "Room $newRoomId not found after waiting, skipping invite")
|
||||||
|
}
|
||||||
|
roomId = newRoomId
|
||||||
|
|
||||||
|
// Seed the channel name from the invited user's profile so it
|
||||||
|
// shows their name immediately rather than "Empty Room"
|
||||||
|
val displayName = try {
|
||||||
|
client.getProfile(normalizedUserId).displayName?.takeIf { it.isNotBlank() }
|
||||||
|
?: normalizedUserId.removePrefix("@").substringBefore(":")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
normalizedUserId.removePrefix("@").substringBefore(":")
|
||||||
|
}
|
||||||
|
channelNameCache[roomId] = displayName
|
||||||
|
}
|
||||||
|
withContext(kotlinx.coroutines.Dispatchers.Main) {
|
||||||
|
selectChannel(roomId)
|
||||||
|
_showChannelList.value = false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("MainVM", "startDm failed", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun logout() {
|
fun logout() {
|
||||||
ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
|
ProcessLifecycleOwner.get().lifecycle.removeObserver(lifecycleObserver)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.fluffytrix.ui.screens.main.components
|
package com.example.fluffytrix.ui.screens.main.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -31,6 +32,7 @@ import com.example.fluffytrix.ui.screens.main.MemberItem
|
|||||||
fun MemberList(
|
fun MemberList(
|
||||||
members: List<MemberItem>,
|
members: List<MemberItem>,
|
||||||
contentPadding: PaddingValues = PaddingValues(),
|
contentPadding: PaddingValues = PaddingValues(),
|
||||||
|
onMemberClick: (MemberItem) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -56,6 +58,7 @@ fun MemberList(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.clickable { onMemberClick(member) }
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
|||||||
@@ -90,8 +90,11 @@ import androidx.compose.runtime.mutableStateMapOf
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import androidx.compose.material.icons.automirrored.filled.Reply
|
import androidx.compose.material.icons.automirrored.filled.Reply
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.ScrollableTabRow
|
import androidx.compose.material3.ScrollableTabRow
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material.icons.filled.PlayCircleFilled
|
import androidx.compose.material.icons.filled.PlayCircleFilled
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
@@ -138,6 +141,9 @@ private val LocalVideoPlayer = compositionLocalOf<(String) -> Unit> { {} }
|
|||||||
private val LocalScrollToEvent = compositionLocalOf<(String) -> Unit> { {} }
|
private val LocalScrollToEvent = compositionLocalOf<(String) -> Unit> { {} }
|
||||||
private val LocalReactionHandler = compositionLocalOf<(eventId: String, emoji: String) -> Unit> { { _, _ -> } }
|
private val LocalReactionHandler = compositionLocalOf<(eventId: String, emoji: String) -> Unit> { { _, _ -> } }
|
||||||
private val LocalCurrentUserId = compositionLocalOf<String?> { null }
|
private val LocalCurrentUserId = compositionLocalOf<String?> { null }
|
||||||
|
private val LocalUserProfileHandler = compositionLocalOf<(userId: String, displayName: String, avatarUrl: String?) -> Unit> { { _, _, _ -> } }
|
||||||
|
// Map of userId -> displayName for resolving reaction sender names
|
||||||
|
private val LocalMemberNames = compositionLocalOf<Map<String, String>> { emptyMap() }
|
||||||
|
|
||||||
private val senderColors = arrayOf(
|
private val senderColors = arrayOf(
|
||||||
Color(0xFF5865F2),
|
Color(0xFF5865F2),
|
||||||
@@ -196,10 +202,15 @@ fun MessageTimeline(
|
|||||||
onSendReaction: (String, String) -> Unit = { _, _ -> },
|
onSendReaction: (String, String) -> Unit = { _, _ -> },
|
||||||
onSendThreadReaction: (String, String) -> Unit = { _, _ -> },
|
onSendThreadReaction: (String, String) -> Unit = { _, _ -> },
|
||||||
emojiPacks: List<EmojiPack> = emptyList(),
|
emojiPacks: List<EmojiPack> = emptyList(),
|
||||||
|
onViewProfile: (userId: String, displayName: String, avatarUrl: String?) -> Unit = { _, _, _ -> },
|
||||||
|
onStartDm: (String) -> Unit = {},
|
||||||
|
memberNames: Map<String, String> = emptyMap(),
|
||||||
) {
|
) {
|
||||||
var fullscreenImageUrl by remember { mutableStateOf<String?>(null) }
|
var fullscreenImageUrl by remember { mutableStateOf<String?>(null) }
|
||||||
var fullscreenVideoUrl by remember { mutableStateOf<String?>(null) }
|
var fullscreenVideoUrl by remember { mutableStateOf<String?>(null) }
|
||||||
var contextMenuMessage by remember { mutableStateOf<MessageItem?>(null) }
|
var contextMenuMessage by remember { mutableStateOf<MessageItem?>(null) }
|
||||||
|
data class ProfileSheetState(val userId: String, val displayName: String, val avatarUrl: String?)
|
||||||
|
var profileSheet by remember { mutableStateOf<ProfileSheetState?>(null) }
|
||||||
|
|
||||||
if (fullscreenImageUrl != null) {
|
if (fullscreenImageUrl != null) {
|
||||||
FullscreenImageViewer(
|
FullscreenImageViewer(
|
||||||
@@ -242,6 +253,20 @@ fun MessageTimeline(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profileSheet?.let { sheet ->
|
||||||
|
UserProfileSheet(
|
||||||
|
userId = sheet.userId,
|
||||||
|
displayName = sheet.displayName,
|
||||||
|
avatarUrl = sheet.avatarUrl,
|
||||||
|
currentUserId = currentUserId,
|
||||||
|
onDismiss = { profileSheet = null },
|
||||||
|
onStartDm = {
|
||||||
|
profileSheet = null
|
||||||
|
onStartDm(sheet.userId)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val reactionHandler: (String, String) -> Unit = remember(selectedThread, onSendReaction, onSendThreadReaction) {
|
val reactionHandler: (String, String) -> Unit = remember(selectedThread, onSendReaction, onSendThreadReaction) {
|
||||||
{ eventId, emoji ->
|
{ eventId, emoji ->
|
||||||
if (selectedThread != null) onSendThreadReaction(eventId, emoji)
|
if (selectedThread != null) onSendThreadReaction(eventId, emoji)
|
||||||
@@ -254,6 +279,8 @@ fun MessageTimeline(
|
|||||||
LocalVideoPlayer provides { url -> fullscreenVideoUrl = url },
|
LocalVideoPlayer provides { url -> fullscreenVideoUrl = url },
|
||||||
LocalReactionHandler provides reactionHandler,
|
LocalReactionHandler provides reactionHandler,
|
||||||
LocalCurrentUserId provides currentUserId,
|
LocalCurrentUserId provides currentUserId,
|
||||||
|
LocalUserProfileHandler provides { userId, displayName, avatarUrl -> profileSheet = ProfileSheetState(userId, displayName, avatarUrl) },
|
||||||
|
LocalMemberNames provides memberNames,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -501,6 +528,7 @@ private fun ReactionRow(eventId: String, reactions: Map<String, List<String>>) {
|
|||||||
if (reactions.isEmpty()) return
|
if (reactions.isEmpty()) return
|
||||||
val onReact = LocalReactionHandler.current
|
val onReact = LocalReactionHandler.current
|
||||||
val currentUserId = LocalCurrentUserId.current
|
val currentUserId = LocalCurrentUserId.current
|
||||||
|
val memberNames = LocalMemberNames.current
|
||||||
val authRepository: AuthRepository = koinInject()
|
val authRepository: AuthRepository = koinInject()
|
||||||
val baseUrl = remember { try { authRepository.getClient()?.session()?.homeserverUrl?.trimEnd('/') ?: "" } catch (_: Exception) { "" } }
|
val baseUrl = remember { try { authRepository.getClient()?.session()?.homeserverUrl?.trimEnd('/') ?: "" } catch (_: Exception) { "" } }
|
||||||
var reactionDetailEmoji by remember { mutableStateOf<String?>(null) }
|
var reactionDetailEmoji by remember { mutableStateOf<String?>(null) }
|
||||||
@@ -523,7 +551,10 @@ private fun ReactionRow(eventId: String, reactions: Map<String, List<String>>) {
|
|||||||
text = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
reactionDetailSenders.forEach { sender ->
|
reactionDetailSenders.forEach { sender ->
|
||||||
Text(sender, style = MaterialTheme.typography.bodyMedium)
|
Text(
|
||||||
|
memberNames[sender] ?: sender.removePrefix("@").substringBefore(":"),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -584,6 +615,7 @@ private fun FullMessage(message: MessageItem, onOpenThread: (String) -> Unit = {
|
|||||||
val senderColor = remember(message.senderName) { colorForSender(message.senderName) }
|
val senderColor = remember(message.senderName) { colorForSender(message.senderName) }
|
||||||
val time = remember(message.timestamp) { formatTimestamp(message.timestamp) }
|
val time = remember(message.timestamp) { formatTimestamp(message.timestamp) }
|
||||||
val reply = message.replyTo
|
val reply = message.replyTo
|
||||||
|
val onViewProfile = LocalUserProfileHandler.current
|
||||||
|
|
||||||
Column(modifier = Modifier.combinedClickable(onClick = {}, onLongClick = { onLongPress(message) })) {
|
Column(modifier = Modifier.combinedClickable(onClick = {}, onLongClick = { onLongPress(message) })) {
|
||||||
if (reply != null) {
|
if (reply != null) {
|
||||||
@@ -594,12 +626,15 @@ private fun FullMessage(message: MessageItem, onOpenThread: (String) -> Unit = {
|
|||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = message.senderAvatarUrl,
|
model = message.senderAvatarUrl,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(40.dp).clip(CircleShape),
|
modifier = Modifier.size(40.dp).clip(CircleShape).clickable {
|
||||||
|
onViewProfile(message.senderId, message.senderName, message.senderAvatarUrl)
|
||||||
|
},
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.size(40.dp).clip(CircleShape).background(senderColor.copy(alpha = 0.3f)),
|
modifier = Modifier.size(40.dp).clip(CircleShape).background(senderColor.copy(alpha = 0.3f))
|
||||||
|
.clickable { onViewProfile(message.senderId, message.senderName, null) },
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@@ -1820,3 +1855,50 @@ private fun CustomEmojiGrid(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun UserProfileSheet(
|
||||||
|
userId: String,
|
||||||
|
displayName: String,
|
||||||
|
avatarUrl: String?,
|
||||||
|
currentUserId: String?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onStartDm: () -> Unit,
|
||||||
|
) {
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp)
|
||||||
|
.padding(bottom = 32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
if (avatarUrl != null) {
|
||||||
|
AsyncImage(
|
||||||
|
model = avatarUrl,
|
||||||
|
contentDescription = displayName,
|
||||||
|
modifier = Modifier.size(80.dp).clip(CircleShape),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val color = remember(displayName) { colorForSender(displayName) }
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(80.dp).clip(CircleShape).background(color.copy(alpha = 0.3f)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(displayName.take(1).uppercase(), fontSize = 32.sp, fontWeight = FontWeight.Bold, color = color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(displayName, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
||||||
|
Text(userId, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
if (userId != currentUserId) {
|
||||||
|
Button(onClick = onStartDm, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text("Message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user