diff --git a/README.md b/README.md index e8ab712..eca059c 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,12 @@ Discord Compose follows the principles of Clean Architecture with Android Archit + + Chat + + + + ## 📷 Screenshots (Light theme) @@ -113,7 +119,7 @@ Discord Compose follows the principles of Clean Architecture with Android Archit Invite Password Manager Dialog Create Server -Friends + Friends @@ -121,6 +127,12 @@ Discord Compose follows the principles of Clean Architecture with Android Archit + + Chat + + + +

[Back to top]

diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7378134..e7fd11b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -117,6 +117,7 @@ dependencies { /* Image Loading */ implementation(Lib.Android.COIL_COMPOSE) + implementation(Lib.Android.ACCOMPANIST_COIL) /*DI*/ implementation(Lib.Di.hiltAndroid) diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/MainActivity.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/MainActivity.kt index 3dc7f70..e98bc38 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/MainActivity.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/MainActivity.kt @@ -1,10 +1,6 @@ package dev.baseio.discordjetpackcompose -import android.os.Build import android.os.Bundle -import android.view.WindowInsets -import android.view.WindowInsetsController -import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.LaunchedEffect diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/DashboardRoute.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/DashboardRoute.kt index 07efc14..7b145a6 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/DashboardRoute.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/DashboardRoute.kt @@ -14,6 +14,7 @@ import dev.baseio.discordjetpackcompose.ui.routes.dashboard.friends.FriendsScree import dev.baseio.discordjetpackcompose.ui.routes.dashboard.invite.InviteScreen import dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.HomeScreen import dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.dasboard.DashboardScreen +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.UserSettings fun NavGraphBuilder.dashboardRoute( composeNavigator: ComposeNavigator, @@ -34,6 +35,9 @@ fun NavGraphBuilder.dashboardRoute( composable(DiscordScreen.CreateServer.name) { CreateServer(composeNavigator) } + composable(DiscordScreen.UserSettings.name) { + UserSettings(composeNavigator = composeNavigator) + } } } @@ -62,5 +66,8 @@ fun NavGraphBuilder.setupDashboardBottomNavScreens( composable(DiscordScreen.Friends.route) { FriendsScreen(composeNavigator = composeNavigator) } + composable(DiscordScreen.UserSettings.name) { + UserSettings(composeNavigator = composeNavigator) + } } } \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/friends/FriendsScreen.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/friends/FriendsScreen.kt index f1ce63c..63cccd3 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/friends/FriendsScreen.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/friends/FriendsScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -139,10 +140,10 @@ fun FriendsScreen( } @Composable -fun Header(title: Int, size: Int) { +fun Header(title: Int, size: Int, style: TextStyle = DirectMessageListTypography.h5) { Text( text = stringResource(title, size), - style = DirectMessageListTypography.h5, + style = style, color = DiscordColorProvider.colors.onSurface.copy(alpha = ContentAlpha.medium) ) } \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/HomeScreen.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/HomeScreen.kt index a7e539c..22871f2 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/HomeScreen.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/HomeScreen.kt @@ -13,13 +13,7 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset -import androidx.compose.material.ContentAlpha -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.FractionalThreshold -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.SwipeableState -import androidx.compose.material.rememberSwipeableState -import androidx.compose.material.swipeable +import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect @@ -45,10 +39,12 @@ import dev.baseio.discordjetpackcompose.entities.ChatUserEntity import dev.baseio.discordjetpackcompose.entities.server.ServerEntity import dev.baseio.discordjetpackcompose.navigator.ComposeNavigator import dev.baseio.discordjetpackcompose.navigator.DiscordScreen +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.chatscreen.ChannelMemberScreen import dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.chatscreen.ChatScreen import dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.dasboard.getFakeChatUserList import dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.dasboard.getFakeServerList import dev.baseio.discordjetpackcompose.ui.theme.DiscordColorProvider +import dev.baseio.discordjetpackcompose.ui.theme.channel_member_bg import dev.baseio.discordjetpackcompose.viewmodels.DashboardScreenViewModel import kotlinx.coroutines.CoroutineScope import kotlin.math.roundToInt @@ -131,9 +127,9 @@ fun HomeScreen( val leftDrawerModifier by remember(drawerOnTop, isAnyItemSelectedInServers) { mutableStateOf( - swipeableModifier - .zIndex(if (drawerOnTop == DrawerTypes.LEFT) 1f else 0f) - .alpha(if (drawerOnTop == DrawerTypes.LEFT) 1f else 0f) + swipeableModifier + .zIndex(if (drawerOnTop == DrawerTypes.LEFT) 1f else 0f) + .alpha(if (drawerOnTop == DrawerTypes.LEFT) 1f else 0f) ) } @@ -190,13 +186,16 @@ fun HomeScreen( openServerInfoBottomSheet = { coroutineScope.launch { sheetState.show() } }, viewModel = viewModel ) - Box( + ChannelMemberScreen( modifier = rightDrawerModifier .fillMaxHeight() .fillMaxWidth(0.85f) - .background(Color.Cyan) - .align(Alignment.CenterEnd) - ) {} + .background(channel_member_bg) + .align(Alignment.CenterEnd), + onInviteButtonClicked = { + composeNavigator.navigate(DiscordScreen.Invite.route) + } + ) val centerScreenZIndex by remember { derivedStateOf { diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChannelMemberScreen.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChannelMemberScreen.kt new file mode 100644 index 0000000..63c592c --- /dev/null +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChannelMemberScreen.kt @@ -0,0 +1,271 @@ +package dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.chatscreen + + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.PushPin +import androidx.compose.material.icons.filled.Settings +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import coil.compose.AsyncImage +import coil.request.ImageRequest +import dev.baseio.discordjetpackcompose.R +import dev.baseio.discordjetpackcompose.entities.ChatUserEntity +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.components.OnlineIndicator +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.friends.Header +import dev.baseio.discordjetpackcompose.ui.theme.* +import dev.baseio.discordjetpackcompose.viewmodels.FriendsViewModel + +@Composable +fun ChannelMemberScreen( + modifier: Modifier, + onInviteButtonClicked: () -> Unit, + viewModel: FriendsViewModel = hiltViewModel() +) { + + val friendsList by viewModel.friendsList.collectAsState() + val friends by friendsList.collectAsState(initial = emptyList()) + + val onlineList = friends.filter { + it.isOnline + } + + val offlineList = friends.filter { + !it.isOnline + } + + Column( + modifier = modifier, + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top + ) { + ChannelHeaderText() + Divider( + color = channel_member_secondary_bg, + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(create_server_screen) + ) + ChannelMemberActions() + InviteMembers(onInviteButtonClicked) + ChannelMembersList(offlineList, onlineList) + } +} + +@Composable +fun ChannelHeaderText() { + Text( + text = stringResource(id = R.string.channel_header), modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + style = Typography.h5.copy( + fontWeight = FontWeight.SemiBold + ), + color = DiscordColorProvider.colors.onSurface + ) +} + +@Composable +fun ChannelMemberActions() { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) + .background(channel_member_bg), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + ActionIcons( + label = stringResource(id = R.string.threads), + painterResource = R.drawable.ic_hashtag_solid, + icon = null + ) + ActionIcons(Icons.Filled.PushPin, stringResource(id = R.string.pins), null) + ActionIcons(Icons.Filled.Notifications, stringResource(id = R.string.notifications), null) + ActionIcons(Icons.Filled.Settings, stringResource(id = R.string.settings), null) + } +} + +@Composable +fun ActionIcons( + icon: ImageVector?, + label: String, + painterResource: Int? +) { + if (icon != null) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + icon, + contentDescription = label, + modifier = Modifier.size(18.dp), + tint = channel_member_action_icon + ) + Spacer(modifier = Modifier.height(4.dp)) + ActionIconLabel(label = label) + } + } else if (painterResource != null) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + painterResource(id = painterResource), + contentDescription = label, + modifier = Modifier.size(18.dp), + tint = channel_member_action_icon + ) + Spacer(modifier = Modifier.height(4.dp)) + ActionIconLabel(label = label) + } + } +} + +@Composable +fun InviteMembers(onInviteButtonClicked: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(channel_member_secondary_bg) + .clickable { + onInviteButtonClicked() + } + .padding(16.dp), + + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(id = R.drawable.ic_baseline_person_add_alt_1_24), + contentDescription = stringResource(id = R.string.invite_members), + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(channel_member_bg) + .padding(6.dp), + tint = channel_member_action_icon + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(id = R.string.invite_members), + color = DiscordColorProvider.colors.onSurface, + style = Typography.subtitle2.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp + ) + ) + + } +} + + +@Composable +fun ActionIconLabel(label: String) { + Text( + text = label, + color = channel_member_action_label, + style = Typography.subtitle2.copy( + fontWeight = FontWeight.Medium, + fontSize = 12.sp + ) + ) +} + +@Composable +fun ChannelMembersList( + offlineMembersList: List, + onlineMembersList: List +) { + LazyColumn(modifier = Modifier + .background(channel_member_secondary_bg) + .padding(8.dp)) { + item { + Header(title = R.string.online, onlineMembersList.size) + } + items(onlineMembersList) { friend -> + FriendComponent(chatUserEntity = friend) + } + item { + Spacer(modifier = Modifier.height(10.dp)) + } + item { + Header( + title = R.string.offline, + offlineMembersList.size, + style = DirectMessageListTypography.h5.copy( + fontSize = 14.sp + ) + ) + } + items(offlineMembersList) { friend -> + FriendComponent(chatUserEntity = friend) + } + item { + Spacer(modifier = Modifier.height(40.dp)) + } + } +} + +@Composable +fun FriendComponent(chatUserEntity: ChatUserEntity) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Box( + contentAlignment = Alignment.BottomEnd, + modifier = Modifier.padding(8.dp), + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(chatUserEntity.profileImage) + .placeholder(R.drawable.light_app_logo) + .crossfade(true) + .build(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .width(32.dp) + .aspectRatio(1f) + .clip(CircleShape), + ) + OnlineIndicator(isOnline = chatUserEntity.isOnline) + } + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = chatUserEntity.username, + style = DirectMessageListTypography.h6.copy( + fontWeight = + FontWeight.Normal, + fontSize = 14.sp + + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = DiscordColorProvider.colors.onSurface + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMesageActions.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMesageActions.kt new file mode 100644 index 0000000..00077bb --- /dev/null +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMesageActions.kt @@ -0,0 +1,171 @@ +package dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.chatscreen + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetState +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.FileCopy +import androidx.compose.material.icons.filled.InsertEmoticon +import androidx.compose.material.icons.filled.MarkAsUnread +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Pin +import androidx.compose.material.icons.filled.Reply +import androidx.compose.material.icons.filled.Share +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import dev.baseio.discordjetpackcompose.ui.theme.DiscordColorProvider +import dev.baseio.discordjetpackcompose.ui.theme.MessageTypography +import dev.baseio.discordjetpackcompose.ui.utils.Drawables + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun MessageActionsBottomSheet( + sheetState: ModalBottomSheetState, + replyAction: () -> Unit = {} +) { + ModalBottomSheetLayout( + modifier = Modifier, + sheetState = sheetState, + sheetContent = { + MessageActionBottomSheetContent( + replyAction = replyAction + ) + }, + sheetShape = RoundedCornerShape(0), + sheetBackgroundColor = DiscordColorProvider.colors.surface, + sheetContentColor = DiscordColorProvider.colors.onPrimary, + scrimColor = Color.Black.copy(alpha = 0.32f), + content = {} + ) +} + +@Composable +fun MessageActionBottomSheetContent( + replyAction: () -> Unit = {} +) { + Column( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + ) { + MessageActionsEmojisList() + MessageActionsList( + replyAction = replyAction + ) + } +} + +@Composable +fun MessageActionsEmojisList() { + Row( + modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp) + ) { + EmojiActionButton(emojiId = Drawables.ic_emoji_1) + EmojiActionButton(emojiId = Drawables.ic_emoji_2) + EmojiActionButton(emojiId = Drawables.ic_emoji_3) + EmojiActionButton(emojiId = Drawables.ic_emoji_4) + EmojiActionButton(emojiId = Drawables.ic_emoji_5) + IconButton( + modifier = Modifier + .background( + color = DiscordColorProvider.colors.chatEditor, + shape = CircleShape + ), + onClick = {} + ) { + Icon( + Icons.Default.InsertEmoticon, + contentDescription = null, + tint = DiscordColorProvider.colors.brand + ) + } + } +} + +@Composable +fun MessageActionsList( + replyAction: () -> Unit = {} +) { + Column( + modifier = Modifier.padding(top = 16.dp, bottom = 48.dp) + ) { + MessageActionItem(Icons.Default.Edit, "Edit") + MessageActionItem(Icons.Default.Reply, "Reply", action = replyAction) + MessageActionItem(Icons.Default.FileCopy, "Copy Text") + MessageActionItem(Icons.Default.Delete, "Delete") + MessageActionItem(Icons.Default.Person, "Profile") + MessageActionItem(Icons.Default.Pin, "Pin") + MessageActionItem(Icons.Default.Share, "Share") + MessageActionItem(Icons.Default.MarkAsUnread, "Mark Unread") + MessageActionItem(Icons.Default.FileCopy, "Copy ID") + } +} + +@Composable +fun MessageActionItem( + imageVector: ImageVector, + text: String, + action: () -> Unit = {} +) { + Row( + modifier = Modifier + .clickable { action() }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier.padding(start = 16.dp, top = 12.dp, bottom = 16.dp), + imageVector = imageVector, + contentDescription = null, + tint = DiscordColorProvider.colors.brand + ) + Text( + modifier = Modifier + .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + style = MessageTypography.caption, + text = text, + color = DiscordColorProvider.colors.onSurface + ) + } +} + +@Composable +private fun EmojiActionButton( + modifier: Modifier = Modifier, + emojiId: Int +) { + IconButton( + modifier = modifier + .padding(end = 8.dp) + .background( + color = DiscordColorProvider.colors.chatEditor, + shape = CircleShape + ), + onClick = {} + ) { + Icon( + painterResource(id = emojiId), + contentDescription = null, + tint = Color.Unspecified + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessageEditor.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessageEditor.kt index fb1a6f5..319e255 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessageEditor.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessageEditor.kt @@ -3,8 +3,10 @@ package dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.chatscreen import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape @@ -16,8 +18,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowForwardIos import androidx.compose.material.icons.filled.CardGiftcard +import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.EmojiEmotions import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -32,7 +36,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import dev.baseio.discordjetpackcompose.custom.MentionsPatterns import dev.baseio.discordjetpackcompose.custom.MentionsTextField +import dev.baseio.discordjetpackcompose.custom.extractSpans import dev.baseio.discordjetpackcompose.ui.theme.DiscordColorProvider import dev.baseio.discordjetpackcompose.ui.theme.MessageTypography import dev.baseio.discordjetpackcompose.ui.utils.Drawables @@ -42,18 +48,22 @@ import dev.baseio.discordjetpackcompose.viewmodels.ChatScreenViewModel fun ChatMessageEditor( modifier: Modifier = Modifier, userName: State, + isReplyMode: MutableState, viewModel: ChatScreenViewModel ) { + // UiState of the ChatScreen + val uiState by viewModel.uiState.collectAsState() + var mentionText by remember { mutableStateOf(TextFieldValue()) } - val messageText by viewModel.message.collectAsState() - var showExtraButtons by remember { mutableStateOf(value = messageText.isEmpty()) } + + var showExtraButtons by remember { mutableStateOf(value = uiState.message.isEmpty()) } Row( modifier = modifier - .height(48.dp) - .padding(start = 8.dp, end = 8.dp), + .height(56.dp) + .padding(start = 8.dp, top = 8.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically ) { AnimatedVisibility(visible = showExtraButtons.not()) { @@ -75,25 +85,45 @@ fun ChatMessageEditor( MessageEditorBar( modifier = Modifier .weight(2f), - search = messageText, + search = uiState.message, userName = userName, mentionText = mentionText, onValueChange = { mentionText = it showExtraButtons = false - viewModel.message.value = it.text + viewModel.updateMessage(it.text) } ) - AnimatedVisibility(visible = messageText.isNotEmpty()) { + AnimatedVisibility(visible = uiState.message.isNotEmpty()) { SendMessageButton( modifier = Modifier .padding(start = 8.dp) .weight(1f), - viewModel = viewModel, - message = mentionText.text, + messageToSend = mentionText.text, onSent = { + var url: String? = null + val linksList = + extractSpans( + text = mentionText.text, + patterns = listOf( + MentionsPatterns.urlPattern, + MentionsPatterns.hashTagPattern, + MentionsPatterns.mentionTagPattern + ) + ) + if (linksList.isNotEmpty() && linksList.first().tag == MentionsPatterns.URL_TAG) { + url = linksList.first().spanText + } + viewModel.sendMessage( + messageToSend = mentionText.text, + messageToReply = uiState.messageAction, + isReply = isReplyMode.value, + url = url + ) mentionText = TextFieldValue() + isReplyMode.value = false + viewModel.resetMessageAction() } ) } @@ -164,8 +194,7 @@ private fun MessageEditor( @Composable private fun SendMessageButton( modifier: Modifier = Modifier, - viewModel: ChatScreenViewModel, - message: String, + messageToSend: String, onSent: () -> Unit ) { IconButton( @@ -176,15 +205,14 @@ private fun SendMessageButton( shape = CircleShape ), onClick = { - viewModel.sendMessage(message) onSent() }, - enabled = message.isNotEmpty() + enabled = messageToSend.isNotEmpty() ) { Icon( painterResource(id = Drawables.ic_send_rounded), contentDescription = null, - tint = DiscordColorProvider.colors.brand + tint = Color.White ) } } @@ -299,4 +327,36 @@ fun ChatPlaceHolder( innerTextField() } } +} + +@Composable +fun ReplyBanner( + modifier: Modifier = Modifier, + userName: State, + onCloseClicked: () -> Unit +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(0.dp) + .background(DiscordColorProvider.colors.chatEditor), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Icon( + modifier = Modifier + .padding(8.dp) + .clickable { onCloseClicked() }, + imageVector = Icons.Default.Clear, + contentDescription = null, + tint = Color(0xFFbabbbf) + ) + Text( + modifier = Modifier.padding(8.dp), + text = "Replying to ${userName.value}", + style = MessageTypography.subtitle2.copy( + color = DiscordColorProvider.colors.textSecondary + ) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessageItem.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessageItem.kt index 5ec3f2c..78f5aba 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessageItem.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessageItem.kt @@ -1,48 +1,206 @@ package dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.chatscreen import android.annotation.SuppressLint +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box 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 import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ListItem +import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import coil.transform.RoundedCornersTransformation +import dev.baseio.discordjetpackcompose.custom.MentionsPatterns.URL_TAG +import dev.baseio.discordjetpackcompose.custom.MentionsPatterns.hashTagPattern +import dev.baseio.discordjetpackcompose.custom.MentionsPatterns.mentionTagPattern +import dev.baseio.discordjetpackcompose.custom.MentionsPatterns.urlPattern +import dev.baseio.discordjetpackcompose.custom.extractSpans import dev.baseio.discordjetpackcompose.entities.message.DiscordMessageEntity import dev.baseio.discordjetpackcompose.ui.theme.DiscordColorProvider import dev.baseio.discordjetpackcompose.ui.theme.MessageTypography +import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.Calendar -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable -fun ChatMessageItem(message: DiscordMessageEntity) { - ListItem( - modifier = Modifier.padding(0.dp), - icon = { - ImageBox( - Modifier.size(48.dp), - imageUrl = "https://images.unsplash.com/photo-1464863979621-258859e62245?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=686&q=80" +fun ChatMessageItem( + message: DiscordMessageEntity, + position: Int, + onItemLongPressed: (Int) -> Unit, + bottomSheetState: ModalBottomSheetState +) { + + val coroutineScope = rememberCoroutineScope() + Column( + modifier = Modifier.combinedClickable( + onClick = {}, + onLongClick = { + coroutineScope.launch { + onItemLongPressed(position) + bottomSheetState.show() + } + } + ) + ) { + if (message.replyToMessage.isNotEmpty()) { + MessageItemReplyHeader( + replyToMessage = message.replyToMessage ) - }, - text = { + } + ChatListItem( + modifier = Modifier + .padding(0.dp), + imageSize = 55.dp, + message = message + ) + if (message.metaImageUrl.isEmpty().not() + && message.metaTitle.isEmpty().not() + && message.metaTitle.isEmpty().not() + ) { + UrlPreviewItem( + url = message.metaUrl, + imageUrl = message.metaImageUrl, + title = message.metaTitle, + desc = message.metaDesc + ) + } + } +} + +@Composable +fun ChatListItem( + modifier: Modifier, + imageSize: Dp, + message: DiscordMessageEntity +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(end = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + ImageBox( + Modifier + .size(imageSize) + .padding(8.dp), + imageUrl = "https://images.unsplash.com/photo-1464863979621-258859e62245?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=686&q=80" + ) + Column( + horizontalAlignment = Alignment.Start, + modifier = Modifier.padding(start = 8.dp) + ) { ChatTitle(message) - }, - secondaryText = { - ChatBody(message) + Spacer(modifier = Modifier.height(4.dp)) + ChatBody(message = message) } - ) + } +} + +@Composable +fun UrlPreviewItem( + url: String?, + imageUrl: String?, + title: String?, + desc: String? +) { + val uriHandler = LocalUriHandler.current + Box( + modifier = Modifier + .padding(top = 2.dp, start = 64.dp, end = 16.dp) + .clickable { uriHandler.openUri(url ?: "https://www.google.com") } + .clip( + RoundedCornerShape(4.dp) + ) + .background( + color = DiscordColorProvider.colors.appBarColor.copy(alpha = 0.8f) + ) + ) { + Row( + modifier = Modifier + .padding(4.dp) + .fillMaxSize(), + verticalAlignment = Alignment.Top + ) { + MetadataTitleAndDesc( + title = title ?: "", + desc = desc ?: "", + modifier = Modifier.weight(4f) + ) + imageUrl?.let { safeImageUrl -> + ImageBox( + Modifier + .height(56.dp) + .padding(8.dp) + .weight(1f), + imageUrl = safeImageUrl + ) + } + } + } +} + +@Composable +fun MetadataTitleAndDesc( + title: String, + desc: String, + modifier: Modifier +) { + Column( + modifier = modifier + ) { + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MessageTypography.subtitle2.copy( + color = DiscordColorProvider.colors.linkColor, + fontWeight = FontWeight.SemiBold + ), + modifier = Modifier.padding(4.dp) + ) + Text( + text = desc, + maxLines = 10, + overflow = TextOverflow.Ellipsis, + style = MessageTypography.subtitle2.copy( + color = DiscordColorProvider.colors.textSecondary + ), + modifier = Modifier.padding(4.dp) + ) + } } @Composable @@ -56,8 +214,7 @@ fun ImageBox( }.build() ) Image( - modifier = Modifier - .size(40.dp) + modifier = modifier .clip(CircleShape), painter = painter, contentDescription = null @@ -65,13 +222,55 @@ fun ImageBox( } @Composable -fun ChatBody(message: DiscordMessageEntity) { +fun ChatBody( + message: DiscordMessageEntity +) { Column { + val linksList = extractSpans( + message.message, listOf(urlPattern, hashTagPattern, mentionTagPattern) + ) + val uriHandler = LocalUriHandler.current + val layoutResult = remember { mutableStateOf(null) } + + val annotatedString = buildAnnotatedString { + append(message.message) + linksList.forEach { + addStyle( + style = SpanStyle( + color = DiscordColorProvider.colors.linkColor, + textDecoration = TextDecoration.Underline + ), + start = it.start, + end = it.end + ) + addStringAnnotation( + tag = it.tag, + annotation = it.spanText, + start = it.start, + end = it.end + ) + } + } Text( - message.message, + text = annotatedString, style = MessageTypography.subtitle2.copy( color = DiscordColorProvider.colors.textSecondary - ) + ), + modifier = Modifier.pointerInput(Unit) { + detectTapGestures { offsetPosition -> + layoutResult.value?.let { + val position = it.getOffsetForPosition(offsetPosition) + annotatedString.getStringAnnotations(position, position).firstOrNull() + ?.let { result -> + when (result.tag) { + URL_TAG -> { + uriHandler.openUri(result.item) + } + } + } + } + } + }, ) } } @@ -84,13 +283,14 @@ fun ChatTitle(message: DiscordMessageEntity) { style = MessageTypography.h1.copy( color = DiscordColorProvider.colors.textPrimary ), - modifier = Modifier.padding(end = 8.dp) + modifier = Modifier.padding(top = 0.dp, bottom = 0.dp, end = 8.dp) ) Text( message.createdDate.calendar().formattedFullDateTime(), style = MessageTypography.overline.copy( color = DiscordColorProvider.colors.textSecondary.copy(alpha = 0.8f) - ) + ), + modifier = Modifier.padding(top = 0.dp, bottom = 0.dp) ) } } diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessages.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessages.kt index 30bca4c..b708057 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessages.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatMessages.kt @@ -6,6 +6,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State @@ -23,41 +25,59 @@ import dev.baseio.discordjetpackcompose.ui.theme.DiscordColorProvider import dev.baseio.discordjetpackcompose.ui.theme.MessageTypography import dev.baseio.discordjetpackcompose.viewmodels.ChatScreenViewModel -@OptIn(ExperimentalFoundationApi::class) +@OptIn( + ExperimentalFoundationApi::class, + ExperimentalMaterialApi::class +) @Composable fun ChatMessages( modifier: Modifier = Modifier, userName: State, + bottomSheetState: ModalBottomSheetState, viewModel: ChatScreenViewModel ) { val flowState by viewModel.chatMessagesFlow.collectAsState() val messages = flowState?.collectAsLazyPagingItems() val listState = rememberLazyListState() - LazyColumn(state = listState, reverseLayout = true, modifier = modifier.padding(top = 24.dp)) { + + LazyColumn( + state = listState, + reverseLayout = true, + modifier = modifier.padding(top = 24.dp) + ) { var lastDrawnMessage: String? = null + messages?.let { safeMessages -> for (messageIndex in 0 until safeMessages.itemCount) { - val message = safeMessages.peek(messageIndex)!! - item { - ChatMessageItem( - message = message - ) - } + val message = safeMessages.peek(messageIndex) - // Add chat date header - lastDrawnMessage = message.createdDate.calendar().formattedFullDate() - if (!isLastMessage(messageIndex, messages)) { - val nextMessageMonth = - messages.peek(messageIndex + 1)?.createdDate?.calendar()?.formattedFullDate() - if (nextMessageMonth != lastDrawnMessage) { + message?.let { safeMessage -> + item { + ChatMessageItem( + message = safeMessage, + position = messageIndex, + onItemLongPressed = { + viewModel.updateMessageAction(safeMessage.message) + }, + bottomSheetState = bottomSheetState + ) + } + + // Add chat date header + lastDrawnMessage = safeMessage.createdDate.calendar().formattedFullDate() + if (!isLastMessage(messageIndex, safeMessages)) { + val nextMessageMonth = + safeMessages.peek(messageIndex + 1)?.createdDate?.calendar()?.formattedFullDate() + if (nextMessageMonth != lastDrawnMessage) { + stickyHeader { + ChatHeader(safeMessage.createdDate) + } + } + } else { stickyHeader { - ChatHeader(message.createdDate) + ChatHeader(safeMessage.createdDate) } } - } else { - stickyHeader { - ChatHeader(message.createdDate) - } } } } @@ -78,7 +98,10 @@ private fun isLastMessage( @Composable private fun ChatHeader(createdDate: Long) { - Row(Modifier.padding(start = 8.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically) { + Row( + Modifier.padding(start = 8.dp, end = 8.dp, top = 16.dp, bottom = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { Divider( modifier = Modifier.weight(2f), color = DiscordColorProvider.colors.textSecondary.copy(alpha = 0.8f), diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatReply.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatReply.kt new file mode 100644 index 0000000..717a9bc --- /dev/null +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatReply.kt @@ -0,0 +1,140 @@ +package dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.chatscreen + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.baseio.discordjetpackcompose.ui.theme.DiscordColorProvider +import dev.baseio.discordjetpackcompose.ui.theme.MessageTypography + +@Composable +fun MessageItemReplyHeader( + replyToMessage: String +) { + ChatListReplyItem( + modifier = Modifier + .padding(0.dp), + replyToMessage = replyToMessage + ) +} + +@Composable +fun ReplyLine() { + Canvas( + modifier = Modifier + .size(40.dp) + .padding(0.dp) + ) { + val width = size.width + val height = size.height + val path = Path().apply { + moveTo( + x = width.times(1.5f), + y = height.times(.5f) + ) + cubicTo( + x1 = width.times(0.55f), + y1 = height.times(.5f), + + x2 = width.times(.7f), + y2 = height.times(.5f), + + x3 = width.times(.7f), + y3 = height.times(1.1f) + ) + } + drawPath( + path = path, + color = Color(0xFF838383), + style = Stroke( + width = 6f, + cap = StrokeCap.Round, + join = StrokeJoin.Miter + ) + ) + } +} + +@Composable +fun ChatReplyTitle( + toUserName: String, + message: String +) { + val annotatedString = buildAnnotatedString { + appendInlineContent(id = "imageId") + pushStyle( + SpanStyle( + fontWeight = FontWeight.Medium, + color = DiscordColorProvider.colors.textPrimary + ) + ) + append(" $toUserName ") + pushStyle( + SpanStyle( + fontWeight = FontWeight.Normal, + color = DiscordColorProvider.colors.textSecondary.copy(alpha = 0.8f) + ) + ) + append(message) + } + val inlineContentMap = mapOf( + "imageId" to InlineTextContent( + Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter) + ) { + ImageBox( + modifier = Modifier.size(32.dp), + imageUrl = "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=687&q=80" + ) + } + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = annotatedString, + maxLines = 1, + overflow = TextOverflow.Clip, + inlineContent = inlineContentMap, + style = MessageTypography.h1.copy( + color = DiscordColorProvider.colors.textPrimary, + fontSize = 14.sp + ), + modifier = Modifier.padding(start = 24.dp, end = 8.dp) + ) + } +} + +@Composable +fun ChatListReplyItem( + modifier: Modifier, + replyToMessage: String +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + ReplyLine() + ChatReplyTitle( + toUserName = "Person", + message = replyToMessage + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatScreen.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatScreen.kt index cfb4b84..a9e2b02 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatScreen.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon +import androidx.compose.material.ModalBottomSheetValue.Hidden import androidx.compose.material.SwipeableState import androidx.compose.material.Text import androidx.compose.material.icons.Icons.Filled @@ -18,10 +19,13 @@ import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.People import androidx.compose.material.icons.filled.PhoneInTalk import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -46,19 +50,22 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable fun ChatScreen( - modifier: Modifier = Modifier, - composeNavigator: ComposeNavigator, - viewModel: ChatScreenViewModel = hiltViewModel(), - focusOpacity: Float, - userName: State, - isOnline: State, - swipeableState: SwipeableState + modifier: Modifier = Modifier, + composeNavigator: ComposeNavigator, + viewModel: ChatScreenViewModel = hiltViewModel(), + focusOpacity: Float, + userName: State, + isOnline: State, + swipeableState: SwipeableState ) { val scaffoldState = rememberScaffoldState() + val bottomSheetState = rememberModalBottomSheetState(initialValue = Hidden) + val isReplyMode = remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() - SideEffect { - viewModel.fetchMessages() - } + SideEffect { + viewModel.fetchMessages() + } DiscordScaffold( modifier = Modifier.clip(RoundedCornerShape(2)), @@ -82,81 +89,98 @@ fun ChatScreen( ChatScreenContent( modifier = Modifier, viewModel = viewModel, + sheetState = bottomSheetState, + isReplyMode = isReplyMode, userName = userName ) } } + MessageActionsBottomSheet( + sheetState = bottomSheetState, + replyAction = { + isReplyMode.value = true + coroutineScope.launch { + bottomSheetState.hide() + } + } + ) } @OptIn(ExperimentalMaterialApi::class) @Composable fun ChatScreenAppBar( - name: String, - isOnline: Boolean, - swipeableState: SwipeableState + name: String, + isOnline: Boolean, + swipeableState: SwipeableState ) { - val coroutineScope = rememberCoroutineScope() - DiscordAppBar( - navigationIcon = { - CountIndicator( - count = 142, - forceCircleShape = false, - modifier = Modifier.padding(8.dp) - ) { - Icon( - imageVector = Filled.Menu, - contentDescription = stringResource(string.menu), - modifier = Modifier - .padding(start = 8.dp) - .clickable { - coroutineScope.launch { - swipeableState.animateTo(CenterScreenState.RIGHT_ANCHORED) - } - }, - ) - } - }, - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = Filled.AlternateEmail, - contentDescription = null, - tint = Color.Gray - ) - Text( - modifier = Modifier - .padding(start = 8.dp, end = 8.dp) - .wrapContentWidth(), - maxLines = 1, - style = MessageTypography.h2, - overflow = TextOverflow.Ellipsis, - text = name, - color = DiscordColorProvider.colors.onSurface - ) - OnlineIndicator( - modifier = Modifier, - isOnline = isOnline - ) - } - }, - actions = { - Icon( - imageVector = Filled.PhoneInTalk, - contentDescription = null, - modifier = Modifier.padding(end = 16.dp), - ) - Icon( - imageVector = Filled.Videocam, - contentDescription = null, - modifier = Modifier.padding(end = 16.dp), - ) - Icon( - imageVector = Filled.People, - contentDescription = null, - modifier = Modifier.padding(end = 16.dp), - ) - }, - backgroundColor = DiscordColorProvider.colors.chatTopBar, - elevation = 0.dp - ) + val coroutineScope = rememberCoroutineScope() + DiscordAppBar( + navigationIcon = { + CountIndicator( + count = 142, + forceCircleShape = false, + modifier = Modifier.padding(8.dp) + ) { + Icon( + imageVector = Filled.Menu, + contentDescription = stringResource(string.menu), + modifier = Modifier + .padding(start = 8.dp) + .clickable { + coroutineScope.launch { + swipeableState.animateTo(CenterScreenState.RIGHT_ANCHORED) + } + }, + ) + } + }, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Filled.AlternateEmail, + contentDescription = null, + tint = Color.Gray + ) + Text( + modifier = Modifier + .padding(start = 8.dp, end = 8.dp) + .wrapContentWidth(), + maxLines = 1, + style = MessageTypography.h2, + overflow = TextOverflow.Ellipsis, + text = name, + color = DiscordColorProvider.colors.onSurface + ) + OnlineIndicator( + modifier = Modifier, + isOnline = isOnline + ) + } + }, + actions = { + Icon( + imageVector = Filled.PhoneInTalk, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + ) + Icon( + imageVector = Filled.Videocam, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + ) + Icon( + imageVector = Filled.People, + contentDescription = null, + modifier = Modifier + .padding(end = 16.dp) + .clickable { + coroutineScope.launch { + swipeableState.animateTo(CenterScreenState.LEFT_ANCHORED) + } + }, + ) + }, + backgroundColor = DiscordColorProvider.colors.chatTopBar, + elevation = 0.dp + ) } \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatScreenContent.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatScreenContent.kt index dd0e2ab..f2aad2f 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatScreenContent.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/chatscreen/ChatScreenContent.kt @@ -2,7 +2,10 @@ package dev.baseio.discordjetpackcompose.ui.routes.dashboard.main.chatscreen import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.ui.Modifier import androidx.compose.ui.layout.layoutId @@ -13,10 +16,13 @@ import com.google.accompanist.insets.navigationBarsWithImePadding import com.google.accompanist.insets.statusBarsPadding import dev.baseio.discordjetpackcompose.viewmodels.ChatScreenViewModel +@OptIn(ExperimentalMaterialApi::class) @Composable fun ChatScreenContent( modifier: Modifier = Modifier, viewModel: ChatScreenViewModel, + sheetState: ModalBottomSheetState, + isReplyMode: MutableState, userName: State ) { BoxWithConstraints( @@ -31,11 +37,22 @@ fun ChatScreenContent( .layoutId("chatMessages") .fillMaxSize(), userName = userName, - viewModel = viewModel + viewModel = viewModel, + bottomSheetState = sheetState ) + if (isReplyMode.value) { + ReplyBanner( + modifier = Modifier.layoutId("chatReplyBar"), + userName = userName, + onCloseClicked = { + isReplyMode.value = false + } + ) + } ChatMessageEditor( modifier = Modifier.layoutId("chatMessageEditor"), userName = userName, + isReplyMode = isReplyMode, viewModel = viewModel ) } @@ -45,6 +62,7 @@ fun ChatScreenContent( private fun decoupledConstraints(): ConstraintSet { return ConstraintSet { val chatMessages = createRefFor("chatMessages") + val chatReplyBar = createRefFor("chatReplyBar") val chatMessageEditor = createRefFor("chatMessageEditor") constrain(chatMessages) { @@ -53,6 +71,11 @@ private fun decoupledConstraints(): ConstraintSet { end.linkTo(parent.end) bottom.linkTo(chatMessageEditor.top, margin = 64.dp) } + constrain(chatReplyBar) { + start.linkTo(parent.start) + end.linkTo(parent.end) + bottom.linkTo(chatMessageEditor.top, margin = 0.dp) + } constrain(chatMessageEditor) { start.linkTo(parent.start) end.linkTo(parent.end) diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/dasboard/DashboardScreen.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/dasboard/DashboardScreen.kt index 7585b11..ee2db8f 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/dasboard/DashboardScreen.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/main/dasboard/DashboardScreen.kt @@ -183,6 +183,7 @@ fun DashboardScreen( unreadCount = null, onClick = { selectedBottomBarItem = DashboardBottomBarItemType.Profile + navController.navigate(DiscordScreen.UserSettings.route) }, type = DashboardBottomBarItemType.Profile ), diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/UserSettings.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/UserSettings.kt new file mode 100644 index 0000000..391aa39 --- /dev/null +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/UserSettings.kt @@ -0,0 +1,89 @@ +package dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import dev.baseio.discordjetpackcompose.R.string +import dev.baseio.discordjetpackcompose.navigator.ComposeNavigator +import dev.baseio.discordjetpackcompose.ui.components.DiscordAppBar +import dev.baseio.discordjetpackcompose.ui.components.DiscordScaffold +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components.UserSettingsAppBar +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components.getAppInfoSettings +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components.getAppSettings +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components.getNitroSettingsList +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components.getUserSettingsList +import dev.baseio.discordjetpackcompose.ui.theme.DiscordColorProvider +import dev.baseio.discordjetpackcompose.ui.theme.Typography +import dev.baseio.discordjetpackcompose.ui.theme.user_settings_bg +import dev.baseio.discordjetpackcompose.utils.Constants + +@Composable +fun UserSettings( + composeNavigator: ComposeNavigator, + userProfileImage: String = Constants.MMLogoUrl, +) { + + val scaffoldState = rememberScaffoldState() + val sysUiController = rememberSystemUiController() + val colors = DiscordColorProvider.colors + val context = LocalContext.current + + SideEffect { + sysUiController.setSystemBarsColor(color = colors.discordBackgroundOne) + sysUiController.setNavigationBarColor(color = colors.discordBackgroundOne) + } + + DiscordScaffold( + scaffoldState = scaffoldState, + topAppBar = { + DiscordAppBar( + title = { + Text( + text = stringResource(string.user_settings_title), + style = Typography.h3.copy( + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ), + textAlign = TextAlign.Start, + ) + }, + actions = { + UserSettingsAppBar(composeNavigator) + }, + backgroundColor = user_settings_bg, + elevation = 0.dp + ) + }, + navigator = composeNavigator + ) { + Column( + modifier = Modifier + .fillMaxSize() + .background(user_settings_bg), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.Start + ) { + UserSettingsList( + userSettings = getUserSettingsList(context), + nitroSettings = getNitroSettingsList(context), + appSettings = getAppSettings(context), + appInfo = getAppInfoSettings(context), + profileImageUrl = userProfileImage + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/UserSettingsList.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/UserSettingsList.kt new file mode 100644 index 0000000..efa300f --- /dev/null +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/UserSettingsList.kt @@ -0,0 +1,85 @@ +package dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.TabRowDefaults.Divider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.google.accompanist.insets.navigationBarsPadding +import dev.baseio.discordjetpackcompose.R.string +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components.GetSettingsSubtitle +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components.GetTopComponent +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components.models.SettingsEntity +import dev.baseio.discordjetpackcompose.ui.theme.DiscordColorProvider.colors + +@Composable +fun UserSettingsList( + userSettings: List, + nitroSettings: List, + appSettings: List, + appInfo: List, + profileImageUrl: String, + username: String = "mutual", + discordTag: String = "#8976" +) { + val lazyListState = rememberLazyListState() + + LazyColumn(state = lazyListState, modifier = Modifier.background(colors.settingsBackground)) { + item { + GetTopComponent(lazyListState, profileImageUrl, username, discordTag) + } + + item { + GetSettingsSubtitle(title = stringResource(string.settings_user_settings)) + } + + items(userSettings.size) { index -> + UserSettingsListItem(settingsEntity = userSettings[index], + onItemClick = {}) + } + + item { + Divider(color = Color.DarkGray, thickness = 1.dp, modifier = Modifier.padding(top = 8.dp)) + GetSettingsSubtitle(title = stringResource(string.settings_nitro_settings)) + } + + items(nitroSettings.size) { index -> + UserSettingsListItem(settingsEntity = nitroSettings[index], + onItemClick = {}) + } + + item { + Divider(color = Color.DarkGray, thickness = 1.dp, modifier = Modifier.padding(top = 8.dp)) + GetSettingsSubtitle(title = stringResource(string.settings_app_settings)) + } + + items(appSettings.size) { index -> + UserSettingsListItem(settingsEntity = appSettings[index], + onItemClick = {}) + } + + item { + Divider(color = Color.DarkGray, thickness = 1.dp, modifier = Modifier.padding(top = 8.dp)) + GetSettingsSubtitle(title = stringResource(string.settings_app_info)) + } + + items(appInfo.size) { index -> + UserSettingsListItem(settingsEntity = appInfo[index], + onItemClick = {}) + } + + item { + Spacer( + modifier = Modifier + .navigationBarsPadding() + .padding(vertical = 32.dp) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/UserSettingsListItem.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/UserSettingsListItem.kt new file mode 100644 index 0000000..7c1da58 --- /dev/null +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/UserSettingsListItem.kt @@ -0,0 +1,71 @@ +package dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.baseio.discordjetpackcompose.R +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components.models.SettingsEntity +import dev.baseio.discordjetpackcompose.ui.theme.DirectMessageListTypography +import dev.baseio.discordjetpackcompose.ui.theme.DiscordColorProvider.colors +import dev.baseio.discordjetpackcompose.ui.theme.discord_settings_icon +import dev.baseio.discordjetpackcompose.ui.theme.user_settings_text +import dev.baseio.discordjetpackcompose.ui.utils.clickableWithRipple + +@Composable +fun UserSettingsListItem( + settingsEntity: SettingsEntity, + onItemClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(colors.settingsBackground) + .clickableWithRipple(onClick = onItemClick) + .padding(top = 16.dp, bottom = 16.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Icon( + painter = painterResource(id = settingsEntity.icon), contentDescription = null, + modifier = Modifier.padding(start = 16.dp), + tint = discord_settings_icon, + ) + Text( + text = settingsEntity.title, style = DirectMessageListTypography.h6, + maxLines = 1, + color = user_settings_text, + modifier = Modifier.padding(start = 24.dp) + ) + settingsEntity.currentStatus?.let { status -> + Spacer(modifier = Modifier.weight(1f)) + Icon( + painter = painterResource(id = R.drawable.ic_baseline_circle_24), + contentDescription = null, + modifier = Modifier + .size(14.dp) + .align(Alignment.CenterVertically), + tint = Color(0xFF3DA45C) + ) + Text( + text = status, + textAlign = TextAlign.End, + modifier = Modifier + .padding(start = 14.dp) + .align(Alignment.CenterVertically) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/SettingsAppBar.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/SettingsAppBar.kt new file mode 100644 index 0000000..b9a1ce5 --- /dev/null +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/SettingsAppBar.kt @@ -0,0 +1,55 @@ +package dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.material.DropdownMenu +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.Icons.Filled +import androidx.compose.material.icons.filled.Logout +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import dev.baseio.discordjetpackcompose.R.string +import dev.baseio.discordjetpackcompose.navigator.ComposeNavigator +import dev.baseio.discordjetpackcompose.navigator.DiscordScreen.Login + +@Composable +fun UserSettingsAppBar(composeNavigator: ComposeNavigator) { + var displayMenu by remember { mutableStateOf(false) } + IconButton( + onClick = { composeNavigator.navigateAndClearBackStack(Login.name) }) { + Icon( + imageVector = Filled.Logout, + contentDescription = stringResource(string.logout), + modifier = Modifier.padding(start = 8.dp), + ) + } + + IconButton(onClick = { displayMenu = !displayMenu }) { + Icon(Icons.Default.MoreVert, null) + } + DropdownMenu( + expanded = displayMenu, + onDismissRequest = { displayMenu = false }, + modifier = Modifier.background(Color.White) + ) { + DropdownMenuItem(onClick = { }) { + Text( + text = stringResource(string.debug), color = Color.Black, + textAlign = TextAlign.Center + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/SettingsData.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/SettingsData.kt new file mode 100644 index 0000000..7d8928d --- /dev/null +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/SettingsData.kt @@ -0,0 +1,138 @@ +package dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components + +import android.content.Context +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components.models.SettingsEntity +import dev.baseio.discordjetpackcompose.ui.utils.Drawables +import dev.baseio.discordjetpackcompose.ui.utils.Strings + +fun getUserSettingsList(context: Context): List { + return listOf( + + SettingsEntity( + title = context.getString(Strings.settings_set_status), + icon = Drawables.ic_baseline_account_circle_24, + currentStatus = "Online" + ), + + SettingsEntity( + title = context.getString(Strings.settings_my_account), + icon = Drawables.ic_baseline_account_box_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_user_profile), + icon = Drawables.ic_baseline_edit_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_privacy), + icon = Drawables.ic_baseline_security_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_auth_apps), + icon = Drawables.ic_baseline_vpn_key_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_connections), + icon = Drawables.ic_baseline_laptop_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_scan_qr), + icon = Drawables.ic_baseline_qr_code_24, + ) + ) +} + +fun getNitroSettingsList(context: Context): List { + return listOf( + + SettingsEntity( + title = context.getString(Strings.settings_subscribe_today), + icon = Drawables.ic_baseline_subscriptions_24 + ), + + SettingsEntity( + title = context.getString(Strings.settings_boosts), + icon = Drawables.ic_baseline_account_box_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_nitro_gifting), + icon = Drawables.ic_baseline_card_giftcard_24, + ) + ) +} + +fun getAppSettings(context: Context): List { + return listOf( + + SettingsEntity( + title = context.getString(Strings.settings_voice_video), + icon = Drawables.ic_baseline_mic_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_notification), + icon = Drawables.ic_baseline_notification_important_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_text_images), + icon = Drawables.ic_baseline_image_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_appearance), + icon = Drawables.ic_baseline_color_lens_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_accessibility), + icon = Drawables.ic_baseline_accessibility_new_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_behavior), + icon = Drawables.ic_baseline_settings_applications_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_language), + icon = Drawables.ic_baseline_language_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_activity_status), + icon = Drawables.ic_baseline_vpn_key_24, + ) + ) +} + +fun getAppInfoSettings(context: Context): List { + return listOf( + + SettingsEntity( + title = context.getString(Strings.settings_change_log), + icon = Drawables.ic_baseline_info_24 + ), + + SettingsEntity( + title = context.getString(Strings.settings_support), + icon = Drawables.ic_baseline_contact_support_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_debug_logs), + icon = Drawables.ic_baseline_info_24, + ), + + SettingsEntity( + title = context.getString(Strings.settings_acknowledgement), + icon = Drawables.ic_baseline_info_24, + ) + + ) +} \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/SettingsTitle.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/SettingsTitle.kt new file mode 100644 index 0000000..fc7f098 --- /dev/null +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/SettingsTitle.kt @@ -0,0 +1,36 @@ +package dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.baseio.discordjetpackcompose.ui.theme.DiscordColorProvider +import dev.baseio.discordjetpackcompose.ui.theme.Typography +import dev.baseio.discordjetpackcompose.ui.theme.user_settings_text + +@Composable +fun GetSettingsSubtitle(title: String) { + Box( + modifier = Modifier + .background(DiscordColorProvider.colors.settingsBackground) + .fillMaxWidth() + ) { + Text( + text = title, + style = Typography.h3.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 15.sp, + color = user_settings_text + ), + textAlign = TextAlign.Start, + modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/TopBarComponents.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/TopBarComponents.kt new file mode 100644 index 0000000..c92c809 --- /dev/null +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/TopBarComponents.kt @@ -0,0 +1,130 @@ +package dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import dev.baseio.discordjetpackcompose.R.string +import dev.baseio.discordjetpackcompose.ui.routes.dashboard.components.OnlineIndicator +import dev.baseio.discordjetpackcompose.ui.theme.Typography +import dev.baseio.discordjetpackcompose.ui.theme.user_settings_bg +import dev.baseio.discordjetpackcompose.ui.utils.rememberCoilImageRequest + +@Composable +fun GetTopComponent( + lazyListState: LazyListState, + profileImageUrl: String, + username: String, + discordTag: String +) { + var scrolledY = 0f + var previousOffset = 0 + + Column( + Modifier + .fillMaxWidth() + .background(user_settings_bg) + .graphicsLayer { + scrolledY += lazyListState.firstVisibleItemScrollOffset - previousOffset + translationY = scrolledY * 0.5f + previousOffset = lazyListState.firstVisibleItemScrollOffset + }, horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top + ) { + GetImageWithBanner(profileImageUrl) + GetUsername(username,discordTag) + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +fun GetImageWithBanner(profileImageUrl: String) { + Spacer( + modifier = Modifier + .height(40.dp) + .fillMaxWidth() + .background(Color(0xFF00a6d5)) + ) + Box( + Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + Spacer( + modifier = Modifier + .height(40.dp) + .fillMaxWidth() + .background(Color(0xFF00a6d5)) + ) + Box( + modifier = Modifier + .padding(start = 16.dp) + .fillMaxWidth() + .wrapContentHeight() + ) { + OnlineIndicator( + isOnline = true, + indicatorSize = 26.dp + ) { + AsyncImage( + model = rememberCoilImageRequest(data = profileImageUrl), + contentDescription = stringResource(string.settings_profile_image), + modifier = Modifier + .size(84.dp) + .clip(CircleShape) + .border(4.dp, user_settings_bg, CircleShape) + ) + } + } + } +} + +@Composable +fun GetUsername( username: String, + discordTag: String) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, top = 8.dp) + ) { + Text( + text = username, + style = Typography.h3.copy( + fontWeight = FontWeight.Bold, + fontSize = 22.sp + ), + textAlign = TextAlign.Start + ) + Text( + text = discordTag, + style = Typography.h3.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + color = Color.Gray + ), + textAlign = TextAlign.Start + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/models/SettingsEntity.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/models/SettingsEntity.kt new file mode 100644 index 0000000..93c92d0 --- /dev/null +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/routes/dashboard/userSettings/components/models/SettingsEntity.kt @@ -0,0 +1,8 @@ +package dev.baseio.discordjetpackcompose.ui.routes.dashboard.userSettings.components.models + +data class SettingsEntity( + val title: String, + val icon: Int, + val currentStatus: String? = null, + val statusIcon: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Color.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Color.kt index a167d34..4a0f227 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Color.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Color.kt @@ -827,4 +827,53 @@ val discord_icon_button_bg Color(0xFF2f3238) } else { Color(0xFFeaeaeb) - } \ No newline at end of file + } + +val user_settings_text + @Composable get() = if (isSystemInDarkTheme()) { + Color(0xFFBABBBF) + } else { + Color(0xFF515963) + } + +val user_settings_bg + @Composable get() = if (isSystemInDarkTheme()) { + Color(0xFF303136) + } else { + Color(0xFFFFFFFF) + } + +val discord_settings_icon + @Composable get() = if (isSystemInDarkTheme()) { + Color(0xFFBABBBF) + } else { + Color(0xFF303136) + } + +val channel_member_action_icon + @Composable get() = if (isSystemInDarkTheme()) { + Color(0xFFbbbabf) + } else { + Color(0xFF4e555f) + } + +val channel_member_action_label + @Composable get() = if (isSystemInDarkTheme()) { + Color(0xFFc5c4c9) + } else { + Color(0xFF4e555f) + } + +val channel_member_bg +@Composable get() = if(isSystemInDarkTheme()){ + Color(0xFF302f34) +}else{ + Color(0xFFf4f4f4) +} + +val channel_member_secondary_bg + @Composable get() = if(isSystemInDarkTheme()){ + Color(0xFF35383f) + }else{ + Color(0xFFFFFFFF) + } diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Theme.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Theme.kt index 820d1ab..634bbd9 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Theme.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Theme.kt @@ -3,10 +3,17 @@ package dev.baseio.discordjetpackcompose.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.ui.graphics.Color - private val LightColorPalette = DiscordColorPalette( primary = design_default_color_primary, primaryVariant = design_default_color_primary_variant, @@ -35,6 +42,7 @@ private val LightColorPalette = DiscordColorPalette( tabsBackgroundColor = design_default_color_secondary_background, tabSelectedColor = design_default_tab_selected_color, textFieldContentColor = text_field_content_color_light, + settingsBackground = Color(0xFFFFFFFF), isLight = true ) @@ -66,6 +74,7 @@ private val DarkColorPalette = DiscordColorPalette( tabsBackgroundColor = Color(0xFF2a2b2f), tabSelectedColor = Color(0xFF4f535c), textFieldContentColor = text_field_content_color_Dark, + settingsBackground = Color(0xFF363940), isLight = false ) @@ -142,6 +151,7 @@ class DiscordColorPalette( tabsBackgroundColor: Color, tabSelectedColor: Color, textFieldContentColor: Color, + settingsBackground: Color, isLight: Boolean = false ) { var primary by mutableStateOf(primary, structuralEqualityPolicy()) @@ -200,6 +210,8 @@ class DiscordColorPalette( internal set var tabSelectedColor by mutableStateOf(tabSelectedColor, structuralEqualityPolicy()) internal set + var settingsBackground by mutableStateOf(settingsBackground, structuralEqualityPolicy()) + internal set fun update(other: DiscordColorPalette) { primary = other.primary diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Type.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Type.kt index 3fd7bb7..fb2a0cf 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Type.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/ui/theme/Type.kt @@ -100,6 +100,7 @@ val ServerInfoTypography = Typography( val MessageTypography = Typography( defaultFontFamily = UniSansFontFamily, h1 = TextStyle( + fontFamily = UniSansFontFamily, fontWeight = FontWeight.Medium, fontSize = 18.sp, ), @@ -119,10 +120,13 @@ val MessageTypography = Typography( fontSize = 14.sp ), subtitle1 = TextStyle( + fontFamily = UniSansFontFamily, fontWeight = FontWeight.Medium, fontSize = 18.sp ), subtitle2 = TextStyle( + fontFamily = UniSansFontFamily, + fontWeight = FontWeight.Normal, fontSize = 14.sp, ) ) diff --git a/app/src/main/java/dev/baseio/discordjetpackcompose/viewmodels/ChatScreenViewModel.kt b/app/src/main/java/dev/baseio/discordjetpackcompose/viewmodels/ChatScreenViewModel.kt index d02eb50..2f01f81 100644 --- a/app/src/main/java/dev/baseio/discordjetpackcompose/viewmodels/ChatScreenViewModel.kt +++ b/app/src/main/java/dev/baseio/discordjetpackcompose/viewmodels/ChatScreenViewModel.kt @@ -5,42 +5,94 @@ import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import dagger.hilt.android.lifecycle.HiltViewModel import dev.baseio.discordjetpackcompose.entities.message.DiscordMessageEntity +import dev.baseio.discordjetpackcompose.entities.message.DiscordUrlMetaEntity import dev.baseio.discordjetpackcompose.usecases.chat.FetchMessagesUseCase +import dev.baseio.discordjetpackcompose.usecases.chat.FetchUrlMetadataUseCase import dev.baseio.discordjetpackcompose.usecases.chat.SendMessageUseCase import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.util.UUID import javax.inject.Inject +/** + * UI state for the Chat screen + */ +data class ChatUiState( + val message: String = "", + val messageAction: String = "", + val loading: Boolean = false, +) + @HiltViewModel class ChatScreenViewModel @Inject constructor( private val fetchMessagesUseCase: FetchMessagesUseCase, - private val sendMessageUseCase: SendMessageUseCase + private val sendMessageUseCase: SendMessageUseCase, + private val fetchUrlMetadataUseCase: FetchUrlMetadataUseCase ) : ViewModel() { + // UI state exposed to the UI + private val _uiState = MutableStateFlow(ChatUiState(loading = true)) + val uiState: StateFlow = _uiState.asStateFlow() + var chatMessagesFlow = MutableStateFlow>?>(null) - var message = MutableStateFlow("") fun fetchMessages() { chatMessagesFlow.value = fetchMessagesUseCase.performStreaming("1") } - fun sendMessage(search: String) { - if (search.isNotEmpty()) { + fun sendMessage( + messageToSend: String, + messageToReply: String, + isReply: Boolean = false, + url: String? = null + ) { + if (messageToSend.isNotEmpty()) { viewModelScope.launch { + var discordUrlMetaEntity: DiscordUrlMetaEntity? = null + url?.let { safeUrl -> + discordUrlMetaEntity = fetchUrlMetadataUseCase.perform(safeUrl) + } + val message = DiscordMessageEntity( uuid = UUID.randomUUID().toString(), channelId = "1", - message = search, + message = messageToSend, userId = UUID.randomUUID().toString(), createdBy = "Person", createdDate = System.currentTimeMillis(), modifiedDate = System.currentTimeMillis(), + replyTo = if (isReply) "Person" else "", + replyToMessage = messageToReply, + metaTitle = discordUrlMetaEntity?.title ?: "", + metaDesc = discordUrlMetaEntity?.desc ?: "", + metaImageUrl = discordUrlMetaEntity?.image ?: "", + metaUrl = discordUrlMetaEntity?.url ?: "" ) sendMessageUseCase.perform(message) } - message.value = "" + updateMessage("") + } + } + + fun updateMessage(message: String) { + _uiState.update { chatUiState -> + chatUiState.copy(message = message) + } + } + + fun updateMessageAction(action: String) { + _uiState.update { chatUiState -> + chatUiState.copy(messageAction = action) + } + } + + fun resetMessageAction() { + _uiState.update { chatUiState -> + chatUiState.copy(messageAction = "") } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_accessibility_new_24.xml b/app/src/main/res/drawable/ic_baseline_accessibility_new_24.xml new file mode 100644 index 0000000..6c0fa6f --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_accessibility_new_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_account_box_24.xml b/app/src/main/res/drawable/ic_baseline_account_box_24.xml new file mode 100644 index 0000000..ff7b180 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_account_box_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_account_circle_24.xml b/app/src/main/res/drawable/ic_baseline_account_circle_24.xml new file mode 100644 index 0000000..bf86aa3 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_account_circle_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_card_giftcard_24.xml b/app/src/main/res/drawable/ic_baseline_card_giftcard_24.xml new file mode 100644 index 0000000..f20e85a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_card_giftcard_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_circle_24.xml b/app/src/main/res/drawable/ic_baseline_circle_24.xml new file mode 100644 index 0000000..0c85947 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_circle_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_color_lens_24.xml b/app/src/main/res/drawable/ic_baseline_color_lens_24.xml new file mode 100644 index 0000000..032d574 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_color_lens_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_contact_support_24.xml b/app/src/main/res/drawable/ic_baseline_contact_support_24.xml new file mode 100644 index 0000000..2cb0591 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_contact_support_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_edit_24.xml b/app/src/main/res/drawable/ic_baseline_edit_24.xml new file mode 100644 index 0000000..83871fa --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_edit_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_image_24.xml b/app/src/main/res/drawable/ic_baseline_image_24.xml new file mode 100644 index 0000000..0559e37 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_image_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_info_24.xml b/app/src/main/res/drawable/ic_baseline_info_24.xml new file mode 100644 index 0000000..d970aab --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_info_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_language_24.xml b/app/src/main/res/drawable/ic_baseline_language_24.xml new file mode 100644 index 0000000..2f1cabc --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_language_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_laptop_24.xml b/app/src/main/res/drawable/ic_baseline_laptop_24.xml new file mode 100644 index 0000000..6284a24 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_laptop_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_mic_24.xml b/app/src/main/res/drawable/ic_baseline_mic_24.xml new file mode 100644 index 0000000..3aa3c6e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_mic_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_notification_important_24.xml b/app/src/main/res/drawable/ic_baseline_notification_important_24.xml new file mode 100644 index 0000000..a8c6859 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_notification_important_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_qr_code_24.xml b/app/src/main/res/drawable/ic_baseline_qr_code_24.xml new file mode 100644 index 0000000..f524890 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_qr_code_24.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_security_24.xml b/app/src/main/res/drawable/ic_baseline_security_24.xml new file mode 100644 index 0000000..7f40242 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_security_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_settings_applications_24.xml b/app/src/main/res/drawable/ic_baseline_settings_applications_24.xml new file mode 100644 index 0000000..1aa51ad --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_settings_applications_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_subscriptions_24.xml b/app/src/main/res/drawable/ic_baseline_subscriptions_24.xml new file mode 100644 index 0000000..92637cc --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_subscriptions_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_vpn_key_24.xml b/app/src/main/res/drawable/ic_baseline_vpn_key_24.xml new file mode 100644 index 0000000..3ad999e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_vpn_key_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_emoji_1.xml b/app/src/main/res/drawable/ic_emoji_1.xml new file mode 100644 index 0000000..f5ef0db --- /dev/null +++ b/app/src/main/res/drawable/ic_emoji_1.xml @@ -0,0 +1,41 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_emoji_2.xml b/app/src/main/res/drawable/ic_emoji_2.xml new file mode 100644 index 0000000..3f6f997 --- /dev/null +++ b/app/src/main/res/drawable/ic_emoji_2.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_emoji_3.xml b/app/src/main/res/drawable/ic_emoji_3.xml new file mode 100644 index 0000000..8b8f7a5 --- /dev/null +++ b/app/src/main/res/drawable/ic_emoji_3.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_emoji_4.xml b/app/src/main/res/drawable/ic_emoji_4.xml new file mode 100644 index 0000000..dc2f3ab --- /dev/null +++ b/app/src/main/res/drawable/ic_emoji_4.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_emoji_5.xml b/app/src/main/res/drawable/ic_emoji_5.xml new file mode 100644 index 0000000..300c0f9 --- /dev/null +++ b/app/src/main/res/drawable/ic_emoji_5.xml @@ -0,0 +1,36 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_hashtag_solid.xml b/app/src/main/res/drawable/ic_hashtag_solid.xml new file mode 100644 index 0000000..975cfa5 --- /dev/null +++ b/app/src/main/res/drawable/ic_hashtag_solid.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 01fcd23..8167c08 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,4 +85,40 @@ LAST CHANNEL SUGGESTIONS Unknown item type! Please add this type inside the DashboardBottomBarItemType enum class. + User Settings + logout + Debug + profile + Set Status + My Account + User Profile + + Authorized Apps + Connections + Scan QR Code + Subscribe Today + Boosts + Nitro Gifting + + Notification + + Appearance + Accessibility + Behavior + Language + Activity Status + Change Log + Support + Upload debug logs to Discord Support + Acknowledgement + USER SETTINGS + NITRO SETTINGS + APP SETTINGS + APP INFORMATION + Notifications + Settings + Threads + Pins + Invite Members + # welcome \ No newline at end of file diff --git a/art/discord_channel_members_dark.png b/art/discord_channel_members_dark.png new file mode 100644 index 0000000..8f84acc Binary files /dev/null and b/art/discord_channel_members_dark.png differ diff --git a/art/discord_channel_members_light.png b/art/discord_channel_members_light.png new file mode 100644 index 0000000..17d4ba5 Binary files /dev/null and b/art/discord_channel_members_light.png differ diff --git a/art/discord_chat_dark.png b/art/discord_chat_dark.png new file mode 100644 index 0000000..9519db8 Binary files /dev/null and b/art/discord_chat_dark.png differ diff --git a/art/discord_chat_light.png b/art/discord_chat_light.png new file mode 100644 index 0000000..f1d9b6e Binary files /dev/null and b/art/discord_chat_light.png differ diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index e0c70d7..9986d39 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -60,6 +60,7 @@ object Lib { "com.google.accompanist:accompanist-insets:${ACCOMPANIST_VERSION}" const val ACCOMPANIST_INSETS_UI = "com.google.accompanist:accompanist-insets-ui:${ACCOMPANIST_VERSION}" + const val ACCOMPANIST_COIL = "com.google.accompanist:accompanist-coil:0.14.0" const val MATERIAL_EXTENDED_ICONS = "androidx.compose.material:material-icons-extended:$COMPOSE_VERSION" const val COMPOSE_JUNIT = "androidx.compose.ui:ui-test-junit4:$COMPOSE_VERSION" @@ -83,6 +84,11 @@ object Lib { const val PAGING_COMPOSE = "androidx.paging:paging-compose:1.0.0-alpha14" } + object Jsoup { + private const val JSOUP_VERSION = "1.13.1" + const val JSOUP = "org.jsoup:jsoup:${JSOUP_VERSION}" + } + object Room { private const val roomVersion = "2.4.1" const val roomRuntime = "androidx.room:room-runtime:$roomVersion" diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 9718866..50ba940 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -40,6 +40,8 @@ dependencies { /* Paging */ implementation(Lib.Paging.PAGING_3) + implementation(Lib.Jsoup.JSOUP) + /* Room */ api(Lib.Room.roomRuntime) kapt(Lib.Room.roomCompiler) diff --git a/data/src/main/java/dev/baseio/discordjetpackcompose/local/model/DBDiscordMessage.kt b/data/src/main/java/dev/baseio/discordjetpackcompose/local/model/DBDiscordMessage.kt index 550fa09..bd2de2f 100644 --- a/data/src/main/java/dev/baseio/discordjetpackcompose/local/model/DBDiscordMessage.kt +++ b/data/src/main/java/dev/baseio/discordjetpackcompose/local/model/DBDiscordMessage.kt @@ -10,7 +10,13 @@ data class DBDiscordMessage( @ColumnInfo(name = "channelId") val channelId: String, @ColumnInfo(name = "message") val message: String, @ColumnInfo(name = "from") val userId: String, + @ColumnInfo(name = "replyTo") val replyTo: String, + @ColumnInfo(name = "replyToMessage") val replyToMessage: String, @ColumnInfo(name = "createdBy") val createdBy: String, @ColumnInfo(name = "createdDate") val createdDate: Long, @ColumnInfo(name = "modifiedDate") val modifiedDate: Long, + @ColumnInfo(name = "metaTitle") val metaTitle: String, + @ColumnInfo(name = "metaDesc") val metaDesc: String, + @ColumnInfo(name = "metaImageUrl") val metaImageUrl: String, + @ColumnInfo(name = "metaUrl") val metaUrl: String ) \ No newline at end of file diff --git a/data/src/main/java/dev/baseio/discordjetpackcompose/mappers/DiscordMessageMapper.kt b/data/src/main/java/dev/baseio/discordjetpackcompose/mappers/DiscordMessageMapper.kt index 3ea7e59..f4b851e 100644 --- a/data/src/main/java/dev/baseio/discordjetpackcompose/mappers/DiscordMessageMapper.kt +++ b/data/src/main/java/dev/baseio/discordjetpackcompose/mappers/DiscordMessageMapper.kt @@ -12,9 +12,15 @@ class DiscordMessageMapper @Inject constructor() : EntityMapper() + override fun fetchMessages(params: String?): Flow> { return Pager(PagingConfig(pageSize = 20)) { discordMessageDao.messagesByDate(params) @@ -32,4 +36,29 @@ class MessagesRepoImpl @Inject constructor( params } } + + override suspend fun fetchUrlMetadata(url: String?): DiscordUrlMetaEntity? { + val discordUrlMetaEntity = DiscordUrlMetaEntity() + if (cacheUrlMap.containsKey(url)) return cacheUrlMap[url!!] + + withContext(coroutineMainDispatcherProvider.io) { + val con = Jsoup.connect(url) + val doc = con.userAgent("Mozilla").get() + val ogTags = doc.select("meta[property^=og:]") + when { + ogTags.size > 0 -> + ogTags.forEachIndexed { index, _ -> + val tag = ogTags[index] + when (tag.attr("property")) { + "og:image" -> discordUrlMetaEntity.image = tag.attr("content") + "og:description" -> discordUrlMetaEntity.desc = tag.attr("content") + "og:url" -> discordUrlMetaEntity.url = tag.attr("content") + "og:title" -> discordUrlMetaEntity.title = tag.attr("content") + } + } + } + cacheUrlMap[url!!] = discordUrlMetaEntity + } + return discordUrlMetaEntity + } } \ No newline at end of file diff --git a/domain/src/main/java/dev/baseio/discordjetpackcompose/entities/message/DiscordMessageEntity.kt b/domain/src/main/java/dev/baseio/discordjetpackcompose/entities/message/DiscordMessageEntity.kt index dae6a94..bd943ec 100644 --- a/domain/src/main/java/dev/baseio/discordjetpackcompose/entities/message/DiscordMessageEntity.kt +++ b/domain/src/main/java/dev/baseio/discordjetpackcompose/entities/message/DiscordMessageEntity.kt @@ -5,7 +5,13 @@ data class DiscordMessageEntity( val channelId: String, val message: String, val userId: String, + val replyTo: String, + val replyToMessage: String, val createdBy: String, val createdDate: Long, val modifiedDate: Long, + val metaTitle: String, + val metaDesc: String, + val metaImageUrl: String, + val metaUrl: String ) \ No newline at end of file diff --git a/domain/src/main/java/dev/baseio/discordjetpackcompose/entities/message/DiscordUrlMetaEntity.kt b/domain/src/main/java/dev/baseio/discordjetpackcompose/entities/message/DiscordUrlMetaEntity.kt new file mode 100644 index 0000000..fc31a15 --- /dev/null +++ b/domain/src/main/java/dev/baseio/discordjetpackcompose/entities/message/DiscordUrlMetaEntity.kt @@ -0,0 +1,8 @@ +package dev.baseio.discordjetpackcompose.entities.message + +data class DiscordUrlMetaEntity( + var title: String? = null, + var desc: String? = null, + var image: String? = null, + var url: String? = null +) \ No newline at end of file diff --git a/domain/src/main/java/dev/baseio/discordjetpackcompose/repositories/MessagesRepo.kt b/domain/src/main/java/dev/baseio/discordjetpackcompose/repositories/MessagesRepo.kt index 140a36d..50e6a3d 100644 --- a/domain/src/main/java/dev/baseio/discordjetpackcompose/repositories/MessagesRepo.kt +++ b/domain/src/main/java/dev/baseio/discordjetpackcompose/repositories/MessagesRepo.kt @@ -2,9 +2,11 @@ package dev.baseio.discordjetpackcompose.repositories import androidx.paging.PagingData import dev.baseio.discordjetpackcompose.entities.message.DiscordMessageEntity +import dev.baseio.discordjetpackcompose.entities.message.DiscordUrlMetaEntity import kotlinx.coroutines.flow.Flow interface MessagesRepo { fun fetchMessages(params: String?): Flow> suspend fun sendMessage(params: DiscordMessageEntity): DiscordMessageEntity + suspend fun fetchUrlMetadata(url: String?): DiscordUrlMetaEntity? } \ No newline at end of file diff --git a/domain/src/main/java/dev/baseio/discordjetpackcompose/usecases/chat/FetchUrlMetadataUseCase.kt b/domain/src/main/java/dev/baseio/discordjetpackcompose/usecases/chat/FetchUrlMetadataUseCase.kt new file mode 100644 index 0000000..7ce8f6a --- /dev/null +++ b/domain/src/main/java/dev/baseio/discordjetpackcompose/usecases/chat/FetchUrlMetadataUseCase.kt @@ -0,0 +1,13 @@ +package dev.baseio.discordjetpackcompose.usecases.chat + +import dev.baseio.discordjetpackcompose.entities.message.DiscordUrlMetaEntity +import dev.baseio.discordjetpackcompose.repositories.MessagesRepo +import dev.baseio.discordjetpackcompose.usecases.BaseUseCase +import javax.inject.Inject + +class FetchUrlMetadataUseCase @Inject constructor(private val messagesRepo: MessagesRepo) : + BaseUseCase { + override suspend fun perform(params: String): DiscordUrlMetaEntity? { + return messagesRepo.fetchUrlMetadata(params) + } +} diff --git a/navigator/src/main/java/dev/baseio/discordjetpackcompose/navigator/Screens.kt b/navigator/src/main/java/dev/baseio/discordjetpackcompose/navigator/Screens.kt index 45458d8..fba9663 100644 --- a/navigator/src/main/java/dev/baseio/discordjetpackcompose/navigator/Screens.kt +++ b/navigator/src/main/java/dev/baseio/discordjetpackcompose/navigator/Screens.kt @@ -15,6 +15,7 @@ sealed class DiscordScreen( object Friends : DiscordScreen("friends") object CreateServer : DiscordScreen("createServer") object Invite : DiscordScreen("invite") + object UserSettings: DiscordScreen("userSettings") object Home : DiscordScreen("home") object Search : DiscordScreen("search") }