From af2248706024b3f76bcd589f3df2ad6d43a92c1b Mon Sep 17 00:00:00 2001 From: Samuel Urbanowicz Date: Tue, 5 Oct 2021 17:57:02 +0200 Subject: [PATCH 01/24] Clean up and adjust theme colors --- slack-clone-compose-sample/build.gradle | 2 +- .../src/main/res/values-night/themes.xml | 16 +++++----------- .../src/main/res/values/colors.xml | 8 +++----- .../src/main/res/values/themes.xml | 16 +++++----------- 4 files changed, 14 insertions(+), 28 deletions(-) diff --git a/slack-clone-compose-sample/build.gradle b/slack-clone-compose-sample/build.gradle index f1ac96f0..24afc5e2 100644 --- a/slack-clone-compose-sample/build.gradle +++ b/slack-clone-compose-sample/build.gradle @@ -10,7 +10,7 @@ android { defaultConfig { applicationId "io.getstream.slack.compose" - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 31 versionCode 1 versionName "1.0" diff --git a/slack-clone-compose-sample/src/main/res/values-night/themes.xml b/slack-clone-compose-sample/src/main/res/values-night/themes.xml index de4bb855..ef31a71a 100644 --- a/slack-clone-compose-sample/src/main/res/values-night/themes.xml +++ b/slack-clone-compose-sample/src/main/res/values-night/themes.xml @@ -1,16 +1,10 @@ - diff --git a/slack-clone-compose-sample/src/main/res/values/colors.xml b/slack-clone-compose-sample/src/main/res/values/colors.xml index ca1931bc..bd4942ac 100644 --- a/slack-clone-compose-sample/src/main/res/values/colors.xml +++ b/slack-clone-compose-sample/src/main/res/values/colors.xml @@ -1,10 +1,8 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 + #57265A #FF000000 + #FF111111 + #FFEEEEEE #FFFFFFFF diff --git a/slack-clone-compose-sample/src/main/res/values/themes.xml b/slack-clone-compose-sample/src/main/res/values/themes.xml index c451c042..2612b00e 100644 --- a/slack-clone-compose-sample/src/main/res/values/themes.xml +++ b/slack-clone-compose-sample/src/main/res/values/themes.xml @@ -1,16 +1,10 @@ - From 9159e19c33cab6556c29dffe14bcc3fa58751af7 Mon Sep 17 00:00:00 2001 From: Samuel Urbanowicz Date: Wed, 6 Oct 2021 14:25:29 +0200 Subject: [PATCH 02/24] Add base messages screen --- .../src/main/AndroidManifest.xml | 3 + .../ChannelListScreen.kt | 2 +- .../MessageListScreen.kt | 2 +- .../compose/ui/messages/MessagesActivity.kt | 110 ++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) rename slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/{channel_list => channels}/ChannelListScreen.kt (62%) rename slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/{message_list => messages}/MessageListScreen.kt (62%) create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt diff --git a/slack-clone-compose-sample/src/main/AndroidManifest.xml b/slack-clone-compose-sample/src/main/AndroidManifest.xml index 82fbe629..f73f2be0 100644 --- a/slack-clone-compose-sample/src/main/AndroidManifest.xml +++ b/slack-clone-compose-sample/src/main/AndroidManifest.xml @@ -15,6 +15,9 @@ android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.SlackClone"> + + diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channel_list/ChannelListScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelListScreen.kt similarity index 62% rename from slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channel_list/ChannelListScreen.kt rename to slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelListScreen.kt index 56704671..be0efed7 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channel_list/ChannelListScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelListScreen.kt @@ -1,4 +1,4 @@ -package io.getstream.slack.compose.ui.channel_list +package io.getstream.slack.compose.ui.channels import androidx.compose.runtime.Composable diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/message_list/MessageListScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessageListScreen.kt similarity index 62% rename from slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/message_list/MessageListScreen.kt rename to slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessageListScreen.kt index bcc42e1b..a8134092 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/message_list/MessageListScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessageListScreen.kt @@ -1,4 +1,4 @@ -package io.getstream.slack.compose.ui.message_list +package io.getstream.slack.compose.ui.messages import androidx.compose.runtime.Composable diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt new file mode 100644 index 00000000..b2ce7663 --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt @@ -0,0 +1,110 @@ +package io.getstream.slack.compose.ui.messages + +import android.content.ClipboardManager +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +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.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.compose.ui.messages.composer.MessageComposer +import io.getstream.chat.android.compose.ui.messages.header.MessageListHeader +import io.getstream.chat.android.compose.ui.messages.list.MessageList +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.viewmodel.messages.AttachmentsPickerViewModel +import io.getstream.chat.android.compose.viewmodel.messages.MessageComposerViewModel +import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel +import io.getstream.chat.android.compose.viewmodel.messages.MessagesViewModelFactory +import io.getstream.chat.android.offline.ChatDomain + +class MessagesActivity : AppCompatActivity() { + private val factory: MessagesViewModelFactory by lazy { + val channelId = "messaging:sample-app-channel-0" // TODO: obtain cid from Intent + return@lazy MessagesViewModelFactory( + context = this, + clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager, + chatClient = ChatClient.instance(), + chatDomain = ChatDomain.instance(), + channelId = channelId, + enforceUniqueReactions = true, + messageLimit = 30 + ) + } + + private val listViewModel by viewModels(factoryProducer = { factory }) + private val composerViewModel by viewModels(factoryProducer = { factory }) + val attachmentsPickerViewModel by viewModels(factoryProducer = { factory }) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + ChatTheme { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + Header( + listViewModel = listViewModel, + attachmentsPickerViewModel = attachmentsPickerViewModel + ) + }, + bottomBar = { + MessageComposer(viewModel = composerViewModel) + } + ) { + MessageList( + modifier = Modifier + .fillMaxSize() + .padding(it), + viewModel = listViewModel, + ) + } + } + } + } + + @Composable + fun Header( + listViewModel: MessageListViewModel, + attachmentsPickerViewModel: AttachmentsPickerViewModel + ) { + val user by listViewModel.user.collectAsState() + val isNetworkAvailable by listViewModel.isOnline.collectAsState() + val messageMode = listViewModel.messageMode + val backAction = { + val isInThread = listViewModel.isInThread + val isShowingOverlay = listViewModel.isShowingOverlay + + when { + attachmentsPickerViewModel.isShowingAttachments -> attachmentsPickerViewModel.changeAttachmentState( + false + ) + isShowingOverlay -> listViewModel.selectMessage(null) + isInThread -> { + listViewModel.leaveThread() + composerViewModel.leaveThread() + } + else -> onBackPressed() + } + } + + MessageListHeader( + modifier = Modifier.fillMaxWidth().height(56.dp), + channel = listViewModel.channel, + currentUser = user, + isNetworkAvailable = isNetworkAvailable, + messageMode = messageMode, + onHeaderActionClick = {}, + onBackPressed = backAction + ) + } +} From 8517f05f682b7b766286d474a809519ccac270cb Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 01:04:53 +0300 Subject: [PATCH 03/24] [2127] Add channel list screen --- .../src/main/AndroidManifest.xml | 10 +- .../getstream/slack/compose/SlackCloneApp.kt | 4 +- .../slack/compose/model/Workspace.kt | 15 ++ .../slack/compose/ui/MainActivity.kt | 18 -- .../slack/compose/ui/channels/ChannelItem.kt | 173 ++++++++++++++++++ .../compose/ui/channels/ChannelListScreen.kt | 7 - .../compose/ui/channels/ChannelsActivity.kt | 55 ++++++ .../compose/ui/channels/ChannelsScreen.kt | 107 +++++++++++ .../ui/channels/components/OnlineIndicator.kt | 33 ++++ .../ui/channels/components/UnreadCount.kt | 44 +++++ .../slack/compose/ui/home/HomeScreen.kt | 9 - .../compose/ui/messages/MessageListScreen.kt | 7 - .../compose/ui/messages/MessagesActivity.kt | 16 +- .../slack/compose/ui/theme/SlackColors.kt | 38 ++++ .../slack/compose/ui/theme/SlackShapes.kt | 23 +++ .../slack/compose/ui/theme/SlackTheme.kt | 22 +++ .../slack/compose/ui/theme/SlackTypography.kt | 54 ++++++ .../slack/compose/ui/util/ChannelUtils.kt | 38 ++++ .../src/main/res/drawable/ic_channel.xml | 4 + .../src/main/res/font/lato_bold.ttf | Bin 0 -> 73316 bytes .../src/main/res/font/lato_light.ttf | Bin 0 -> 77192 bytes .../src/main/res/font/lato_regular.ttf | Bin 0 -> 75136 bytes .../src/main/res/values/strings.xml | 6 + 23 files changed, 634 insertions(+), 49 deletions(-) create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt delete mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/MainActivity.kt create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt delete mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelListScreen.kt create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCount.kt delete mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/home/HomeScreen.kt delete mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessageListScreen.kt create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTheme.kt create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTypography.kt create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt create mode 100644 slack-clone-compose-sample/src/main/res/drawable/ic_channel.xml create mode 100644 slack-clone-compose-sample/src/main/res/font/lato_bold.ttf create mode 100644 slack-clone-compose-sample/src/main/res/font/lato_light.ttf create mode 100644 slack-clone-compose-sample/src/main/res/font/lato_regular.ttf diff --git a/slack-clone-compose-sample/src/main/AndroidManifest.xml b/slack-clone-compose-sample/src/main/AndroidManifest.xml index f73f2be0..a6c3f967 100644 --- a/slack-clone-compose-sample/src/main/AndroidManifest.xml +++ b/slack-clone-compose-sample/src/main/AndroidManifest.xml @@ -11,12 +11,7 @@ android:supportsRtl="false" android:theme="@style/Theme.SlackClone"> - - @@ -24,6 +19,9 @@ + \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/SlackCloneApp.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/SlackCloneApp.kt index d118b572..6cfb6147 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/SlackCloneApp.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/SlackCloneApp.kt @@ -18,7 +18,9 @@ class SlackCloneApp : Application() { val client = ChatClient.Builder("qx5us2v6xvmh", applicationContext) .logLevel(ChatLogLevel.ALL) .build() - ChatDomain.Builder(client, applicationContext).build() + ChatDomain.Builder(client, applicationContext) + .userPresenceEnabled() + .build() } private fun connectUser() { diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt new file mode 100644 index 00000000..89bfce22 --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt @@ -0,0 +1,15 @@ +package io.getstream.slack.compose.model + +import androidx.annotation.DrawableRes + +/** + * A model that represents a Slack workspace. + * + * @param title The name name of the workspace. + * @param logo The logo of the workspace. + */ +data class Workspace( + val title: String, + @DrawableRes + val logo: Int +) \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/MainActivity.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/MainActivity.kt deleted file mode 100644 index 87784e48..00000000 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/MainActivity.kt +++ /dev/null @@ -1,18 +0,0 @@ -package io.getstream.slack.compose.ui - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import io.getstream.chat.android.compose.ui.theme.ChatTheme -import io.getstream.slack.compose.ui.home.HomeScreen - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - ChatTheme { - HomeScreen() - } - } - } -} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt new file mode 100644 index 00000000..f1d05d80 --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt @@ -0,0 +1,173 @@ +package io.getstream.slack.compose.ui.channels + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +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.width +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.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.getstream.chat.android.client.models.Channel +import io.getstream.chat.android.compose.ui.common.avatar.UserAvatar +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.ui.util.getDisplayName +import io.getstream.slack.compose.R +import io.getstream.slack.compose.ui.channels.components.OnlineIndicator +import io.getstream.slack.compose.ui.channels.components.UnreadCount +import io.getstream.slack.compose.ui.util.getOtherUser + +/** + * Component that represents a group channel item. + * + * @param channel The channel to display. + * @param onChannelClick Handler for a single tap on an item. + * @param modifier Modifier for styling. + * */ +@Composable +fun GroupChannelItem( + channel: Channel, + onChannelClick: (Channel) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .clickable { onChannelClick(channel) } + .padding(vertical = 8.dp) + .fillMaxWidth() + .height(24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(16.dp)) + + Icon( + modifier = Modifier + .size(12.dp), + painter = painterResource(id = R.drawable.ic_channel), + contentDescription = null + ) + + Spacer(Modifier.width(16.dp)) + + ChannelName(channel) + } +} + + +/** + * Component that represents a distinct channel item. + * + * A distinct channel is a channel created without ID based on members. + * + * @param channel The channel to display. + * @param onChannelClick Handler for a single tap on an item. + * @param modifier Modifier for styling. + */ +@Composable +fun DirectMessagingChannelItem( + channel: Channel, + onChannelClick: (Channel) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .clickable { onChannelClick(channel) } + .padding(vertical = 8.dp) + .fillMaxWidth() + .height(24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier + .padding(horizontal = 16.dp), + text = "" + (channel.memberCount - 1), + style = if (channel.hasUnread) ChatTheme.typography.bodyBold else ChatTheme.typography.body, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = ChatTheme.colors.textHighEmphasis, + ) + + ChannelName(channel) + } +} + +/** + * Component that represents a one-to-one channel item. + * + * One-to-one cha + * + * / If the other user is online, then show the green presence indicator next to his name + * + * @param channel The channel to display. + * @param onChannelClick Handler for a single tap on an item. + * @param modifier Modifier for styling. + */ +@Composable +fun OneToOneChannelItem( + channel: Channel, + onChannelClick: (Channel) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .clickable { onChannelClick(channel) } + .padding(vertical = 8.dp) + .fillMaxWidth() + .height(24.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + + val user = channel.getOtherUser()!! + + Box( + modifier = Modifier + .clip(ChatTheme.shapes.avatar) + .size(24.dp), + ) { + UserAvatar( + modifier = Modifier + .width(56.dp) + .height(56.dp), + user = user + ) + OnlineIndicator( + modifier = Modifier + .align(Alignment.BottomEnd) + .size(12.dp), + isOnline = user.online + ) + } + + ChannelName(channel) + + UnreadCount() + } +} + +@Composable +fun ChannelName(channel: Channel) { + val textStyle = if (channel.hasUnread) { + ChatTheme.typography.title3Bold + } else { + ChatTheme.typography.body + } + Text( + text = channel.getDisplayName(), + style = textStyle, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = ChatTheme.colors.textHighEmphasis, + ) +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelListScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelListScreen.kt deleted file mode 100644 index be0efed7..00000000 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelListScreen.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.getstream.slack.compose.ui.channels - -import androidx.compose.runtime.Composable - -@Composable -fun ChannelListScreen() { -} \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt new file mode 100644 index 00000000..6b766388 --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt @@ -0,0 +1,55 @@ +package io.getstream.slack.compose.ui.channels + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.client.api.models.QuerySort +import io.getstream.chat.android.client.models.Channel +import io.getstream.chat.android.client.models.Filters +import io.getstream.chat.android.compose.viewmodel.channel.ChannelListViewModel +import io.getstream.chat.android.compose.viewmodel.channel.ChannelViewModelFactory +import io.getstream.chat.android.offline.ChatDomain +import io.getstream.slack.compose.R +import io.getstream.slack.compose.model.Workspace +import io.getstream.slack.compose.ui.messages.MessagesActivity +import io.getstream.slack.compose.ui.theme.SlackTheme + +class ChannelsActivity : ComponentActivity() { + + private val factory by lazy { + ChannelViewModelFactory( + ChatClient.instance(), ChatDomain.instance(), + QuerySort + .desc("unread_count") + .desc("last_updated"), + Filters.and( + Filters.eq("type", "messaging"), + Filters.`in`("members", listOf(ChatClient.instance().getCurrentUser()?.id ?: "")) + ), + ) + } + + private val listViewModel: ChannelListViewModel by viewModels { factory } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + SlackTheme { + ChannelsScreen( + listViewModel = listViewModel, + workspace = Workspace( + title = "getstream", + logo = R.drawable.ic_channel + ), + onItemClick = ::openMessages + ) + } + } + } + + private fun openMessages(channel: Channel) { + startActivity(MessagesActivity.getIntent(this, channel.cid)) + } +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt new file mode 100644 index 00000000..b6f85665 --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt @@ -0,0 +1,107 @@ +package io.getstream.slack.compose.ui.channels + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.getstream.chat.android.client.models.Channel +import io.getstream.chat.android.compose.ui.channel.header.ChannelListHeader +import io.getstream.chat.android.compose.ui.channel.list.ChannelList +import io.getstream.chat.android.compose.ui.common.SearchInput +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.viewmodel.channel.ChannelListViewModel +import io.getstream.slack.compose.R +import io.getstream.slack.compose.model.Workspace +import io.getstream.slack.compose.ui.util.isDirectMessaging +import io.getstream.slack.compose.ui.util.isOneToOne + +@Composable +fun ChannelsScreen( + listViewModel: ChannelListViewModel, + workspace: Workspace, + onItemClick: (Channel) -> Unit = {}, +) { + val currentUser by listViewModel.user.collectAsState() + val isNetworkAvailable by listViewModel.isOnline.collectAsState() + + var searchQuery by rememberSaveable { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.appBackground) + ) { + + ChannelListHeader( + currentUser = currentUser, + title = workspace.title, + isNetworkAvailable = isNetworkAvailable, + trailingContent = { Spacer(Modifier.width(36.dp)) }, + leadingContent = { + Image( + painter = painterResource(id = R.drawable.ic_channel), + contentDescription = null, + modifier = Modifier.clip(RoundedCornerShape(10.dp)), + ) + } + ) + + SearchInput( + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 8.dp) + .fillMaxWidth(), + query = searchQuery, + onValueChange = { + searchQuery = it + listViewModel.setSearchQuery(it) + }, + leadingIcon = { Spacer(Modifier.width(8.dp)) }, + label = { + Text( + text = stringResource(id = R.string.search_input_hint), + style = ChatTheme.typography.body, + color = ChatTheme.colors.textLowEmphasis, + ) + } + ) + + ChannelList( + modifier = Modifier.fillMaxSize(), + viewModel = listViewModel, + onChannelClick = onItemClick, + itemContent = { + when { + it.isOneToOne() -> OneToOneChannelItem( + channel = it, + onChannelClick = onItemClick + ) + it.isDirectMessaging() -> DirectMessagingChannelItem( + channel = it, + onChannelClick = onItemClick + ) + else -> GroupChannelItem( + channel = it, + onChannelClick = onItemClick + ) + } + } + ) + } +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt new file mode 100644 index 00000000..4d7bb7e0 --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt @@ -0,0 +1,33 @@ +package io.getstream.slack.compose.ui.channels.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp + +/** + * A simple view component representation for User online status green indicator. + * + * @param isOnline - boolean toggle to update to either a green or grey dot. + * @param modifier - Modifier for styling. + * @param shape - The shape of the online indicator. + */ +@Composable +fun OnlineIndicator( + isOnline: Boolean, + modifier: Modifier = Modifier, + shape: Shape = CircleShape, +) { + Box( + modifier = modifier + .clip(shape) + .background(if (isOnline) Color.Green else Color.Transparent) + .border(1.dp, if (isOnline) Color.Transparent else Color.Gray) + ) +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCount.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCount.kt new file mode 100644 index 00000000..bb052d6f --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCount.kt @@ -0,0 +1,44 @@ +package io.getstream.slack.compose.ui.channels.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +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.Shape +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.getstream.chat.android.compose.ui.theme.ChatTheme + +@Composable +fun UnreadCount( + unreadCount: String = "1", + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(16.dp) +) { + Box( + modifier = modifier + .clip(shape) + .sizeIn(minWidth = 16.dp) + .background(Color.Red) + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = unreadCount, + style = ChatTheme.typography.footnote, + color = Color.White + ) + } +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/home/HomeScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/home/HomeScreen.kt deleted file mode 100644 index 0c186cba..00000000 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/home/HomeScreen.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.getstream.slack.compose.ui.home - -import androidx.compose.material.Text -import androidx.compose.runtime.Composable - -@Composable -fun HomeScreen() { - Text(text = "Hello world!") -} \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessageListScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessageListScreen.kt deleted file mode 100644 index a8134092..00000000 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessageListScreen.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.getstream.slack.compose.ui.messages - -import androidx.compose.runtime.Composable - -@Composable -fun MessageListScreen() { -} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt index b2ce7663..1c2a2223 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt @@ -1,6 +1,8 @@ package io.getstream.slack.compose.ui.messages import android.content.ClipboardManager +import android.content.Context +import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.viewModels @@ -98,7 +100,9 @@ class MessagesActivity : AppCompatActivity() { } MessageListHeader( - modifier = Modifier.fillMaxWidth().height(56.dp), + modifier = Modifier + .fillMaxWidth() + .height(56.dp), channel = listViewModel.channel, currentUser = user, isNetworkAvailable = isNetworkAvailable, @@ -107,4 +111,14 @@ class MessagesActivity : AppCompatActivity() { onBackPressed = backAction ) } + + companion object { + private const val KEY_CHANNEL_ID = "channelId" + + fun getIntent(context: Context, channelId: String): Intent { + return Intent(context, MessagesActivity::class.java).apply { + putExtra(KEY_CHANNEL_ID, channelId) + } + } + } } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt new file mode 100644 index 00000000..c7f37af2 --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt @@ -0,0 +1,38 @@ +package io.getstream.slack.compose.ui.theme + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.colorResource +import io.getstream.chat.android.compose.ui.theme.StreamColors +import io.getstream.slack.compose.R + +@Composable +fun slackLightColors(): StreamColors = StreamColors( + textHighEmphasis = colorResource(R.color.stream_compose_text_high_emphasis), + textLowEmphasis = colorResource(R.color.stream_compose_text_low_emphasis), + disabled = colorResource(R.color.stream_compose_disabled), + borders = colorResource(R.color.stream_compose_borders), + inputBackground = colorResource(R.color.stream_compose_input_background), + appBackground = colorResource(R.color.stream_compose_app_background), + barsBackground = colorResource(R.color.stream_gray_dark), // colorResource(R.color.stream_compose_bars_background), + linkBackground = colorResource(R.color.stream_compose_link_background), + overlay = colorResource(R.color.stream_compose_overlay), + primaryAccent = colorResource(id = R.color.stream_compose_primary_accent), + errorAccent = colorResource(R.color.stream_compose_error_accent), + infoAccent = colorResource(R.color.stream_compose_info_accent), +) + +@Composable +fun slackDarkColors(): StreamColors = StreamColors( + textHighEmphasis = colorResource(R.color.stream_compose_text_high_emphasis_dark), + textLowEmphasis = colorResource(R.color.stream_compose_text_low_emphasis_dark), + disabled = colorResource(R.color.stream_compose_disabled_dark), + borders = colorResource(R.color.stream_compose_borders), + inputBackground = colorResource(R.color.stream_compose_input_background_dark), + appBackground = colorResource(R.color.stream_compose_app_background_dark), + barsBackground = colorResource(R.color.stream_compose_bars_background_dark), + linkBackground = colorResource(R.color.stream_compose_link_background_dark), + overlay = colorResource(R.color.stream_compose_overlay_dark), + primaryAccent = colorResource(id = R.color.stream_compose_primary_accent_dark), + errorAccent = colorResource(R.color.stream_compose_error_accent_dark), + infoAccent = colorResource(R.color.stream_compose_info_accent_dark), +) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt new file mode 100644 index 00000000..4b758dfd --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt @@ -0,0 +1,23 @@ +package io.getstream.slack.compose.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import io.getstream.chat.android.compose.ui.theme.StreamShapes + +@Composable +fun slackShapes(): StreamShapes { + return StreamShapes( + avatar = RoundedCornerShape(8.dp), + myMessageBubble = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 16.dp), + otherMessageBubble = RoundedCornerShape( + topStart = 16.dp, + topEnd = 16.dp, + bottomEnd = 16.dp + ), + inputField = RoundedCornerShape(0.dp), + attachment = RoundedCornerShape(8.dp), + imageThumbnail = RoundedCornerShape(8.dp), + bottomSheet = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + ) +} \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTheme.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTheme.kt new file mode 100644 index 00000000..5919305b --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTheme.kt @@ -0,0 +1,22 @@ +package io.getstream.slack.compose.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import io.getstream.chat.android.compose.ui.theme.ChatTheme + +/** + * Customized [ChatTheme] for a visual parity with Slack. + * + * The custom theme overrides the default colors, typography and shapes + * used by the SDK. + */ +@Composable +fun SlackTheme(content: @Composable () -> Unit) { + ChatTheme( + colors = if (isSystemInDarkTheme()) slackDarkColors() else slackLightColors(), + typography = slackTypography, + shapes = slackShapes() + ) { + content() + } +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTypography.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTypography.kt new file mode 100644 index 00000000..4c5828b8 --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTypography.kt @@ -0,0 +1,54 @@ +package io.getstream.slack.compose.ui.theme + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import io.getstream.chat.android.compose.ui.theme.StreamTypography +import io.getstream.slack.compose.R + +private val light = Font(R.font.lato_light, FontWeight.W300) +private val regular = Font(R.font.lato_regular, FontWeight.W400) +private val medium = Font(R.font.lato_regular, FontWeight.W500) +private val bold = Font(R.font.lato_bold, FontWeight.W700) + +private val slackFontFamily = FontFamily(fonts = listOf(light, regular, medium, bold)) + +/** + * The default [StreamTypography] from the SDK with a custom font family + * used by Slack ([Lato](https://fonts.google.com/specimen/Lato)). + */ +val slackTypography: StreamTypography = StreamTypography + .defaultTypography() + .withFontFamily(slackFontFamily) + +/** + * Returns a copy of [StreamTypography] with a custom font family applied to + * every text style.. + * + * @param fontFamily The font family to be used with every text style. + */ +private fun StreamTypography.withFontFamily(fontFamily: FontFamily): StreamTypography { + return copy( + title1 = title1.withFontFamily(fontFamily), + title3 = title3.withFontFamily(fontFamily), + title3Bold = title3Bold.withFontFamily(fontFamily), + body = body.withFontFamily(fontFamily), + bodyItalic = bodyItalic.withFontFamily(fontFamily), + bodyBold = bodyBold.withFontFamily(fontFamily), + footnote = footnote.withFontFamily(fontFamily), + footnoteItalic = footnoteItalic.withFontFamily(fontFamily), + footnoteBold = footnoteBold.withFontFamily(fontFamily), + captionBold = captionBold.withFontFamily(fontFamily), + tabBar = tabBar.withFontFamily(fontFamily) + ) +} + +/** + * Returns a copy of [TextStyle] with a custom font family. + * + * @param fontFamily The font family to be used with this text style. + */ +private fun TextStyle.withFontFamily(fontFamily: FontFamily): TextStyle { + return copy(fontFamily = fontFamily) +} \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt new file mode 100644 index 00000000..e9b0d973 --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt @@ -0,0 +1,38 @@ +package io.getstream.slack.compose.ui.util + +import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.client.models.Channel +import io.getstream.chat.android.client.models.User + +/** + * Checks if the channel is distinct. + * + * A distinct channel is a channel created without ID based on members. Internally + * the server creates a CID which starts with "!members" prefix and is unique for + * this particular group of users. + */ +fun Channel.isDirectMessaging(): Boolean = cid.contains("!members") + +/** + * Checks if the channel is a direct conversation between the current user and some + * other user. + */ +fun Channel.isOneToOne(): Boolean { + return isDirectMessaging() && members.size == 2 && includesCurrentUser() +} + +/** + * Checks if the current user is among the members of this channel. + */ +private fun Channel.includesCurrentUser(): Boolean { + val currentUserId = ChatClient.instance().getCurrentUser()?.id ?: return false + return members.any { it.user.id == currentUserId } +} + +/** + * Returns the first user in this channel apart from the current user. + */ +fun Channel.getOtherUser(): User? { + val currentUserId = ChatClient.instance().getCurrentUser()?.id + return members.find { it.user.id != currentUserId }?.user +} \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/res/drawable/ic_channel.xml b/slack-clone-compose-sample/src/main/res/drawable/ic_channel.xml new file mode 100644 index 00000000..05b14fa6 --- /dev/null +++ b/slack-clone-compose-sample/src/main/res/drawable/ic_channel.xml @@ -0,0 +1,4 @@ + + + diff --git a/slack-clone-compose-sample/src/main/res/font/lato_bold.ttf b/slack-clone-compose-sample/src/main/res/font/lato_bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b63a14d6aeec11bce432820914e73db28e489634 GIT binary patch literal 73316 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 M$8RP7dXDOU1KQ%3=>Px# literal 0 HcmV?d00001 diff --git a/slack-clone-compose-sample/src/main/res/font/lato_light.ttf b/slack-clone-compose-sample/src/main/res/font/lato_light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9c0a7053621b248d8b6589dcaa0854ef49c33985 GIT binary patch literal 77192 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 z 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 U$W?y#kn_3J31@K6Y>Y|#e~5^r5YS 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 ojk(g9PAtLVP?*9aY%s*7 Slack Clone Compose Sample + + + Jump to… + + + \ No newline at end of file From 2e9a3c7d141dc32b4fa35646ccaacf13f9daf527 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 01:28:32 +0300 Subject: [PATCH 04/24] [2127] Update query sort --- .../compose/ui/channels/ChannelsActivity.kt | 25 +++++++++++++------ .../slack/compose/ui/util/ChannelUtils.kt | 2 +- .../slack/compose/ui/util/ClientUtils.kt | 10 ++++++++ 3 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ClientUtils.kt diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt index 6b766388..e5ff4527 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt @@ -15,18 +15,20 @@ import io.getstream.slack.compose.R import io.getstream.slack.compose.model.Workspace import io.getstream.slack.compose.ui.messages.MessagesActivity import io.getstream.slack.compose.ui.theme.SlackTheme +import io.getstream.slack.compose.ui.util.currentUserId class ChannelsActivity : ComponentActivity() { private val factory by lazy { ChannelViewModelFactory( - ChatClient.instance(), ChatDomain.instance(), + ChatClient.instance(), + ChatDomain.instance(), QuerySort - .desc("unread_count") - .desc("last_updated"), + .desc("has_unread") + .desc("last_message_at"), Filters.and( Filters.eq("type", "messaging"), - Filters.`in`("members", listOf(ChatClient.instance().getCurrentUser()?.id ?: "")) + Filters.`in`("members", listOf(currentUserId())) ), ) } @@ -39,10 +41,7 @@ class ChannelsActivity : ComponentActivity() { SlackTheme { ChannelsScreen( listViewModel = listViewModel, - workspace = Workspace( - title = "getstream", - logo = R.drawable.ic_channel - ), + workspace = streamWorkspace, onItemClick = ::openMessages ) } @@ -52,4 +51,14 @@ class ChannelsActivity : ComponentActivity() { private fun openMessages(channel: Channel) { startActivity(MessagesActivity.getIntent(this, channel.cid)) } + + companion object { + /** + * For the sake of example we hardcoded the "Stream" workspace. + */ + private val streamWorkspace = Workspace( + title = "getstream", + logo = R.drawable.ic_channel + ) + } } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt index e9b0d973..ca559c93 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt @@ -33,6 +33,6 @@ private fun Channel.includesCurrentUser(): Boolean { * Returns the first user in this channel apart from the current user. */ fun Channel.getOtherUser(): User? { - val currentUserId = ChatClient.instance().getCurrentUser()?.id + val currentUserId = currentUserId() return members.find { it.user.id != currentUserId }?.user } \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ClientUtils.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ClientUtils.kt new file mode 100644 index 00000000..33f6ae1a --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ClientUtils.kt @@ -0,0 +1,10 @@ +package io.getstream.slack.compose.ui.util + +import io.getstream.chat.android.client.ChatClient + +/** + * Returns the ID of the current user or an empty string as a fallback. + */ +fun currentUserId(): String { + return ChatClient.instance().getCurrentUser()?.id ?: "" +} \ No newline at end of file From 3abe896cb78bb953772d6cd3933baad881000397 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 01:58:13 +0300 Subject: [PATCH 05/24] [2127] Update workspace logo --- .../compose/ui/channels/ChannelsActivity.kt | 2 +- .../slack/compose/ui/channels/ChannelsScreen.kt | 7 +++++-- .../src/main/res/drawable/ic_stream_logo.png | Bin 0 -> 1631 bytes 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 slack-clone-compose-sample/src/main/res/drawable/ic_stream_logo.png diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt index e5ff4527..2e619685 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt @@ -58,7 +58,7 @@ class ChannelsActivity : ComponentActivity() { */ private val streamWorkspace = Workspace( title = "getstream", - logo = R.drawable.ic_channel + logo = R.drawable.ic_stream_logo ) } } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt index b6f85665..358e3336 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text @@ -56,9 +57,11 @@ fun ChannelsScreen( trailingContent = { Spacer(Modifier.width(36.dp)) }, leadingContent = { Image( - painter = painterResource(id = R.drawable.ic_channel), + painter = painterResource(id = workspace.logo), contentDescription = null, - modifier = Modifier.clip(RoundedCornerShape(10.dp)), + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(8.dp)), ) } ) diff --git a/slack-clone-compose-sample/src/main/res/drawable/ic_stream_logo.png b/slack-clone-compose-sample/src/main/res/drawable/ic_stream_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..856969b950830cbeb3eb1d819738ad3923d1c0dd GIT binary patch literal 1631 zcmb_d`!~}I0RPff-t8WXO`cQ9QQ4ZZA$jIGF;j*cwrX`lYGTpSZL-O;^2jJXa4g~u z$-;(PjMnHOkG!AlE`&TAyZRqG=kqw9&+nh}N%Hn`QCHDX0RTYV&D9CN=^?+Uv~^QQ z@5f*^O*zt)NCf~@&0kai*!i%{Qh|zhaRAE2x{}QRqS)i?0f3vXDhmYz0QiTSlRbf^ zFr9xiOxGF8q^uenu|rJ3eriu1+b0m9(88pz$o)87Q?Pf2p^u%Tm;iOfe7b{FSMmvY zc^)uTPyJi0w+qL1bz@hz(fe!$`ujyQ;%_qzffD+Mwxz+rZ=#>?;uaRrEH(&pHe;O zyvGD+DVQ$PcgeV59dX&y9T;6NYZ6v9qKg|(gD`O|ymgJxL~kcow#q%oaMnlh`Vxm~ z-cXBKYE7cbhf3q+bD!r|^OHk-L&m_3D}s0*ITR_|He|jr*x;jzFoYsj3VI2L0{}HR z+SYW6;qrD0!J>g!k^Js3#LoS8?JNApV36`q-c6-mMMxlVd<9#aERVc;D2-WVvujeX z8a1h}u=9KKp54jupS>_yj4cex@P>4$;YS=UaonoTK568GjaW`?eStv8))z-%;g_z> zBG<9Gp0d|u+5@(rX0l>h7&oudE0FY4j`G9=th{7>=;gm1Yu>t#TE-Y7XPJ@>LS5-> zXjHv$>U%9eQ4|1+wW$^?x-BnM+1!9*qL^R$Jm7*F7L>Y#N3si|QRF#|t!PEzK#Q*N@r3xEd2BNZG6Z+*(V){8sVAMdK-7)26?FIU zCR#!6!UoToA}OIk1Y}v{3I3vAjHUPwyEUkaS3E?4oIJAL>lKW3ao6J#bjPL=^&kJY zP;aQF8*v$y5}c!;j0+DoAjuB!_!(w^Aq_Tj&AK z0Fe!$B4N7ufWIDWc(ABy@3!(wkOYU6;|2a^_urMNhZ?|rYbzc-ux(*Jo7Jo$asB#V zCHb=yiDo3kt#O*2FUwwbxWl@({#z6Ff}ds=Bz>P$%H~4eG^V9V591kOns$}2KCL@> z4xTD~J+qL%JW3K{&jy7Z5vEFTaHWTrqU!UxuR)L0ZIj>QiJsy_RCZYAjF?Ptw5>Mx z#O3l>-j$ALhevmCAQP&cmahwyq``E&7{*ZP%w}gJjBYPF^ex5o!A^c4rW`;wTx2Qv ziu)~is$HGw&rfUJRbI{T8`t8HLc$VQUCU=`j8`CsI6auP;(XM@D-;Ua($FMEs*_$#np~!&JsZsnrL)u&>M2{l?#pW;yv^!Vdd`vD&gF@Uh{$Is{r&(p zyQMFwi=>3zmoPpN#EAB&EP;xy3BDdCw8Nk3NZWW^J&LyWUX&&B}!(|#3OOJ7+n`?7^zcDz1 zc&by`-kbFv%ql7to$Ey^@7`I|PYUv7jk2%z8yHWd_>zNVxc0jl=0GmhPA9NNqWm zUyfVGasu7<>SteTy6f?xytUicgLm+bbq>*_q_3RuVgTOia3Vm%-(+`i*>Y&OqA=zk z$001rf;{ol^t0AYkQT1;&U5v;;CppO8x81F`~uRnBJ6FCT-l7QxcB#B0c17N0zXrK SyJ+(d05@kZr*emooBspY-|5f* literal 0 HcmV?d00001 From d7825aff8b842deaefb47c3bde939dac2b54170b Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 02:08:32 +0300 Subject: [PATCH 06/24] [2127] Resize message list screen when keyboard is shown --- slack-clone-compose-sample/src/main/AndroidManifest.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/slack-clone-compose-sample/src/main/AndroidManifest.xml b/slack-clone-compose-sample/src/main/AndroidManifest.xml index a6c3f967..b2d58f3d 100644 --- a/slack-clone-compose-sample/src/main/AndroidManifest.xml +++ b/slack-clone-compose-sample/src/main/AndroidManifest.xml @@ -21,7 +21,8 @@ + android:exported="true" + android:windowSoftInputMode="adjustResize" /> \ No newline at end of file From 20a29b981d5678ab493c293f3128f82aa41d01b7 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 03:25:20 +0300 Subject: [PATCH 07/24] [2127] Improve unread badge --- .../slack/compose/ui/channels/ChannelItem.kt | 7 ++- .../ui/channels/components/UnreadCount.kt | 44 --------------- .../channels/components/UnreadCountBadge.kt | 55 +++++++++++++++++++ 3 files changed, 60 insertions(+), 46 deletions(-) delete mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCount.kt create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt index f1d05d80..59978701 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt @@ -25,7 +25,7 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.getDisplayName import io.getstream.slack.compose.R import io.getstream.slack.compose.ui.channels.components.OnlineIndicator -import io.getstream.slack.compose.ui.channels.components.UnreadCount +import io.getstream.slack.compose.ui.channels.components.UnreadCountBadge import io.getstream.slack.compose.ui.util.getOtherUser /** @@ -152,7 +152,10 @@ fun OneToOneChannelItem( ChannelName(channel) - UnreadCount() + val unreadCount = channel.unreadCount ?: 0 + if (unreadCount > 0) { + UnreadCountBadge(unreadCount) + } } } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCount.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCount.kt deleted file mode 100644 index bb052d6f..00000000 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCount.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.getstream.slack.compose.ui.channels.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.RoundedCornerShape -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.Shape -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import io.getstream.chat.android.compose.ui.theme.ChatTheme - -@Composable -fun UnreadCount( - unreadCount: String = "1", - modifier: Modifier = Modifier, - shape: Shape = RoundedCornerShape(16.dp) -) { - Box( - modifier = modifier - .clip(shape) - .sizeIn(minWidth = 16.dp) - .background(Color.Red) - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = unreadCount, - style = ChatTheme.typography.footnote, - color = Color.White - ) - } -} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt new file mode 100644 index 00000000..7ea09dd6 --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt @@ -0,0 +1,55 @@ +package io.getstream.slack.compose.ui.channels.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +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.text.TextStyle +import androidx.compose.ui.unit.dp +import io.getstream.chat.android.compose.ui.theme.ChatTheme + +/** + * Component that represents a badge with the number of unread messages. + * + * @param unreadCount The number of unread messages. + * @param modifier Modifier for styling. + * @param badgeColor Color of the badge. + * @param textColor Color of the unread count. + * @param textStyle Shape of the badge. + */ +@Composable +fun UnreadCountBadge( + unreadCount: Int, + modifier: Modifier = Modifier, + badgeColor: Color = ChatTheme.colors.errorAccent, + textColor: Color = Color.White, + textStyle: TextStyle = ChatTheme.typography.bodyBold, +) { + val unreadCountText = if (unreadCount > 99) "99+" else unreadCount.toString() + + Box( + modifier = modifier + .widthIn(min = 28.dp) + .heightIn(min = 28.dp) + .background( + shape = RoundedCornerShape(16.dp), + color = badgeColor, + ) + ) { + Text( + modifier = Modifier + .align(Alignment.Center) + .padding(2.dp), + text = unreadCountText, + style = textStyle, + color = textColor, + ) + } +} From 86a0ee76a24ba0179115e881e8dba3264f955940 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 05:52:09 +0300 Subject: [PATCH 08/24] [2127] Improve channel items --- .../slack/compose/ui/channels/ChannelItem.kt | 100 +++++++++++++----- .../compose/ui/channels/ChannelsScreen.kt | 44 +++++--- .../ui/channels/components/OnlineIndicator.kt | 23 +++- .../channels/components/UnreadCountBadge.kt | 24 ++++- .../slack/compose/ui/theme/SlackShapes.kt | 2 +- 5 files changed, 142 insertions(+), 51 deletions(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt index 59978701..934d56b8 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt @@ -1,14 +1,18 @@ package io.getstream.slack.compose.ui.channels +import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -16,7 +20,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.getstream.chat.android.client.models.Channel @@ -26,6 +32,7 @@ import io.getstream.chat.android.compose.ui.util.getDisplayName import io.getstream.slack.compose.R import io.getstream.slack.compose.ui.channels.components.OnlineIndicator import io.getstream.slack.compose.ui.channels.components.UnreadCountBadge +import io.getstream.slack.compose.ui.theme.SlackTheme import io.getstream.slack.compose.ui.util.getOtherUser /** @@ -55,8 +62,14 @@ fun GroupChannelItem( modifier = Modifier .size(12.dp), painter = painterResource(id = R.drawable.ic_channel), - contentDescription = null - ) + contentDescription = null, + tint = if (channel.hasUnread) { + ChatTheme.colors.textHighEmphasis + } else { + ChatTheme.colors.textLowEmphasis + }, + + ) Spacer(Modifier.width(16.dp)) @@ -64,6 +77,28 @@ fun GroupChannelItem( } } +@Preview(showBackground = true) +@Composable +fun GroupChannelItemPreview() { + SlackTheme { + Column( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.appBackground) + ) { + DirectMessagingChannelItem( + Channel( + watcherCount = 3, + extraData = mutableMapOf( + "name" to "test test", + "image" to "", + ) + ), + {}, + ) + } + } +} /** * Component that represents a distinct channel item. @@ -88,17 +123,25 @@ fun DirectMessagingChannelItem( .height(24.dp), verticalAlignment = Alignment.CenterVertically, ) { - Text( - modifier = Modifier - .padding(horizontal = 16.dp), - text = "" + (channel.memberCount - 1), - style = if (channel.hasUnread) ChatTheme.typography.bodyBold else ChatTheme.typography.body, - fontSize = 12.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = ChatTheme.colors.textHighEmphasis, - ) - + Spacer(Modifier.width(10.dp)) + Box( + modifier = modifier + .clip(RoundedCornerShape(4.dp)) + .size(24.dp) + .background(ChatTheme.colors.borders), + ) { + Text( + modifier = Modifier + .align(Alignment.Center), + text = "" + (channel.memberCount - 1), + style = ChatTheme.typography.captionBold, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = ChatTheme.colors.textHighEmphasis, + ) + } + Spacer(Modifier.width(10.dp)) ChannelName(channel) } } @@ -106,10 +149,6 @@ fun DirectMessagingChannelItem( /** * Component that represents a one-to-one channel item. * - * One-to-one cha - * - * / If the other user is online, then show the green presence indicator next to his name - * * @param channel The channel to display. * @param onChannelClick Handler for a single tap on an item. * @param modifier Modifier for styling. @@ -123,23 +162,23 @@ fun OneToOneChannelItem( Row( modifier = modifier .clickable { onChannelClick(channel) } - .padding(vertical = 8.dp) + .padding(vertical = 2.dp, horizontal = 16.dp) .fillMaxWidth() - .height(24.dp), + .height(40.dp), verticalAlignment = Alignment.CenterVertically, ) { - val user = channel.getOtherUser()!! Box( modifier = Modifier .clip(ChatTheme.shapes.avatar) - .size(24.dp), + .size(36.dp), ) { UserAvatar( modifier = Modifier - .width(56.dp) - .height(56.dp), + .width(32.dp) + .height(32.dp) + .align(Alignment.Center), user = user ) OnlineIndicator( @@ -150,6 +189,8 @@ fun OneToOneChannelItem( ) } + Spacer(Modifier.width(10.dp)) + ChannelName(channel) val unreadCount = channel.unreadCount ?: 0 @@ -159,18 +200,19 @@ fun OneToOneChannelItem( } } +/** + * Component that represents a channel name. + * + * @param channel The channel used to display the name. + */ @Composable fun ChannelName(channel: Channel) { - val textStyle = if (channel.hasUnread) { - ChatTheme.typography.title3Bold - } else { - ChatTheme.typography.body - } Text( text = channel.getDisplayName(), - style = textStyle, + style = ChatTheme.typography.body, maxLines = 1, overflow = TextOverflow.Ellipsis, color = ChatTheme.colors.textHighEmphasis, + fontWeight = if (channel.hasUnread) FontWeight.Bold else FontWeight.Normal ) } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt index 358e3336..03bf5d9b 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt @@ -1,8 +1,10 @@ package io.getstream.slack.compose.ui.channels +import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background 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 @@ -55,15 +57,7 @@ fun ChannelsScreen( title = workspace.title, isNetworkAvailable = isNetworkAvailable, trailingContent = { Spacer(Modifier.width(36.dp)) }, - leadingContent = { - Image( - painter = painterResource(id = workspace.logo), - contentDescription = null, - modifier = Modifier - .size(36.dp) - .clip(RoundedCornerShape(8.dp)), - ) - } + leadingContent = { WorkspaceLogo(logo = workspace.logo) } ) SearchInput( @@ -76,13 +70,7 @@ fun ChannelsScreen( listViewModel.setSearchQuery(it) }, leadingIcon = { Spacer(Modifier.width(8.dp)) }, - label = { - Text( - text = stringResource(id = R.string.search_input_hint), - style = ChatTheme.typography.body, - color = ChatTheme.colors.textLowEmphasis, - ) - } + label = { SearchLabel() } ) ChannelList( @@ -108,3 +96,27 @@ fun ChannelsScreen( ) } } + +@Composable +private fun WorkspaceLogo(@DrawableRes logo: Int) { + Row { + Spacer(Modifier.width(8.dp)) + + Image( + painter = painterResource(id = logo), + contentDescription = null, + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(8.dp)), + ) + } +} + +@Composable +private fun SearchLabel() { + Text( + text = stringResource(id = R.string.search_input_hint), + style = ChatTheme.typography.body, + color = ChatTheme.colors.textLowEmphasis, + ) +} \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt index 4d7bb7e0..be510713 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt @@ -9,7 +9,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.slack.compose.ui.theme.SlackTheme /** * A simple view component representation for User online status green indicator. @@ -27,7 +30,23 @@ fun OnlineIndicator( Box( modifier = modifier .clip(shape) - .background(if (isOnline) Color.Green else Color.Transparent) - .border(1.dp, if (isOnline) Color.Transparent else Color.Gray) + .background(if (isOnline) ChatTheme.colors.infoAccent else Color.Transparent) + .border(2.dp, if (isOnline) Color.Transparent else ChatTheme.colors.borders) ) } + +@Preview(showBackground = true) +@Composable +fun OnlineIndicatorPreviewOnline() { + SlackTheme { + OnlineIndicator(isOnline = true) + } +} + +@Preview(showBackground = true) +@Composable +fun OnlineIndicatorPreviewOffline() { + SlackTheme { + OnlineIndicator(isOnline = false) + } +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt index 7ea09dd6..f9f250f5 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt @@ -12,8 +12,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.slack.compose.ui.theme.SlackTheme /** * Component that represents a badge with the number of unread messages. @@ -36,10 +38,10 @@ fun UnreadCountBadge( Box( modifier = modifier - .widthIn(min = 28.dp) - .heightIn(min = 28.dp) + .widthIn(min = 24.dp) + .heightIn(min = 24.dp) .background( - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(12.dp), color = badgeColor, ) ) { @@ -53,3 +55,19 @@ fun UnreadCountBadge( ) } } + +@Preview(showBackground = true) +@Composable +fun UnreadCountBadgePreviewFew() { + SlackTheme { + UnreadCountBadge(unreadCount = 5) + } +} + +@Preview(showBackground = true) +@Composable +fun UnreadCountBadgePreviewMany() { + SlackTheme { + UnreadCountBadge(unreadCount = 150) + } +} \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt index 4b758dfd..b3f4c02b 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt @@ -8,7 +8,7 @@ import io.getstream.chat.android.compose.ui.theme.StreamShapes @Composable fun slackShapes(): StreamShapes { return StreamShapes( - avatar = RoundedCornerShape(8.dp), + avatar = RoundedCornerShape(4.dp), myMessageBubble = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 16.dp), otherMessageBubble = RoundedCornerShape( topStart = 16.dp, From 8eeeb5d786ea27e8605bde92bbbcf54e48a748a0 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 06:58:59 +0300 Subject: [PATCH 09/24] [2127] Improve channel items docs --- .../slack/compose/ui/channels/ChannelItem.kt | 121 +++++++----------- .../compose/ui/channels/ChannelsScreen.kt | 51 ++++++-- .../slack/compose/ui/util/ChannelUtils.kt | 22 ++-- .../src/main/res/values/strings.xml | 2 +- 4 files changed, 103 insertions(+), 93 deletions(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt index 934d56b8..dc0a5845 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt @@ -3,10 +3,8 @@ package io.getstream.slack.compose.ui.channels import androidx.compose.foundation.background import androidx.compose.foundation.clickable 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 @@ -22,7 +20,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.getstream.chat.android.client.models.Channel @@ -32,18 +29,19 @@ import io.getstream.chat.android.compose.ui.util.getDisplayName import io.getstream.slack.compose.R import io.getstream.slack.compose.ui.channels.components.OnlineIndicator import io.getstream.slack.compose.ui.channels.components.UnreadCountBadge -import io.getstream.slack.compose.ui.theme.SlackTheme import io.getstream.slack.compose.ui.util.getOtherUser /** - * Component that represents a group channel item. + * Component that represents a one-to-one channel item. + * + * One-to-one channel is a distinct channel with only two members. * * @param channel The channel to display. * @param onChannelClick Handler for a single tap on an item. * @param modifier Modifier for styling. - * */ + */ @Composable -fun GroupChannelItem( +fun DirectOneToOneChatItem( channel: Channel, onChannelClick: (Channel) -> Unit, modifier: Modifier = Modifier, @@ -51,51 +49,40 @@ fun GroupChannelItem( Row( modifier = modifier .clickable { onChannelClick(channel) } - .padding(vertical = 8.dp) + .padding(vertical = 2.dp, horizontal = 16.dp) .fillMaxWidth() - .height(24.dp), + .height(40.dp), verticalAlignment = Alignment.CenterVertically, ) { - Spacer(Modifier.width(16.dp)) + val user = channel.getOtherUser()!! - Icon( + Box( modifier = Modifier - .size(12.dp), - painter = painterResource(id = R.drawable.ic_channel), - contentDescription = null, - tint = if (channel.hasUnread) { - ChatTheme.colors.textHighEmphasis - } else { - ChatTheme.colors.textLowEmphasis - }, - + .clip(ChatTheme.shapes.avatar) + .size(36.dp), + ) { + UserAvatar( + modifier = Modifier + .width(32.dp) + .height(32.dp) + .align(Alignment.Center), + user = user ) + OnlineIndicator( + modifier = Modifier + .align(Alignment.BottomEnd) + .size(12.dp), + isOnline = user.online + ) + } - Spacer(Modifier.width(16.dp)) + Spacer(Modifier.width(10.dp)) ChannelName(channel) - } -} -@Preview(showBackground = true) -@Composable -fun GroupChannelItemPreview() { - SlackTheme { - Column( - modifier = Modifier - .fillMaxSize() - .background(ChatTheme.colors.appBackground) - ) { - DirectMessagingChannelItem( - Channel( - watcherCount = 3, - extraData = mutableMapOf( - "name" to "test test", - "image" to "", - ) - ), - {}, - ) + val unreadCount = channel.unreadCount ?: 0 + if (unreadCount > 0) { + UnreadCountBadge(unreadCount) } } } @@ -110,7 +97,7 @@ fun GroupChannelItemPreview() { * @param modifier Modifier for styling. */ @Composable -fun DirectMessagingChannelItem( +fun DirectGroupChatItem( channel: Channel, onChannelClick: (Channel) -> Unit, modifier: Modifier = Modifier, @@ -146,15 +133,16 @@ fun DirectMessagingChannelItem( } } + /** - * Component that represents a one-to-one channel item. + * Component that represents a regular named channel. * * @param channel The channel to display. * @param onChannelClick Handler for a single tap on an item. * @param modifier Modifier for styling. - */ + * */ @Composable -fun OneToOneChannelItem( +fun ChannelItem( channel: Channel, onChannelClick: (Channel) -> Unit, modifier: Modifier = Modifier, @@ -162,41 +150,28 @@ fun OneToOneChannelItem( Row( modifier = modifier .clickable { onChannelClick(channel) } - .padding(vertical = 2.dp, horizontal = 16.dp) + .padding(vertical = 8.dp) .fillMaxWidth() - .height(40.dp), + .height(24.dp), verticalAlignment = Alignment.CenterVertically, ) { - val user = channel.getOtherUser()!! + Spacer(Modifier.width(16.dp)) - Box( + Icon( modifier = Modifier - .clip(ChatTheme.shapes.avatar) - .size(36.dp), - ) { - UserAvatar( - modifier = Modifier - .width(32.dp) - .height(32.dp) - .align(Alignment.Center), - user = user - ) - OnlineIndicator( - modifier = Modifier - .align(Alignment.BottomEnd) - .size(12.dp), - isOnline = user.online - ) - } + .size(12.dp), + painter = painterResource(id = R.drawable.ic_channel), + contentDescription = null, + tint = if (channel.hasUnread) { + ChatTheme.colors.textHighEmphasis + } else { + ChatTheme.colors.textLowEmphasis + }, + ) - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(16.dp)) ChannelName(channel) - - val unreadCount = channel.unreadCount ?: 0 - if (unreadCount > 0) { - UnreadCountBadge(unreadCount) - } } } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt index 03bf5d9b..3e5bad58 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt @@ -3,11 +3,13 @@ package io.getstream.slack.compose.ui.channels import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background +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.layout.width @@ -19,10 +21,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import io.getstream.chat.android.client.models.Channel import io.getstream.chat.android.compose.ui.channel.header.ChannelListHeader @@ -32,8 +36,8 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.viewmodel.channel.ChannelListViewModel import io.getstream.slack.compose.R import io.getstream.slack.compose.model.Workspace -import io.getstream.slack.compose.ui.util.isDirectMessaging -import io.getstream.slack.compose.ui.util.isOneToOne +import io.getstream.slack.compose.ui.util.isDirectGroupChat +import io.getstream.slack.compose.ui.util.isDirectOneToOneChat @Composable fun ChannelsScreen( @@ -62,32 +66,34 @@ fun ChannelsScreen( SearchInput( modifier = Modifier - .padding(horizontal = 12.dp, vertical = 8.dp) + .padding(16.dp) + .height(36.dp) .fillMaxWidth(), query = searchQuery, onValueChange = { searchQuery = it listViewModel.setSearchQuery(it) }, - leadingIcon = { Spacer(Modifier.width(8.dp)) }, - label = { SearchLabel() } + leadingIcon = { Spacer(Modifier.width(16.dp)) }, + label = { SearchInputHint() } ) ChannelList( modifier = Modifier.fillMaxSize(), viewModel = listViewModel, onChannelClick = onItemClick, + emptyContent = { EmptyContent() }, itemContent = { when { - it.isOneToOne() -> OneToOneChannelItem( + it.isDirectOneToOneChat() -> DirectOneToOneChatItem( channel = it, onChannelClick = onItemClick ) - it.isDirectMessaging() -> DirectMessagingChannelItem( + it.isDirectGroupChat() -> DirectGroupChatItem( channel = it, onChannelClick = onItemClick ) - else -> GroupChannelItem( + else -> ChannelItem( channel = it, onChannelClick = onItemClick ) @@ -97,6 +103,9 @@ fun ChannelsScreen( } } +/** + * Component that represents the workspace switch shown in the toolbar. + */ @Composable private fun WorkspaceLogo(@DrawableRes logo: Int) { Row { @@ -112,11 +121,35 @@ private fun WorkspaceLogo(@DrawableRes logo: Int) { } } +/** + * Component that represents the label shown in the search component, when there's no input. + */ @Composable -private fun SearchLabel() { +private fun SearchInputHint() { Text( text = stringResource(id = R.string.search_input_hint), style = ChatTheme.typography.body, color = ChatTheme.colors.textLowEmphasis, ) +} + +/** + * Component that represents the empty content if there are no channels. + */ +@Composable +private fun EmptyContent() { + Box( + modifier = Modifier + .background(color = ChatTheme.colors.appBackground) + .fillMaxSize(), + ) { + Text( + modifier = Modifier + .align(Alignment.Center), + text = stringResource(id = R.string.search_no_results), + style = ChatTheme.typography.bodyBold, + color = ChatTheme.colors.textHighEmphasis, + textAlign = TextAlign.Center + ) + } } \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt index ca559c93..cb525262 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ChannelUtils.kt @@ -4,6 +4,16 @@ import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.models.Channel import io.getstream.chat.android.client.models.User +/** + * Checks if the channel is a direct conversation between the current user and some + * other user. + * + * A one-to-one chat is basically a corner case of a distinct channel with only 2 members. + */ +fun Channel.isDirectOneToOneChat(): Boolean { + return isDirectGroupChat() && members.size == 2 && includesCurrentUser() +} + /** * Checks if the channel is distinct. * @@ -11,15 +21,7 @@ import io.getstream.chat.android.client.models.User * the server creates a CID which starts with "!members" prefix and is unique for * this particular group of users. */ -fun Channel.isDirectMessaging(): Boolean = cid.contains("!members") - -/** - * Checks if the channel is a direct conversation between the current user and some - * other user. - */ -fun Channel.isOneToOne(): Boolean { - return isDirectMessaging() && members.size == 2 && includesCurrentUser() -} +fun Channel.isDirectGroupChat(): Boolean = cid.contains("!members") /** * Checks if the current user is among the members of this channel. @@ -35,4 +37,4 @@ private fun Channel.includesCurrentUser(): Boolean { fun Channel.getOtherUser(): User? { val currentUserId = currentUserId() return members.find { it.user.id != currentUserId }?.user -} \ No newline at end of file +} diff --git a/slack-clone-compose-sample/src/main/res/values/strings.xml b/slack-clone-compose-sample/src/main/res/values/strings.xml index ecd07a57..4acf2bef 100644 --- a/slack-clone-compose-sample/src/main/res/values/strings.xml +++ b/slack-clone-compose-sample/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ Jump to… + No results - \ No newline at end of file From c4a8a09bc91535d97a635f2c3636e723897bb4d8 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 08:18:56 +0300 Subject: [PATCH 10/24] [2127] Change toolbar color --- .../kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt index c7f37af2..8aff9a5a 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt @@ -13,7 +13,7 @@ fun slackLightColors(): StreamColors = StreamColors( borders = colorResource(R.color.stream_compose_borders), inputBackground = colorResource(R.color.stream_compose_input_background), appBackground = colorResource(R.color.stream_compose_app_background), - barsBackground = colorResource(R.color.stream_gray_dark), // colorResource(R.color.stream_compose_bars_background), + barsBackground = colorResource(R.color.slackish), linkBackground = colorResource(R.color.stream_compose_link_background), overlay = colorResource(R.color.stream_compose_overlay), primaryAccent = colorResource(id = R.color.stream_compose_primary_accent), @@ -29,7 +29,7 @@ fun slackDarkColors(): StreamColors = StreamColors( borders = colorResource(R.color.stream_compose_borders), inputBackground = colorResource(R.color.stream_compose_input_background_dark), appBackground = colorResource(R.color.stream_compose_app_background_dark), - barsBackground = colorResource(R.color.stream_compose_bars_background_dark), + barsBackground = colorResource(R.color.slackish), linkBackground = colorResource(R.color.stream_compose_link_background_dark), overlay = colorResource(R.color.stream_compose_overlay_dark), primaryAccent = colorResource(id = R.color.stream_compose_primary_accent_dark), From 15e9041e78aa2799e31c5cb4c2f2468ad288d689 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 09:38:32 +0300 Subject: [PATCH 11/24] [2127] Add rounded cornenrs to search input --- .../io/getstream/slack/compose/model/Workspace.kt | 4 ++-- .../getstream/slack/compose/ui/theme/SlackShapes.kt | 13 ++----------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt index 89bfce22..3b48409a 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt @@ -5,11 +5,11 @@ import androidx.annotation.DrawableRes /** * A model that represents a Slack workspace. * - * @param title The name name of the workspace. + * @param title The name of the workspace. * @param logo The logo of the workspace. */ data class Workspace( val title: String, @DrawableRes val logo: Int -) \ No newline at end of file +) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt index b3f4c02b..7db744a2 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt @@ -7,17 +7,8 @@ import io.getstream.chat.android.compose.ui.theme.StreamShapes @Composable fun slackShapes(): StreamShapes { - return StreamShapes( + return StreamShapes.defaultShapes().copy( avatar = RoundedCornerShape(4.dp), - myMessageBubble = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 16.dp), - otherMessageBubble = RoundedCornerShape( - topStart = 16.dp, - topEnd = 16.dp, - bottomEnd = 16.dp - ), - inputField = RoundedCornerShape(0.dp), - attachment = RoundedCornerShape(8.dp), - imageThumbnail = RoundedCornerShape(8.dp), - bottomSheet = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + inputField = RoundedCornerShape(8.dp), ) } \ No newline at end of file From c26c127d800ad83e6be20303371f95873857131d Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 13:23:45 +0300 Subject: [PATCH 12/24] [2127] Improve online indicator --- .../getstream/slack/compose/ui/channels/ChannelItem.kt | 2 +- .../compose/ui/channels/components/OnlineIndicator.kt | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt index dc0a5845..73997bff 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt @@ -71,7 +71,7 @@ fun DirectOneToOneChatItem( OnlineIndicator( modifier = Modifier .align(Alignment.BottomEnd) - .size(12.dp), + .size(10.dp), isOnline = user.online ) } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt index be510713..eb1a885b 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt @@ -11,11 +11,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import io.getstream.chat.android.compose.ui.common.avatar.UserAvatar import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.slack.compose.ui.theme.SlackTheme /** - * A simple view component representation for User online status green indicator. + * Component that represents an online indicator to be used with [UserAvatar]. * * @param isOnline - boolean toggle to update to either a green or grey dot. * @param modifier - Modifier for styling. @@ -27,11 +28,13 @@ fun OnlineIndicator( modifier: Modifier = Modifier, shape: Shape = CircleShape, ) { + val borderColor = if (isOnline) Color.Transparent else ChatTheme.colors.borders + val indicatorColor = if (isOnline) ChatTheme.colors.infoAccent else Color.White Box( modifier = modifier + .border(2.dp, borderColor, shape) .clip(shape) - .background(if (isOnline) ChatTheme.colors.infoAccent else Color.Transparent) - .border(2.dp, if (isOnline) Color.Transparent else ChatTheme.colors.borders) + .background(indicatorColor) ) } From 4c28940489d2fbc20b70e6f73122163c1380ad19 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 15:19:08 +0300 Subject: [PATCH 13/24] [2127] Use snapshot version of the SDK --- build.gradle | 1 + slack-clone-compose-sample/build.gradle | 2 +- .../slack/compose/ui/messages/MessagesActivity.kt | 4 +--- .../slack/compose/ui/theme/SlackColors.kt | 14 +++++++++----- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 5dc1141a..0e830539 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ buildscript { allprojects { apply plugin: "org.jlleitschuh.gradle.ktlint" repositories { + maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } mavenCentral() google() jcenter() diff --git a/slack-clone-compose-sample/build.gradle b/slack-clone-compose-sample/build.gradle index 24afc5e2..56a7921a 100644 --- a/slack-clone-compose-sample/build.gradle +++ b/slack-clone-compose-sample/build.gradle @@ -33,7 +33,7 @@ android { dependencies { // Stream SDK - implementation "io.getstream:stream-chat-android-compose:4.18.0-beta" + implementation "io.getstream:stream-chat-android-compose:4.19.1-SNAPSHOT" implementation Dependencies.androidxCoreKtx implementation Dependencies.androidxAppCompat diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt index 1c2a2223..b939f767 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt @@ -1,6 +1,5 @@ package io.getstream.slack.compose.ui.messages -import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.os.Bundle @@ -33,10 +32,9 @@ class MessagesActivity : AppCompatActivity() { val channelId = "messaging:sample-app-channel-0" // TODO: obtain cid from Intent return@lazy MessagesViewModelFactory( context = this, - clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager, + channelId = channelId, chatClient = ChatClient.instance(), chatDomain = ChatDomain.instance(), - channelId = channelId, enforceUniqueReactions = true, messageLimit = 30 ) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt index 8aff9a5a..04db6d3a 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt @@ -15,10 +15,12 @@ fun slackLightColors(): StreamColors = StreamColors( appBackground = colorResource(R.color.stream_compose_app_background), barsBackground = colorResource(R.color.slackish), linkBackground = colorResource(R.color.stream_compose_link_background), - overlay = colorResource(R.color.stream_compose_overlay), - primaryAccent = colorResource(id = R.color.stream_compose_primary_accent), + overlay = colorResource(R.color.stream_compose_overlay_regular), + overlayDark = colorResource(R.color.stream_compose_overlay_dark), + primaryAccent = colorResource(R.color.stream_compose_primary_accent), errorAccent = colorResource(R.color.stream_compose_error_accent), infoAccent = colorResource(R.color.stream_compose_info_accent), + highlight = colorResource(R.color.stream_compose_highlight), ) @Composable @@ -26,13 +28,15 @@ fun slackDarkColors(): StreamColors = StreamColors( textHighEmphasis = colorResource(R.color.stream_compose_text_high_emphasis_dark), textLowEmphasis = colorResource(R.color.stream_compose_text_low_emphasis_dark), disabled = colorResource(R.color.stream_compose_disabled_dark), - borders = colorResource(R.color.stream_compose_borders), + borders = colorResource(R.color.stream_compose_borders_dark), inputBackground = colorResource(R.color.stream_compose_input_background_dark), appBackground = colorResource(R.color.stream_compose_app_background_dark), barsBackground = colorResource(R.color.slackish), linkBackground = colorResource(R.color.stream_compose_link_background_dark), - overlay = colorResource(R.color.stream_compose_overlay_dark), - primaryAccent = colorResource(id = R.color.stream_compose_primary_accent_dark), + overlay = colorResource(R.color.stream_compose_overlay_regular_dark), + overlayDark = colorResource(R.color.stream_compose_overlay_dark_dark), + primaryAccent = colorResource(R.color.stream_compose_primary_accent_dark), errorAccent = colorResource(R.color.stream_compose_error_accent_dark), infoAccent = colorResource(R.color.stream_compose_info_accent_dark), + highlight = colorResource(R.color.stream_compose_highlight_dark), ) From a44065775ba3c36469e6bc51c9adf07ec7a09da7 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 21:56:31 +0300 Subject: [PATCH 14/24] [2127] Add color palette --- .../io/getstream/samples/Dependencies.kt | 3 +- slack-clone-compose-sample/build.gradle | 2 +- .../compose/ui/channels/ChannelsActivity.kt | 27 ++++++++++++ .../slack/compose/ui/theme/Colors.kt | 42 +++++++++++++++++++ .../ui/theme/{SlackShapes.kt => Shapes.kt} | 0 .../slack/compose/ui/theme/SlackColors.kt | 42 ------------------- .../slack/compose/ui/theme/SlackTheme.kt | 7 +++- .../{SlackTypography.kt => Typography.kt} | 0 .../src/main/res/values/colors.xml | 34 ++++++++++++++- 9 files changed, 110 insertions(+), 47 deletions(-) create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Colors.kt rename slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/{SlackShapes.kt => Shapes.kt} (100%) delete mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt rename slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/{SlackTypography.kt => Typography.kt} (100%) diff --git a/buildSrc/src/main/kotlin/io/getstream/samples/Dependencies.kt b/buildSrc/src/main/kotlin/io/getstream/samples/Dependencies.kt index ffc616d8..ac2928a7 100644 --- a/buildSrc/src/main/kotlin/io/getstream/samples/Dependencies.kt +++ b/buildSrc/src/main/kotlin/io/getstream/samples/Dependencies.kt @@ -14,7 +14,7 @@ private const val ANDROIDX_RECYCLERVIEW_VERSION = "1.2.0-beta01" private const val ANDROIDX_VIEW_PAGER_2_VERSION = "1.0.0" private const val COIL_VERSION = "1.3.2" private const val COMPOSE_VERSION = "1.0.1" -private const val COMPOSE_ACCOMPANIST_VERSION = "0.18.0" +private const val COMPOSE_ACCOMPANIST_VERSION = "0.19.0" private const val COMPOSE_ACTIVITY_VERSION = "1.3.1" private const val COMPOSE_VIEW_MODEL_VERSION = "1.0.0-alpha07" private const val FRAGMENT_KTX_VERSION = "1.3.0" @@ -49,6 +49,7 @@ object Dependencies { const val composeMaterialIconsExtended = "androidx.compose.material:material-icons-extended:$COMPOSE_VERSION" const val composeAccompanistPermissions = "com.google.accompanist:accompanist-permissions:$COMPOSE_ACCOMPANIST_VERSION" const val composeAccompanistPager = "com.google.accompanist:accompanist-pager:$COMPOSE_ACCOMPANIST_VERSION" + const val composeAccompanistSystemUiController = "com.google.accompanist:accompanist-systemuicontroller:$COMPOSE_ACCOMPANIST_VERSION" const val composeActivity = "androidx.activity:activity-compose:$COMPOSE_ACTIVITY_VERSION" const val composeViewModel = "androidx.lifecycle:lifecycle-viewmodel-compose:$COMPOSE_VIEW_MODEL_VERSION" const val fragmentKtx = "androidx.fragment:fragment-ktx:$FRAGMENT_KTX_VERSION" diff --git a/slack-clone-compose-sample/build.gradle b/slack-clone-compose-sample/build.gradle index 56a7921a..0dc58bc7 100644 --- a/slack-clone-compose-sample/build.gradle +++ b/slack-clone-compose-sample/build.gradle @@ -47,9 +47,9 @@ dependencies { implementation Dependencies.composeMaterial implementation Dependencies.composeMaterialIconsCore implementation Dependencies.composeMaterialIconsExtended - implementation Dependencies.composeActivity implementation Dependencies.composeViewModel implementation Dependencies.composeAccompanistPermissions implementation Dependencies.composeAccompanistPager + implementation Dependencies.composeAccompanistSystemUiController } \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt index 2e619685..b00206aa 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt @@ -4,10 +4,14 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import com.google.accompanist.systemuicontroller.rememberSystemUiController import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.api.models.QuerySort import io.getstream.chat.android.client.models.Channel import io.getstream.chat.android.client.models.Filters +import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.viewmodel.channel.ChannelListViewModel import io.getstream.chat.android.compose.viewmodel.channel.ChannelViewModelFactory import io.getstream.chat.android.offline.ChatDomain @@ -39,6 +43,7 @@ class ChannelsActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { SlackTheme { + SetupSystemUI() ChannelsScreen( listViewModel = listViewModel, workspace = streamWorkspace, @@ -48,6 +53,28 @@ class ChannelsActivity : ComponentActivity() { } } + /** + * Responsible for updating the system UI. + */ + @Composable + private fun SetupSystemUI() { + val systemUiController = rememberSystemUiController() + + val statusBarColor = ChatTheme.colors.barsBackground + val navigationBarColor = ChatTheme.colors.appBackground + + SideEffect { + systemUiController.setStatusBarColor( + color = statusBarColor, + darkIcons = false + ) + systemUiController.setNavigationBarColor( + color = navigationBarColor, + darkIcons = false + ) + } + } + private fun openMessages(channel: Channel) { startActivity(MessagesActivity.getIntent(this, channel.cid)) } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Colors.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Colors.kt new file mode 100644 index 00000000..d1f85c85 --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Colors.kt @@ -0,0 +1,42 @@ +package io.getstream.slack.compose.ui.theme + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.colorResource +import io.getstream.chat.android.compose.ui.theme.StreamColors +import io.getstream.slack.compose.R + +@Composable +fun slackLightColors(): StreamColors = StreamColors( + textHighEmphasis = colorResource(R.color.text_high_emphasis), + textLowEmphasis = colorResource(R.color.text_low_emphasis), + disabled = colorResource(R.color.disabled), + borders = colorResource(R.color.borders), + inputBackground = colorResource(R.color.input_background), + appBackground = colorResource(R.color.app_background), + barsBackground = colorResource(R.color.bars_background), + linkBackground = colorResource(R.color.link_background), + overlay = colorResource(R.color.overlay_regular), + overlayDark = colorResource(R.color.overlay_dark), + primaryAccent = colorResource(R.color.primary_accent), + errorAccent = colorResource(R.color.error_accent), + infoAccent = colorResource(R.color.info_accent), + highlight = colorResource(R.color.highlight), +) + +@Composable +fun slackDarkColors(): StreamColors = StreamColors( + textHighEmphasis = colorResource(R.color.text_high_emphasis_dark), + textLowEmphasis = colorResource(R.color.text_low_emphasis_dark), + disabled = colorResource(R.color.disabled_dark), + borders = colorResource(R.color.borders_dark), + inputBackground = colorResource(R.color.input_background_dark), + appBackground = colorResource(R.color.app_background_dark), + barsBackground = colorResource(R.color.bars_background_dark), + linkBackground = colorResource(R.color.link_background_dark), + overlay = colorResource(R.color.overlay_regular_dark), + overlayDark = colorResource(R.color.overlay_dark_dark), + primaryAccent = colorResource(R.color.primary_accent_dark), + errorAccent = colorResource(R.color.error_accent_dark), + infoAccent = colorResource(R.color.info_accent_dark), + highlight = colorResource(R.color.highlight_dark), +) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Shapes.kt similarity index 100% rename from slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackShapes.kt rename to slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Shapes.kt diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt deleted file mode 100644 index 04db6d3a..00000000 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackColors.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.getstream.slack.compose.ui.theme - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.colorResource -import io.getstream.chat.android.compose.ui.theme.StreamColors -import io.getstream.slack.compose.R - -@Composable -fun slackLightColors(): StreamColors = StreamColors( - textHighEmphasis = colorResource(R.color.stream_compose_text_high_emphasis), - textLowEmphasis = colorResource(R.color.stream_compose_text_low_emphasis), - disabled = colorResource(R.color.stream_compose_disabled), - borders = colorResource(R.color.stream_compose_borders), - inputBackground = colorResource(R.color.stream_compose_input_background), - appBackground = colorResource(R.color.stream_compose_app_background), - barsBackground = colorResource(R.color.slackish), - linkBackground = colorResource(R.color.stream_compose_link_background), - overlay = colorResource(R.color.stream_compose_overlay_regular), - overlayDark = colorResource(R.color.stream_compose_overlay_dark), - primaryAccent = colorResource(R.color.stream_compose_primary_accent), - errorAccent = colorResource(R.color.stream_compose_error_accent), - infoAccent = colorResource(R.color.stream_compose_info_accent), - highlight = colorResource(R.color.stream_compose_highlight), -) - -@Composable -fun slackDarkColors(): StreamColors = StreamColors( - textHighEmphasis = colorResource(R.color.stream_compose_text_high_emphasis_dark), - textLowEmphasis = colorResource(R.color.stream_compose_text_low_emphasis_dark), - disabled = colorResource(R.color.stream_compose_disabled_dark), - borders = colorResource(R.color.stream_compose_borders_dark), - inputBackground = colorResource(R.color.stream_compose_input_background_dark), - appBackground = colorResource(R.color.stream_compose_app_background_dark), - barsBackground = colorResource(R.color.slackish), - linkBackground = colorResource(R.color.stream_compose_link_background_dark), - overlay = colorResource(R.color.stream_compose_overlay_regular_dark), - overlayDark = colorResource(R.color.stream_compose_overlay_dark_dark), - primaryAccent = colorResource(R.color.stream_compose_primary_accent_dark), - errorAccent = colorResource(R.color.stream_compose_error_accent_dark), - infoAccent = colorResource(R.color.stream_compose_info_accent_dark), - highlight = colorResource(R.color.stream_compose_highlight_dark), -) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTheme.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTheme.kt index 5919305b..02ab53fb 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTheme.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTheme.kt @@ -11,9 +11,12 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme * used by the SDK. */ @Composable -fun SlackTheme(content: @Composable () -> Unit) { +fun SlackTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { ChatTheme( - colors = if (isSystemInDarkTheme()) slackDarkColors() else slackLightColors(), + colors = if (darkTheme) slackDarkColors() else slackLightColors(), typography = slackTypography, shapes = slackShapes() ) { diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTypography.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Typography.kt similarity index 100% rename from slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/SlackTypography.kt rename to slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Typography.kt diff --git a/slack-clone-compose-sample/src/main/res/values/colors.xml b/slack-clone-compose-sample/src/main/res/values/colors.xml index bd4942ac..ec2db806 100644 --- a/slack-clone-compose-sample/src/main/res/values/colors.xml +++ b/slack-clone-compose-sample/src/main/res/values/colors.xml @@ -1,8 +1,40 @@ - #57265A + #39133E #FF000000 #FF111111 #FFEEEEEE #FFFFFFFF + + + #000000 + #72767E + #B4B7BB + #D7D7D7 + #E9EAED + #FFFFFF + #39133E + #E9F1FF + #80000000 + #99000000 + #006CFF + #CF375C + #34785D + #FBF4DD + + + #FFFFFF + #72767E + #4C525C + #313337 + #1C1E22 + #1B1D22 + #19161D + #00193D + #33000000 + #99FFFFFF + #006CFF + #CF375C + #34785D + #302D22 From a8072ec1586692b0910b0c97b8f2b8e196720c37 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 7 Oct 2021 22:45:11 +0300 Subject: [PATCH 15/24] [2127] Change workspace title color --- .../compose/ui/channels/ChannelsScreen.kt | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt index 3e5bad58..01afa929 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -24,6 +25,7 @@ import androidx.compose.runtime.setValue 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.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -60,8 +62,14 @@ fun ChannelsScreen( currentUser = currentUser, title = workspace.title, isNetworkAvailable = isNetworkAvailable, + leadingContent = { WorkspaceLogo(logo = workspace.logo) }, + titleContent = { + WorkspaceTitle( + modifier = Modifier.weight(1f), + title = workspace.title + ) + }, trailingContent = { Spacer(Modifier.width(36.dp)) }, - leadingContent = { WorkspaceLogo(logo = workspace.logo) } ) SearchInput( @@ -121,6 +129,22 @@ private fun WorkspaceLogo(@DrawableRes logo: Int) { } } +@Composable +internal fun WorkspaceTitle( + title: String, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier + .wrapContentWidth(align = Alignment.Start) + .padding(horizontal = 16.dp), + text = title, + style = ChatTheme.typography.title1, + maxLines = 1, + color = Color.White, + ) +} + /** * Component that represents the label shown in the search component, when there's no input. */ @@ -152,4 +176,4 @@ private fun EmptyContent() { textAlign = TextAlign.Center ) } -} \ No newline at end of file +} From 609fa17e615cb5499c535d5b28260a32787938d5 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Fri, 8 Oct 2021 07:54:52 +0300 Subject: [PATCH 16/24] [2127] Improve design --- .../slack/compose/ui/channels/ChannelItem.kt | 62 ++++++++--------- .../compose/ui/channels/ChannelsScreen.kt | 67 ++++++++++++------- .../ui/channels/components/OnlineIndicator.kt | 10 +-- .../slack/compose/ui/theme/Typography.kt | 3 +- .../src/main/res/values/colors.xml | 2 +- 5 files changed, 79 insertions(+), 65 deletions(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt index 73997bff..e923d08b 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -48,10 +48,10 @@ fun DirectOneToOneChatItem( ) { Row( modifier = modifier - .clickable { onChannelClick(channel) } - .padding(vertical = 2.dp, horizontal = 16.dp) - .fillMaxWidth() - .height(40.dp), + .clip(shape = RoundedCornerShape(6.dp)) + .clickable (onClick = { onChannelClick(channel) }) + .padding(horizontal = 8.dp) + .fillMaxSize(), verticalAlignment = Alignment.CenterVertically, ) { val user = channel.getOtherUser()!! @@ -59,25 +59,25 @@ fun DirectOneToOneChatItem( Box( modifier = Modifier .clip(ChatTheme.shapes.avatar) - .size(36.dp), + .height(32.dp) + .width(30.dp), ) { UserAvatar( modifier = Modifier - .width(32.dp) - .height(32.dp) - .align(Alignment.Center), + .width(24.dp) + .height(24.dp) + .align(Alignment.CenterStart), user = user ) OnlineIndicator( modifier = Modifier .align(Alignment.BottomEnd) - .size(10.dp), + .size(14.dp), isOnline = user.online ) } - Spacer(Modifier.width(10.dp)) - + Spacer(Modifier.width(6.dp)) ChannelName(channel) val unreadCount = channel.unreadCount ?: 0 @@ -104,13 +104,12 @@ fun DirectGroupChatItem( ) { Row( modifier = modifier - .clickable { onChannelClick(channel) } - .padding(vertical = 8.dp) - .fillMaxWidth() - .height(24.dp), + .clip(shape = RoundedCornerShape(6.dp)) + .clickable (onClick = { onChannelClick(channel) }) + .padding(horizontal = 8.dp) + .fillMaxSize(), verticalAlignment = Alignment.CenterVertically, ) { - Spacer(Modifier.width(10.dp)) Box( modifier = modifier .clip(RoundedCornerShape(4.dp)) @@ -118,8 +117,7 @@ fun DirectGroupChatItem( .background(ChatTheme.colors.borders), ) { Text( - modifier = Modifier - .align(Alignment.Center), + modifier = Modifier.align(Alignment.Center), text = "" + (channel.memberCount - 1), style = ChatTheme.typography.captionBold, fontSize = 12.sp, @@ -128,7 +126,7 @@ fun DirectGroupChatItem( color = ChatTheme.colors.textHighEmphasis, ) } - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(12.dp)) ChannelName(channel) } } @@ -149,17 +147,15 @@ fun ChannelItem( ) { Row( modifier = modifier - .clickable { onChannelClick(channel) } - .padding(vertical = 8.dp) - .fillMaxWidth() - .height(24.dp), + .clip(shape = RoundedCornerShape(6.dp)) + .clickable (onClick = { onChannelClick(channel) }) + .padding(horizontal = 8.dp) + .fillMaxSize(), verticalAlignment = Alignment.CenterVertically, ) { - Spacer(Modifier.width(16.dp)) - + Spacer(Modifier.width(2.dp)) Icon( - modifier = Modifier - .size(12.dp), + modifier = Modifier.size(16.dp), painter = painterResource(id = R.drawable.ic_channel), contentDescription = null, tint = if (channel.hasUnread) { @@ -168,9 +164,7 @@ fun ChannelItem( ChatTheme.colors.textLowEmphasis }, ) - - Spacer(Modifier.width(16.dp)) - + Spacer(Modifier.width(18.dp)) ChannelName(channel) } } @@ -187,7 +181,9 @@ fun ChannelName(channel: Channel) { style = ChatTheme.typography.body, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = ChatTheme.colors.textHighEmphasis, + color = if (channel.hasUnread) { + ChatTheme.colors.textHighEmphasis + } else ChatTheme.colors.textLowEmphasis, fontWeight = if (channel.hasUnread) FontWeight.Bold else FontWeight.Normal ) -} +} \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt index 01afa929..6a6008db 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -45,7 +46,7 @@ import io.getstream.slack.compose.ui.util.isDirectOneToOneChat fun ChannelsScreen( listViewModel: ChannelListViewModel, workspace: Workspace, - onItemClick: (Channel) -> Unit = {}, + onItemClick: (Channel) -> Unit = {} ) { val currentUser by listViewModel.user.collectAsState() val isNetworkAvailable by listViewModel.isOnline.collectAsState() @@ -59,6 +60,7 @@ fun ChannelsScreen( ) { ChannelListHeader( + modifier = Modifier.height(56.dp), currentUser = currentUser, title = workspace.title, isNetworkAvailable = isNetworkAvailable, @@ -72,19 +74,25 @@ fun ChannelsScreen( trailingContent = { Spacer(Modifier.width(36.dp)) }, ) - SearchInput( + Card( modifier = Modifier - .padding(16.dp) - .height(36.dp) + .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp) + .height(44.dp) .fillMaxWidth(), - query = searchQuery, - onValueChange = { - searchQuery = it - listViewModel.setSearchQuery(it) - }, - leadingIcon = { Spacer(Modifier.width(16.dp)) }, - label = { SearchInputHint() } - ) + elevation = 2.dp, + shape = ChatTheme.shapes.inputField + ) { + SearchInput( + modifier = Modifier.fillMaxSize(), + query = searchQuery, + onValueChange = { + searchQuery = it + listViewModel.setSearchQuery(it) + }, + leadingIcon = { Spacer(Modifier.width(16.dp)) }, + label = { SearchInputHint() } + ) + } ChannelList( modifier = Modifier.fillMaxSize(), @@ -92,19 +100,26 @@ fun ChannelsScreen( onChannelClick = onItemClick, emptyContent = { EmptyContent() }, itemContent = { - when { - it.isDirectOneToOneChat() -> DirectOneToOneChatItem( - channel = it, - onChannelClick = onItemClick - ) - it.isDirectGroupChat() -> DirectGroupChatItem( - channel = it, - onChannelClick = onItemClick - ) - else -> ChannelItem( - channel = it, - onChannelClick = onItemClick - ) + Box( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth() + .height(45.dp), + ) { + when { + it.isDirectOneToOneChat() -> DirectOneToOneChatItem( + channel = it, + onChannelClick = onItemClick + ) + it.isDirectGroupChat() -> DirectGroupChatItem( + channel = it, + onChannelClick = onItemClick + ) + else -> ChannelItem( + channel = it, + onChannelClick = onItemClick + ) + } } } ) @@ -176,4 +191,4 @@ private fun EmptyContent() { textAlign = TextAlign.Center ) } -} +} \ No newline at end of file diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt index eb1a885b..95a607b9 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt @@ -3,11 +3,11 @@ package io.getstream.slack.compose.ui.channels.components import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -28,11 +28,13 @@ fun OnlineIndicator( modifier: Modifier = Modifier, shape: Shape = CircleShape, ) { - val borderColor = if (isOnline) Color.Transparent else ChatTheme.colors.borders - val indicatorColor = if (isOnline) ChatTheme.colors.infoAccent else Color.White + val borderColor = if (isOnline) ChatTheme.colors.infoAccent else ChatTheme.colors.disabled + val indicatorColor = if (isOnline) ChatTheme.colors.infoAccent else ChatTheme.colors.appBackground Box( modifier = modifier - .border(2.dp, borderColor, shape) + .border(2.5.dp, ChatTheme.colors.appBackground, shape) + .padding(2.5.dp) + .border(1.5.dp, borderColor, shape) .clip(shape) .background(indicatorColor) ) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Typography.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Typography.kt index 4c5828b8..6ea61a48 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Typography.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Typography.kt @@ -4,6 +4,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp import io.getstream.chat.android.compose.ui.theme.StreamTypography import io.getstream.slack.compose.R @@ -33,7 +34,7 @@ private fun StreamTypography.withFontFamily(fontFamily: FontFamily): StreamTypog title1 = title1.withFontFamily(fontFamily), title3 = title3.withFontFamily(fontFamily), title3Bold = title3Bold.withFontFamily(fontFamily), - body = body.withFontFamily(fontFamily), + body = body.withFontFamily(fontFamily).copy(fontSize = 15.sp), bodyItalic = bodyItalic.withFontFamily(fontFamily), bodyBold = bodyBold.withFontFamily(fontFamily), footnote = footnote.withFontFamily(fontFamily), diff --git a/slack-clone-compose-sample/src/main/res/values/colors.xml b/slack-clone-compose-sample/src/main/res/values/colors.xml index ec2db806..5a351f84 100644 --- a/slack-clone-compose-sample/src/main/res/values/colors.xml +++ b/slack-clone-compose-sample/src/main/res/values/colors.xml @@ -11,7 +11,7 @@ #72767E #B4B7BB #D7D7D7 - #E9EAED + #FFFFFF #FFFFFF #39133E #E9F1FF From a741cf188d5861491d927f3cc4f78c79bc6ffd39 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Fri, 8 Oct 2021 08:54:24 +0300 Subject: [PATCH 17/24] [2127] Refactoring --- .../slack/compose/model/Workspace.kt | 2 + .../slack/compose/ui/channels/ChannelItem.kt | 8 +- .../compose/ui/channels/ChannelsActivity.kt | 13 +-- .../ui/channels/components/OnlineIndicator.kt | 19 ++-- .../compose/ui/messages/MessagesActivity.kt | 92 +++---------------- .../compose/ui/messages/MessagesScreen.kt | 88 ++++++++++++++++++ 6 files changed, 121 insertions(+), 101 deletions(-) create mode 100644 slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt index 3b48409a..482b3f39 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt @@ -13,3 +13,5 @@ data class Workspace( @DrawableRes val logo: Int ) + + diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt index e923d08b..aff21041 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt @@ -49,7 +49,7 @@ fun DirectOneToOneChatItem( Row( modifier = modifier .clip(shape = RoundedCornerShape(6.dp)) - .clickable (onClick = { onChannelClick(channel) }) + .clickable(onClick = { onChannelClick(channel) }) .padding(horizontal = 8.dp) .fillMaxSize(), verticalAlignment = Alignment.CenterVertically, @@ -73,7 +73,7 @@ fun DirectOneToOneChatItem( modifier = Modifier .align(Alignment.BottomEnd) .size(14.dp), - isOnline = user.online + online = user.online ) } @@ -105,7 +105,7 @@ fun DirectGroupChatItem( Row( modifier = modifier .clip(shape = RoundedCornerShape(6.dp)) - .clickable (onClick = { onChannelClick(channel) }) + .clickable(onClick = { onChannelClick(channel) }) .padding(horizontal = 8.dp) .fillMaxSize(), verticalAlignment = Alignment.CenterVertically, @@ -148,7 +148,7 @@ fun ChannelItem( Row( modifier = modifier .clip(shape = RoundedCornerShape(6.dp)) - .clickable (onClick = { onChannelClick(channel) }) + .clickable(onClick = { onChannelClick(channel) }) .padding(horizontal = 8.dp) .fillMaxSize(), verticalAlignment = Alignment.CenterVertically, diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt index b00206aa..811a9afd 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsActivity.kt @@ -33,11 +33,11 @@ class ChannelsActivity : ComponentActivity() { Filters.and( Filters.eq("type", "messaging"), Filters.`in`("members", listOf(currentUserId())) - ), + ) ) } - private val listViewModel: ChannelListViewModel by viewModels { factory } + private val listViewModel: ChannelListViewModel by viewModels(::factory) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -46,7 +46,7 @@ class ChannelsActivity : ComponentActivity() { SetupSystemUI() ChannelsScreen( listViewModel = listViewModel, - workspace = streamWorkspace, + workspace = sampleWorkspace, onItemClick = ::openMessages ) } @@ -81,11 +81,8 @@ class ChannelsActivity : ComponentActivity() { companion object { /** - * For the sake of example we hardcoded the "Stream" workspace. + * For the sake of this sample app, the workspace is hardcoded. */ - private val streamWorkspace = Workspace( - title = "getstream", - logo = R.drawable.ic_stream_logo - ) + private val sampleWorkspace = Workspace("getstream", R.drawable.ic_stream_logo) } } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt index 95a607b9..00084b34 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/OnlineIndicator.kt @@ -18,18 +18,21 @@ import io.getstream.slack.compose.ui.theme.SlackTheme /** * Component that represents an online indicator to be used with [UserAvatar]. * - * @param isOnline - boolean toggle to update to either a green or grey dot. - * @param modifier - Modifier for styling. - * @param shape - The shape of the online indicator. + * By default, the indicator is a green dot when the user is online and a grey + * dot when the user is offline. + * + * @param online If the user is online. + * @param modifier Modifier for styling. + * @param shape The shape of the online indicator. */ @Composable fun OnlineIndicator( - isOnline: Boolean, + online: Boolean, modifier: Modifier = Modifier, shape: Shape = CircleShape, ) { - val borderColor = if (isOnline) ChatTheme.colors.infoAccent else ChatTheme.colors.disabled - val indicatorColor = if (isOnline) ChatTheme.colors.infoAccent else ChatTheme.colors.appBackground + val borderColor = if (online) ChatTheme.colors.infoAccent else ChatTheme.colors.disabled + val indicatorColor = if (online) ChatTheme.colors.infoAccent else ChatTheme.colors.appBackground Box( modifier = modifier .border(2.5.dp, ChatTheme.colors.appBackground, shape) @@ -44,7 +47,7 @@ fun OnlineIndicator( @Composable fun OnlineIndicatorPreviewOnline() { SlackTheme { - OnlineIndicator(isOnline = true) + OnlineIndicator(online = true) } } @@ -52,6 +55,6 @@ fun OnlineIndicatorPreviewOnline() { @Composable fun OnlineIndicatorPreviewOffline() { SlackTheme { - OnlineIndicator(isOnline = false) + OnlineIndicator(online = false) } } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt index b939f767..6a6683e4 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt @@ -6,110 +6,40 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -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.material.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import io.getstream.chat.android.client.ChatClient -import io.getstream.chat.android.compose.ui.messages.composer.MessageComposer -import io.getstream.chat.android.compose.ui.messages.header.MessageListHeader -import io.getstream.chat.android.compose.ui.messages.list.MessageList import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.viewmodel.messages.AttachmentsPickerViewModel import io.getstream.chat.android.compose.viewmodel.messages.MessageComposerViewModel import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel import io.getstream.chat.android.compose.viewmodel.messages.MessagesViewModelFactory -import io.getstream.chat.android.offline.ChatDomain class MessagesActivity : AppCompatActivity() { + private val factory: MessagesViewModelFactory by lazy { - val channelId = "messaging:sample-app-channel-0" // TODO: obtain cid from Intent return@lazy MessagesViewModelFactory( context = this, - channelId = channelId, - chatClient = ChatClient.instance(), - chatDomain = ChatDomain.instance(), - enforceUniqueReactions = true, - messageLimit = 30 + channelId = intent.getStringExtra(KEY_CHANNEL_ID) ?: "", ) } - private val listViewModel by viewModels(factoryProducer = { factory }) - private val composerViewModel by viewModels(factoryProducer = { factory }) - val attachmentsPickerViewModel by viewModels(factoryProducer = { factory }) + private val listViewModel by viewModels(::factory) + private val composerViewModel by viewModels(::factory) + private val attachmentsPickerViewModel by viewModels(::factory) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContent { ChatTheme { - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - Header( - listViewModel = listViewModel, - attachmentsPickerViewModel = attachmentsPickerViewModel - ) - }, - bottomBar = { - MessageComposer(viewModel = composerViewModel) - } - ) { - MessageList( - modifier = Modifier - .fillMaxSize() - .padding(it), - viewModel = listViewModel, - ) - } - } - } - } - - @Composable - fun Header( - listViewModel: MessageListViewModel, - attachmentsPickerViewModel: AttachmentsPickerViewModel - ) { - val user by listViewModel.user.collectAsState() - val isNetworkAvailable by listViewModel.isOnline.collectAsState() - val messageMode = listViewModel.messageMode - val backAction = { - val isInThread = listViewModel.isInThread - val isShowingOverlay = listViewModel.isShowingOverlay - - when { - attachmentsPickerViewModel.isShowingAttachments -> attachmentsPickerViewModel.changeAttachmentState( - false + MessagesScreen( + listViewModel = listViewModel, + composerViewModel = composerViewModel, + attachmentsPickerViewModel = attachmentsPickerViewModel, + onBackPressed = { finish() }, ) - isShowingOverlay -> listViewModel.selectMessage(null) - isInThread -> { - listViewModel.leaveThread() - composerViewModel.leaveThread() - } - else -> onBackPressed() } } - - MessageListHeader( - modifier = Modifier - .fillMaxWidth() - .height(56.dp), - channel = listViewModel.channel, - currentUser = user, - isNetworkAvailable = isNetworkAvailable, - messageMode = messageMode, - onHeaderActionClick = {}, - onBackPressed = backAction - ) } + companion object { private const val KEY_CHANNEL_ID = "channelId" diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt new file mode 100644 index 00000000..0f19e11e --- /dev/null +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt @@ -0,0 +1,88 @@ +package io.getstream.slack.compose.ui.messages + +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.material.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.getstream.chat.android.compose.ui.messages.composer.MessageComposer +import io.getstream.chat.android.compose.ui.messages.header.MessageListHeader +import io.getstream.chat.android.compose.ui.messages.list.MessageList +import io.getstream.chat.android.compose.viewmodel.messages.AttachmentsPickerViewModel +import io.getstream.chat.android.compose.viewmodel.messages.MessageComposerViewModel +import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel + +@Composable +fun MessagesScreen( + composerViewModel: MessageComposerViewModel, + listViewModel: MessageListViewModel, + attachmentsPickerViewModel: AttachmentsPickerViewModel, + onBackPressed: () -> Unit = {}, +) { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + Header( + composerViewModel = composerViewModel, + listViewModel = listViewModel, + attachmentsPickerViewModel = attachmentsPickerViewModel, + onBackPressed = onBackPressed + ) + }, + bottomBar = { + MessageComposer(viewModel = composerViewModel) + } + ) { + MessageList( + modifier = Modifier + .fillMaxSize() + .padding(it), + viewModel = listViewModel, + ) + } +} + +@Composable +fun Header( + composerViewModel: MessageComposerViewModel, + listViewModel: MessageListViewModel, + attachmentsPickerViewModel: AttachmentsPickerViewModel, + onBackPressed: () -> Unit = {}, +) { + val user by listViewModel.user.collectAsState() + val isNetworkAvailable by listViewModel.isOnline.collectAsState() + val messageMode = listViewModel.messageMode + val backAction = { + val isInThread = listViewModel.isInThread + val isShowingOverlay = listViewModel.isShowingOverlay + + when { + attachmentsPickerViewModel.isShowingAttachments -> attachmentsPickerViewModel.changeAttachmentState( + false + ) + isShowingOverlay -> listViewModel.selectMessage(null) + isInThread -> { + listViewModel.leaveThread() + composerViewModel.leaveThread() + } + else -> onBackPressed() + } + } + + MessageListHeader( + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + channel = listViewModel.channel, + currentUser = user, + isNetworkAvailable = isNetworkAvailable, + messageMode = messageMode, + onHeaderActionClick = {}, + onBackPressed = backAction + ) +} \ No newline at end of file From 416ccab8969524485c2dc243c8653cada4891a92 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Fri, 8 Oct 2021 09:08:10 +0300 Subject: [PATCH 18/24] [2127] Fix unread badge alignment --- .../slack/compose/ui/channels/ChannelItem.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt index aff21041..e9d0eada 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.Text @@ -78,7 +79,10 @@ fun DirectOneToOneChatItem( } Spacer(Modifier.width(6.dp)) - ChannelName(channel) + ChannelName( + channel = channel, + modifier = Modifier.weight(1f), + ) val unreadCount = channel.unreadCount ?: 0 if (unreadCount > 0) { @@ -173,17 +177,21 @@ fun ChannelItem( * Component that represents a channel name. * * @param channel The channel used to display the name. + * @param modifier Modifier for styling. */ @Composable -fun ChannelName(channel: Channel) { +fun ChannelName( + channel: Channel, + modifier: Modifier = Modifier +) { Text( + modifier = modifier + .wrapContentWidth(align = Alignment.Start), text = channel.getDisplayName(), style = ChatTheme.typography.body, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = if (channel.hasUnread) { - ChatTheme.colors.textHighEmphasis - } else ChatTheme.colors.textLowEmphasis, + color = if (channel.hasUnread) ChatTheme.colors.textHighEmphasis else ChatTheme.colors.textLowEmphasis, fontWeight = if (channel.hasUnread) FontWeight.Bold else FontWeight.Normal ) } \ No newline at end of file From efa84a6ec5b2a3c7fa4b1eca9b4eae4e43d1c147 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Fri, 8 Oct 2021 12:50:07 +0300 Subject: [PATCH 19/24] [2127] Fix ktlint --- slack-clone-compose-sample/build.gradle | 6 ++++++ .../kotlin/io/getstream/slack/compose/model/Workspace.kt | 2 -- .../io/getstream/slack/compose/ui/channels/ChannelItem.kt | 3 +-- .../getstream/slack/compose/ui/channels/ChannelsScreen.kt | 2 +- .../compose/ui/channels/components/UnreadCountBadge.kt | 2 +- .../getstream/slack/compose/ui/messages/MessagesActivity.kt | 1 - .../getstream/slack/compose/ui/messages/MessagesScreen.kt | 2 +- .../kotlin/io/getstream/slack/compose/ui/theme/Shapes.kt | 2 +- .../io/getstream/slack/compose/ui/theme/Typography.kt | 2 +- .../io/getstream/slack/compose/ui/util/ClientUtils.kt | 2 +- 10 files changed, 13 insertions(+), 11 deletions(-) diff --git a/slack-clone-compose-sample/build.gradle b/slack-clone-compose-sample/build.gradle index 0dc58bc7..90e55e84 100644 --- a/slack-clone-compose-sample/build.gradle +++ b/slack-clone-compose-sample/build.gradle @@ -29,6 +29,12 @@ android { kotlinCompilerExtensionVersion '1.0.1' kotlinCompilerVersion '1.5.21' } + + sourceSets { + all { + it.java.srcDir "src/$it.name/kotlin" + } + } } dependencies { diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt index 482b3f39..3b48409a 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/model/Workspace.kt @@ -13,5 +13,3 @@ data class Workspace( @DrawableRes val logo: Int ) - - diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt index e9d0eada..c452be0d 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt @@ -135,7 +135,6 @@ fun DirectGroupChatItem( } } - /** * Component that represents a regular named channel. * @@ -194,4 +193,4 @@ fun ChannelName( color = if (channel.hasUnread) ChatTheme.colors.textHighEmphasis else ChatTheme.colors.textLowEmphasis, fontWeight = if (channel.hasUnread) FontWeight.Bold else FontWeight.Normal ) -} \ No newline at end of file +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt index 6a6008db..6ff09140 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt @@ -191,4 +191,4 @@ private fun EmptyContent() { textAlign = TextAlign.Center ) } -} \ No newline at end of file +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt index f9f250f5..0cced3f7 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/components/UnreadCountBadge.kt @@ -70,4 +70,4 @@ fun UnreadCountBadgePreviewMany() { SlackTheme { UnreadCountBadge(unreadCount = 150) } -} \ No newline at end of file +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt index 6a6683e4..9314636d 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesActivity.kt @@ -39,7 +39,6 @@ class MessagesActivity : AppCompatActivity() { } } - companion object { private const val KEY_CHANNEL_ID = "channelId" diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt index 0f19e11e..b3fdbf07 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt @@ -85,4 +85,4 @@ fun Header( onHeaderActionClick = {}, onBackPressed = backAction ) -} \ No newline at end of file +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Shapes.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Shapes.kt index 7db744a2..2ceec882 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Shapes.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Shapes.kt @@ -11,4 +11,4 @@ fun slackShapes(): StreamShapes { avatar = RoundedCornerShape(4.dp), inputField = RoundedCornerShape(8.dp), ) -} \ No newline at end of file +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Typography.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Typography.kt index 6ea61a48..d091fc1d 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Typography.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/theme/Typography.kt @@ -52,4 +52,4 @@ private fun StreamTypography.withFontFamily(fontFamily: FontFamily): StreamTypog */ private fun TextStyle.withFontFamily(fontFamily: FontFamily): TextStyle { return copy(fontFamily = fontFamily) -} \ No newline at end of file +} diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ClientUtils.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ClientUtils.kt index 33f6ae1a..e014aea2 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ClientUtils.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/util/ClientUtils.kt @@ -7,4 +7,4 @@ import io.getstream.chat.android.client.ChatClient */ fun currentUserId(): String { return ChatClient.instance().getCurrentUser()?.id ?: "" -} \ No newline at end of file +} From 013d4ad3dd5bd3a97d6caf1b3e0506058ca36b24 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Fri, 8 Oct 2021 19:47:17 +0300 Subject: [PATCH 20/24] [2127] Refactor channel items --- .../slack/compose/ui/channels/ChannelItem.kt | 229 +++++++++--------- .../compose/ui/channels/ChannelsScreen.kt | 27 +-- 2 files changed, 109 insertions(+), 147 deletions(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt index c452be0d..35fb790d 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box 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 @@ -24,6 +25,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.getstream.chat.android.client.models.Channel +import io.getstream.chat.android.client.models.User import io.getstream.chat.android.compose.ui.common.avatar.UserAvatar import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.ui.util.getDisplayName @@ -31,166 +33,151 @@ import io.getstream.slack.compose.R import io.getstream.slack.compose.ui.channels.components.OnlineIndicator import io.getstream.slack.compose.ui.channels.components.UnreadCountBadge import io.getstream.slack.compose.ui.util.getOtherUser +import io.getstream.slack.compose.ui.util.isDirectGroupChat +import io.getstream.slack.compose.ui.util.isDirectOneToOneChat /** - * Component that represents a one-to-one channel item. + * Component that represents a channel item. * - * One-to-one channel is a distinct channel with only two members. + * There are 3 types of channel items that are displayed differently: + * - Direct ono-to-one channel - displayed with user avatar, online indicator and unread count badge. + * - Direct group channel - displayed with member counter and unread count badge. + * - Regular named channel - displayed with a hash sign. * - * @param channel The channel to display. + * @param channel The channel data to show. * @param onChannelClick Handler for a single tap on an item. - * @param modifier Modifier for styling. */ @Composable -fun DirectOneToOneChatItem( +fun ChannelItem( channel: Channel, - onChannelClick: (Channel) -> Unit, - modifier: Modifier = Modifier, + onChannelClick: (Channel) -> Unit ) { - Row( - modifier = modifier - .clip(shape = RoundedCornerShape(6.dp)) - .clickable(onClick = { onChannelClick(channel) }) + Box( + modifier = Modifier .padding(horizontal = 8.dp) - .fillMaxSize(), - verticalAlignment = Alignment.CenterVertically, + .fillMaxWidth() + .height(45.dp), ) { - val user = channel.getOtherUser()!! - - Box( + Row( modifier = Modifier - .clip(ChatTheme.shapes.avatar) - .height(32.dp) - .width(30.dp), + .clip(shape = RoundedCornerShape(6.dp)) + .clickable(onClick = { onChannelClick(channel) }) + .padding(horizontal = 8.dp) + .fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, ) { - UserAvatar( - modifier = Modifier - .width(24.dp) - .height(24.dp) - .align(Alignment.CenterStart), - user = user - ) - OnlineIndicator( + val isDirectOneToOneChat = channel.isDirectOneToOneChat() + val isDirectGroupChat = channel.isDirectGroupChat() + val hasUnread = channel.hasUnread + + when { + isDirectOneToOneChat -> OneToOneAvatar(user = channel.getOtherUser()!!) + isDirectGroupChat -> MemberCountBadge(memberCount = channel.memberCount) + else -> HashIcon(isEmphasized = channel.hasUnread) + } + + Text( modifier = Modifier - .align(Alignment.BottomEnd) - .size(14.dp), - online = user.online + .weight(1f) + .wrapContentWidth(align = Alignment.Start), + text = channel.getDisplayName(), + style = ChatTheme.typography.body, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = if (hasUnread) { + ChatTheme.colors.textHighEmphasis + } else { + ChatTheme.colors.textLowEmphasis + }, + fontWeight = if (hasUnread) FontWeight.Bold else FontWeight.Normal ) - } - - Spacer(Modifier.width(6.dp)) - ChannelName( - channel = channel, - modifier = Modifier.weight(1f), - ) - val unreadCount = channel.unreadCount ?: 0 - if (unreadCount > 0) { - UnreadCountBadge(unreadCount) + // unread count badge is not displayed for named channels + if (isDirectOneToOneChat || isDirectGroupChat) { + val unreadCount = channel.unreadCount ?: 0 + if (unreadCount > 0) { + UnreadCountBadge(unreadCount) + } + } } } } /** - * Component that represents a distinct channel item. + * Component that represents a user avatar with online indicator. * - * A distinct channel is a channel created without ID based on members. - * - * @param channel The channel to display. - * @param onChannelClick Handler for a single tap on an item. - * @param modifier Modifier for styling. + * @param user The user for the avatar. */ @Composable -fun DirectGroupChatItem( - channel: Channel, - onChannelClick: (Channel) -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .clip(shape = RoundedCornerShape(6.dp)) - .clickable(onClick = { onChannelClick(channel) }) - .padding(horizontal = 8.dp) - .fillMaxSize(), - verticalAlignment = Alignment.CenterVertically, +private fun OneToOneAvatar(user: User) { + Box( + modifier = Modifier + .clip(ChatTheme.shapes.avatar) + .height(32.dp) + .width(30.dp), ) { - Box( - modifier = modifier - .clip(RoundedCornerShape(4.dp)) - .size(24.dp) - .background(ChatTheme.colors.borders), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = "" + (channel.memberCount - 1), - style = ChatTheme.typography.captionBold, - fontSize = 12.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = ChatTheme.colors.textHighEmphasis, - ) - } - Spacer(Modifier.width(12.dp)) - ChannelName(channel) + UserAvatar( + modifier = Modifier + .width(24.dp) + .height(24.dp) + .align(Alignment.CenterStart), + user = user + ) + OnlineIndicator( + modifier = Modifier + .align(Alignment.BottomEnd) + .size(14.dp), + online = user.online + ) } + Spacer(Modifier.width(6.dp)) } /** - * Component that represents a regular named channel. + * Component that represents a badge with member count in the channel. * - * @param channel The channel to display. - * @param onChannelClick Handler for a single tap on an item. - * @param modifier Modifier for styling. - * */ + * @param memberCount Total members in the channel. + */ @Composable -fun ChannelItem( - channel: Channel, - onChannelClick: (Channel) -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .clip(shape = RoundedCornerShape(6.dp)) - .clickable(onClick = { onChannelClick(channel) }) - .padding(horizontal = 8.dp) - .fillMaxSize(), - verticalAlignment = Alignment.CenterVertically, +private fun MemberCountBadge(memberCount: Int) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .size(24.dp) + .background(ChatTheme.colors.borders), ) { - Spacer(Modifier.width(2.dp)) - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(id = R.drawable.ic_channel), - contentDescription = null, - tint = if (channel.hasUnread) { - ChatTheme.colors.textHighEmphasis - } else { - ChatTheme.colors.textLowEmphasis - }, + // the current user is not counted + val otherMemberCount = memberCount - 1 + Text( + modifier = Modifier.align(Alignment.Center), + text = otherMemberCount.toString(), + style = ChatTheme.typography.captionBold, + fontSize = 12.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = ChatTheme.colors.textHighEmphasis, ) - Spacer(Modifier.width(18.dp)) - ChannelName(channel) } + Spacer(Modifier.width(12.dp)) } /** - * Component that represents a channel name. + * Component that represents a Slack style hash sign that used for the named channels. * - * @param channel The channel used to display the name. - * @param modifier Modifier for styling. + * @param isEmphasized If the icon is highlighted. */ @Composable -fun ChannelName( - channel: Channel, - modifier: Modifier = Modifier -) { - Text( - modifier = modifier - .wrapContentWidth(align = Alignment.Start), - text = channel.getDisplayName(), - style = ChatTheme.typography.body, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = if (channel.hasUnread) ChatTheme.colors.textHighEmphasis else ChatTheme.colors.textLowEmphasis, - fontWeight = if (channel.hasUnread) FontWeight.Bold else FontWeight.Normal +private fun HashIcon(isEmphasized: Boolean) { + Spacer(Modifier.width(2.dp)) + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = R.drawable.ic_channel), + contentDescription = null, + tint = if (isEmphasized) { + ChatTheme.colors.textHighEmphasis + } else { + ChatTheme.colors.textLowEmphasis + }, ) + Spacer(Modifier.width(18.dp)) } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt index 6ff09140..2b595a03 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt @@ -39,8 +39,6 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.viewmodel.channel.ChannelListViewModel import io.getstream.slack.compose.R import io.getstream.slack.compose.model.Workspace -import io.getstream.slack.compose.ui.util.isDirectGroupChat -import io.getstream.slack.compose.ui.util.isDirectOneToOneChat @Composable fun ChannelsScreen( @@ -58,7 +56,6 @@ fun ChannelsScreen( .fillMaxSize() .background(ChatTheme.colors.appBackground) ) { - ChannelListHeader( modifier = Modifier.height(56.dp), currentUser = currentUser, @@ -99,29 +96,7 @@ fun ChannelsScreen( viewModel = listViewModel, onChannelClick = onItemClick, emptyContent = { EmptyContent() }, - itemContent = { - Box( - modifier = Modifier - .padding(horizontal = 8.dp) - .fillMaxWidth() - .height(45.dp), - ) { - when { - it.isDirectOneToOneChat() -> DirectOneToOneChatItem( - channel = it, - onChannelClick = onItemClick - ) - it.isDirectGroupChat() -> DirectGroupChatItem( - channel = it, - onChannelClick = onItemClick - ) - else -> ChannelItem( - channel = it, - onChannelClick = onItemClick - ) - } - } - } + itemContent = { ChannelItem(it, onItemClick) } ) } } From 9c24b5a738f128f01700553773ccc4081165afdc Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Fri, 8 Oct 2021 20:14:29 +0300 Subject: [PATCH 21/24] [2127] Add ripple effect --- .../getstream/slack/compose/ui/channels/ChannelItem.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt index 35fb790d..609feaed 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelItem.kt @@ -2,6 +2,7 @@ package io.getstream.slack.compose.ui.channels import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -15,7 +16,9 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -61,7 +64,11 @@ fun ChannelItem( Row( modifier = Modifier .clip(shape = RoundedCornerShape(6.dp)) - .clickable(onClick = { onChannelClick(channel) }) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = { onChannelClick(channel) } + ) .padding(horizontal = 8.dp) .fillMaxSize(), verticalAlignment = Alignment.CenterVertically, From 2a8486d45d9d2a0af51995049a87f1ad41864586 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Thu, 21 Oct 2021 05:03:23 +0300 Subject: [PATCH 22/24] Simplify search input field evelation logic --- .../compose/ui/channels/ChannelsScreen.kt | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt index 2b595a03..95b4b4dd 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -26,6 +25,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -71,25 +71,20 @@ fun ChannelsScreen( trailingContent = { Spacer(Modifier.width(36.dp)) }, ) - Card( + SearchInput( modifier = Modifier .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp) .height(44.dp) - .fillMaxWidth(), - elevation = 2.dp, - shape = ChatTheme.shapes.inputField - ) { - SearchInput( - modifier = Modifier.fillMaxSize(), - query = searchQuery, - onValueChange = { - searchQuery = it - listViewModel.setSearchQuery(it) - }, - leadingIcon = { Spacer(Modifier.width(16.dp)) }, - label = { SearchInputHint() } - ) - } + .fillMaxWidth() + .shadow(elevation = 4.dp, shape = ChatTheme.shapes.inputField, clip = false), + query = searchQuery, + onValueChange = { + searchQuery = it + listViewModel.setSearchQuery(it) + }, + leadingIcon = { Spacer(Modifier.width(16.dp)) }, + label = { SearchInputHint() } + ) ChannelList( modifier = Modifier.fillMaxSize(), From 740cb915d2ffd51020654464443bcb1f3f82a656 Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Fri, 22 Oct 2021 11:54:17 +0300 Subject: [PATCH 23/24] Update test user credentials in Compose Slack clone --- .../kotlin/io/getstream/slack/compose/SlackCloneApp.kt | 8 ++++---- .../getstream/slack/compose/ui/channels/ChannelsScreen.kt | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/SlackCloneApp.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/SlackCloneApp.kt index 6cfb6147..3e2c12e7 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/SlackCloneApp.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/SlackCloneApp.kt @@ -26,13 +26,13 @@ class SlackCloneApp : Application() { private fun connectUser() { ChatClient.instance().connectUser( user = User( - id = "1f37e58d-d8b0-476a-a4f2-f8611e0d85d9", + id = "jc", extraData = mutableMapOf( - "name" to "Jc", - "image" to "https://firebasestorage.googleapis.com/v0/b/stream-chat-internal.appspot.com/o/users%2FJc.png?alt=media", + "name" to "Jc MiƱarro", + "image" to "https://ca.slack-edge.com/T02RM6X6B-U011KEXDPB2-891dbb8df64f-128", ), ), - token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMWYzN2U1OGQtZDhiMC00NzZhLWE0ZjItZjg2MTFlMGQ4NWQ5In0.l3u9P1NKhJ91rI1tzOcABGh045Kj69-iVkC2yUtohVw" + token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiamMifQ.2_5Hae3LKjVSfA0gQxXlZn54Bq6xDlhjPx2J7azUNB4" ).enqueue() } } diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt index 2b595a03..ecb6acee 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt @@ -107,12 +107,11 @@ fun ChannelsScreen( @Composable private fun WorkspaceLogo(@DrawableRes logo: Int) { Row { - Spacer(Modifier.width(8.dp)) - Image( painter = painterResource(id = logo), contentDescription = null, modifier = Modifier + .padding(start = 8.dp) .size(36.dp) .clip(RoundedCornerShape(8.dp)), ) From ac1966d1e8623a0858f4ddd7a67b6e5ea84c5bda Mon Sep 17 00:00:00 2001 From: Dmitrii Bychkov Date: Tue, 26 Oct 2021 16:33:29 +0300 Subject: [PATCH 24/24] Use network connection state --- slack-clone-compose-sample/build.gradle | 2 +- .../io/getstream/slack/compose/ui/channels/ChannelsScreen.kt | 4 ++-- .../io/getstream/slack/compose/ui/messages/MessagesScreen.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/slack-clone-compose-sample/build.gradle b/slack-clone-compose-sample/build.gradle index 90e55e84..a273d9fa 100644 --- a/slack-clone-compose-sample/build.gradle +++ b/slack-clone-compose-sample/build.gradle @@ -39,7 +39,7 @@ android { dependencies { // Stream SDK - implementation "io.getstream:stream-chat-android-compose:4.19.1-SNAPSHOT" + implementation "io.getstream:stream-chat-android-compose:4.20.1-SNAPSHOT" implementation Dependencies.androidxCoreKtx implementation Dependencies.androidxAppCompat diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt index 4a6a21bc..7df1117e 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/channels/ChannelsScreen.kt @@ -47,7 +47,7 @@ fun ChannelsScreen( onItemClick: (Channel) -> Unit = {} ) { val currentUser by listViewModel.user.collectAsState() - val isNetworkAvailable by listViewModel.isOnline.collectAsState() + val connectionState by listViewModel.connectionState.collectAsState() var searchQuery by rememberSaveable { mutableStateOf("") } @@ -60,7 +60,7 @@ fun ChannelsScreen( modifier = Modifier.height(56.dp), currentUser = currentUser, title = workspace.title, - isNetworkAvailable = isNetworkAvailable, + connectionState = connectionState, leadingContent = { WorkspaceLogo(logo = workspace.logo) }, titleContent = { WorkspaceTitle( diff --git a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt index b3fdbf07..08f45e31 100644 --- a/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt +++ b/slack-clone-compose-sample/src/main/kotlin/io/getstream/slack/compose/ui/messages/MessagesScreen.kt @@ -55,7 +55,7 @@ fun Header( onBackPressed: () -> Unit = {}, ) { val user by listViewModel.user.collectAsState() - val isNetworkAvailable by listViewModel.isOnline.collectAsState() + val connectionState by listViewModel.connectionState.collectAsState() val messageMode = listViewModel.messageMode val backAction = { val isInThread = listViewModel.isInThread @@ -80,7 +80,7 @@ fun Header( .height(56.dp), channel = listViewModel.channel, currentUser = user, - isNetworkAvailable = isNetworkAvailable, + connectionState = connectionState, messageMode = messageMode, onHeaderActionClick = {}, onBackPressed = backAction