From f75d061dc47b68f8e7bf0bdaced81b147ced32c5 Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:08:39 +0200 Subject: [PATCH] Update tutorial code to use Chat v7 --- build.gradle | 2 +- samplejava/build.gradle | 18 ++++---- .../example/chattutorial/ChannelActivity.java | 40 +++++++++--------- .../chattutorial/ChannelActivity2.java | 40 +++++++++--------- .../chattutorial/ChannelActivity3.java | 42 ++++++++++--------- .../chattutorial/ChannelActivity4.java | 40 +++++++++--------- .../com/example/chattutorial/EdgeToEdge.java | 22 ++++++++++ .../chattutorial/ImgurAttachmentFactory.java | 25 +++++------ .../example/chattutorial/MainActivity.java | 24 ++++------- .../src/main/res/layout/activity_channel.xml | 6 +-- .../main/res/layout/activity_channel_2.xml | 6 +-- .../main/res/layout/activity_channel_3.xml | 8 ++-- .../main/res/layout/activity_channel_4.xml | 8 ++-- samplejava/src/main/res/values/themes.xml | 2 + samplekotlin/build.gradle | 13 +++--- .../example/chattutorial/ChannelActivity.kt | 34 ++++++++------- .../example/chattutorial/ChannelActivity2.kt | 34 ++++++++------- .../example/chattutorial/ChannelActivity3.kt | 36 ++++++++-------- .../example/chattutorial/ChannelActivity4.kt | 34 ++++++++------- .../com/example/chattutorial/EdgeToEdge.kt | 16 +++++++ .../chattutorial/ImgurAttachmentFactory.kt | 9 ++-- .../com/example/chattutorial/MainActivity.kt | 26 ++++-------- .../src/main/res/layout/activity_channel.xml | 6 +-- .../main/res/layout/activity_channel_2.xml | 6 +-- .../main/res/layout/activity_channel_3.xml | 8 ++-- .../main/res/layout/activity_channel_4.xml | 8 ++-- samplekotlin/src/main/res/values/themes.xml | 2 + 27 files changed, 279 insertions(+), 236 deletions(-) create mode 100644 samplejava/src/main/java/com/example/chattutorial/EdgeToEdge.java create mode 100644 samplekotlin/src/main/java/com/example/chattutorial/EdgeToEdge.kt diff --git a/build.gradle b/build.gradle index 1442959..a19aed4 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:8.13.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0" } } diff --git a/samplejava/build.gradle b/samplejava/build.gradle index 1d8d86b..c2d2fbe 100644 --- a/samplejava/build.gradle +++ b/samplejava/build.gradle @@ -3,13 +3,13 @@ plugins { } android { - compileSdk 35 + compileSdk 36 namespace "com.example.chattutorial" defaultConfig { applicationId "com.example.chattutorial" - minSdk 21 - targetSdk 34 + minSdk 24 + targetSdk 36 versionCode 1 versionName "1.0" @@ -27,14 +27,18 @@ android { buildFeatures { viewBinding true } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } } dependencies { - // Add new dependencies - implementation "io.getstream:stream-chat-android-ui-components:6.28.0" - implementation "io.getstream:stream-chat-android-offline:6.28.0" + // Add the dependency + implementation "io.getstream:stream-chat-android-ui-components:7.3.0" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.7" implementation "com.google.android.material:material:1.12.0" implementation "androidx.activity:activity-ktx:1.10.1" - implementation "io.coil-kt:coil:2.7.0" + implementation "io.coil-kt.coil3:coil:3.1.0" } diff --git a/samplejava/src/main/java/com/example/chattutorial/ChannelActivity.java b/samplejava/src/main/java/com/example/chattutorial/ChannelActivity.java index 9084168..915c7cf 100644 --- a/samplejava/src/main/java/com/example/chattutorial/ChannelActivity.java +++ b/samplejava/src/main/java/com/example/chattutorial/ChannelActivity.java @@ -15,14 +15,14 @@ import io.getstream.chat.android.models.Message; import io.getstream.chat.android.ui.common.state.messages.Edit; import io.getstream.chat.android.ui.common.state.messages.MessageMode; -import io.getstream.chat.android.ui.feature.messages.header.MessageListHeaderView; +import io.getstream.chat.android.ui.feature.messages.header.ChannelHeaderView; import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModel; import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelBinding; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModel; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModelBinding; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModel; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModelBinding; import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel; import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelBinding; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelFactory; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelViewModelFactory; public class ChannelActivity extends AppCompatActivity { @@ -38,59 +38,61 @@ public static Intent newIntent(Context context, Channel channel) { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Step 0 - inflate binding + // inflate binding ActivityChannelBinding binding = ActivityChannelBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + EdgeToEdge.applySystemBarInsetsAsPadding(binding.getRoot()); + String cid = getIntent().getStringExtra(CID_KEY); if (cid == null) { throw new IllegalStateException("Specifying a channel id is required when starting ChannelActivity"); } - // Step 1 - Create three separate ViewModels for the views so it's easy - // to customize them individually - ViewModelProvider.Factory factory = new MessageListViewModelFactory.Builder(this) + // Create three separate ViewModels for the views so it's easy + // to customize them individually + ViewModelProvider.Factory factory = new ChannelViewModelFactory.Builder(this) .cid(cid) .build(); ViewModelProvider provider = new ViewModelProvider(this, factory); - MessageListHeaderViewModel messageListHeaderViewModel = provider.get(MessageListHeaderViewModel.class); + ChannelHeaderViewModel channelHeaderViewModel = provider.get(ChannelHeaderViewModel.class); MessageListViewModel messageListViewModel = provider.get(MessageListViewModel.class); MessageComposerViewModel messageComposerViewModel = provider.get(MessageComposerViewModel.class); // TODO set custom Imgur attachment factory - // Step 2 - Bind the view and ViewModels, they are loosely coupled so it's easy to customize - MessageListHeaderViewModelBinding.bind(messageListHeaderViewModel, binding.messageListHeaderView, this); + // Bind the view and ViewModels, they are loosely coupled so it's easy to customize + ChannelHeaderViewModelBinding.bind(channelHeaderViewModel, binding.channelHeaderView, this); MessageListViewModelBinding.bind(messageListViewModel, binding.messageListView, this); MessageComposerViewModelBinding.bind(messageComposerViewModel, binding.messageComposerView, this); - // Step 3 - Let both MessageListHeaderView and MessageComposerView know when we open a thread + // Let both ChannelHeaderView and MessageComposerView know when we open a thread messageListViewModel.getMode().observe(this, mode -> { if (mode instanceof MessageMode.MessageThread) { Message parentMessage = ((MessageMode.MessageThread) mode).getParentMessage(); - messageListHeaderViewModel.setActiveThread(parentMessage); + channelHeaderViewModel.setActiveThread(parentMessage); messageComposerViewModel.setMessageMode(new MessageMode.MessageThread(parentMessage)); } else if (mode instanceof MessageMode.Normal) { - messageListHeaderViewModel.resetThread(); + channelHeaderViewModel.resetThread(); messageComposerViewModel.leaveThread(); } }); - // Step 4 - Let the message input know when we are editing a message + // Let the message input know when we are editing a message binding.messageListView.setMessageEditHandler(message -> { messageComposerViewModel.performMessageAction(new Edit(message)); }); - // Step 5 - Handle navigate up state + // Handle navigate up state messageListViewModel.getState().observe(this, state -> { if (state instanceof MessageListViewModel.State.NavigateUp) { finish(); } }); - // Step 6 - Handle back button behaviour correctly when you're in a thread - MessageListHeaderView.OnClickListener backHandler = () -> messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed.INSTANCE); - binding.messageListHeaderView.setBackButtonClickListener(backHandler); + // Handle back button behaviour correctly when you're in a thread + ChannelHeaderView.OnClickListener backHandler = () -> messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed.INSTANCE); + binding.channelHeaderView.setBackButtonClickListener(backHandler); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { diff --git a/samplejava/src/main/java/com/example/chattutorial/ChannelActivity2.java b/samplejava/src/main/java/com/example/chattutorial/ChannelActivity2.java index 1e92cac..d0c5c96 100644 --- a/samplejava/src/main/java/com/example/chattutorial/ChannelActivity2.java +++ b/samplejava/src/main/java/com/example/chattutorial/ChannelActivity2.java @@ -18,15 +18,15 @@ import io.getstream.chat.android.models.Message; import io.getstream.chat.android.ui.common.state.messages.Edit; import io.getstream.chat.android.ui.common.state.messages.MessageMode; -import io.getstream.chat.android.ui.feature.messages.header.MessageListHeaderView; +import io.getstream.chat.android.ui.feature.messages.header.ChannelHeaderView; import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.AttachmentFactoryManager; import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModel; import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelBinding; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModel; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModelBinding; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModel; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModelBinding; import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel; import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelBinding; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelFactory; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelViewModelFactory; public class ChannelActivity2 extends AppCompatActivity { @@ -42,22 +42,24 @@ public static Intent newIntent(Context context, Channel channel) { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Step 0 - inflate binding + // inflate binding ActivityChannel2Binding binding = ActivityChannel2Binding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + EdgeToEdge.applySystemBarInsetsAsPadding(binding.getRoot()); + String cid = getIntent().getStringExtra(CID_KEY); if (cid == null) { throw new IllegalStateException("Specifying a channel id is required when starting ChannelActivity2"); } - // Step 1 - Create three separate ViewModels for the views so it's easy - // to customize them individually - ViewModelProvider.Factory factory = new MessageListViewModelFactory.Builder(this) + // Create three separate ViewModels for the views so it's easy + // to customize them individually + ViewModelProvider.Factory factory = new ChannelViewModelFactory.Builder(this) .cid(cid) .build(); ViewModelProvider provider = new ViewModelProvider(this, factory); - MessageListHeaderViewModel messageListHeaderViewModel = provider.get(MessageListHeaderViewModel.class); + ChannelHeaderViewModel channelHeaderViewModel = provider.get(ChannelHeaderViewModel.class); MessageListViewModel messageListViewModel = provider.get(MessageListViewModel.class); MessageComposerViewModel messageComposerViewModel = provider.get(MessageComposerViewModel.class); @@ -70,38 +72,38 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { AttachmentFactoryManager attachmentFactoryManager = new AttachmentFactoryManager(imgurAttachmentViewFactories); binding.messageListView.setAttachmentFactoryManager(attachmentFactoryManager); - // Step 2 - Bind the view and ViewModels, they are loosely coupled so it's easy to customize - MessageListHeaderViewModelBinding.bind(messageListHeaderViewModel, binding.messageListHeaderView, this); + // Bind the view and ViewModels, they are loosely coupled so it's easy to customize + ChannelHeaderViewModelBinding.bind(channelHeaderViewModel, binding.channelHeaderView, this); MessageListViewModelBinding.bind(messageListViewModel, binding.messageListView, this); MessageComposerViewModelBinding.bind(messageComposerViewModel, binding.messageComposerView, this); - // Step 3 - Let both MessageListHeaderView and MessageComposerView know when we open a thread + // Let both ChannelHeaderView and MessageComposerView know when we open a thread messageListViewModel.getMode().observe(this, mode -> { if (mode instanceof MessageMode.MessageThread) { Message parentMessage = ((MessageMode.MessageThread) mode).getParentMessage(); - messageListHeaderViewModel.setActiveThread(parentMessage); + channelHeaderViewModel.setActiveThread(parentMessage); messageComposerViewModel.setMessageMode(new MessageMode.MessageThread(parentMessage)); } else if (mode instanceof MessageMode.Normal) { - messageListHeaderViewModel.resetThread(); + channelHeaderViewModel.resetThread(); messageComposerViewModel.leaveThread(); } }); - // Step 4 - Let the message input know when we are editing a message + // Let the message input know when we are editing a message binding.messageListView.setMessageEditHandler(message -> { messageComposerViewModel.performMessageAction(new Edit(message)); }); - // Step 5 - Handle navigate up state + // Handle navigate up state messageListViewModel.getState().observe(this, state -> { if (state instanceof MessageListViewModel.State.NavigateUp) { finish(); } }); - // Step 6 - Handle back button behaviour correctly when you're in a thread - MessageListHeaderView.OnClickListener backHandler = () -> messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed.INSTANCE); - binding.messageListHeaderView.setBackButtonClickListener(backHandler); + // Handle back button behaviour correctly when you're in a thread + ChannelHeaderView.OnClickListener backHandler = () -> messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed.INSTANCE); + binding.channelHeaderView.setBackButtonClickListener(backHandler); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { diff --git a/samplejava/src/main/java/com/example/chattutorial/ChannelActivity3.java b/samplejava/src/main/java/com/example/chattutorial/ChannelActivity3.java index a273b01..b778fc6 100644 --- a/samplejava/src/main/java/com/example/chattutorial/ChannelActivity3.java +++ b/samplejava/src/main/java/com/example/chattutorial/ChannelActivity3.java @@ -23,18 +23,18 @@ import io.getstream.chat.android.models.Channel; import io.getstream.chat.android.models.Message; import io.getstream.chat.android.models.TypingEvent; -import io.getstream.chat.android.state.extensions.ChatClientExtensions; +import io.getstream.chat.android.client.api.state.ChatClientExtensions; import io.getstream.chat.android.ui.common.state.messages.Edit; import io.getstream.chat.android.ui.common.state.messages.MessageMode; -import io.getstream.chat.android.ui.feature.messages.header.MessageListHeaderView; +import io.getstream.chat.android.ui.feature.messages.header.ChannelHeaderView; import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.AttachmentFactoryManager; import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModel; import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelBinding; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModel; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModelBinding; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModel; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModelBinding; import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel; import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelBinding; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelFactory; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelViewModelFactory; import kotlinx.coroutines.flow.Flow; public class ChannelActivity3 extends AppCompatActivity { @@ -51,22 +51,24 @@ public static Intent newIntent(Context context, Channel channel) { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Step 0 - inflate binding + // inflate binding ActivityChannel3Binding binding = ActivityChannel3Binding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + EdgeToEdge.applySystemBarInsetsAsPadding(binding.getRoot()); + String cid = getIntent().getStringExtra(CID_KEY); if (cid == null) { throw new IllegalStateException("Specifying a channel id is required when starting ChannelActivity3"); } - // Step 1 - Create three separate ViewModels for the views so it's easy - // to customize them individually - ViewModelProvider.Factory factory = new MessageListViewModelFactory.Builder(this) + // Create three separate ViewModels for the views so it's easy + // to customize them individually + ViewModelProvider.Factory factory = new ChannelViewModelFactory.Builder(this) .cid(cid) .build(); ViewModelProvider provider = new ViewModelProvider(this, factory); - MessageListHeaderViewModel messageListHeaderViewModel = provider.get(MessageListHeaderViewModel.class); + ChannelHeaderViewModel channelHeaderViewModel = provider.get(ChannelHeaderViewModel.class); MessageListViewModel messageListViewModel = provider.get(MessageListViewModel.class); MessageComposerViewModel messageComposerViewModel = provider.get(MessageComposerViewModel.class); @@ -79,38 +81,38 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { AttachmentFactoryManager attachmentFactoryManager = new AttachmentFactoryManager(imgurAttachmentViewFactories); binding.messageListView.setAttachmentFactoryManager(attachmentFactoryManager); - // Step 2 - Bind the view and ViewModels, they are loosely coupled so it's easy to customize - MessageListHeaderViewModelBinding.bind(messageListHeaderViewModel, binding.messageListHeaderView, this); + // Bind the view and ViewModels, they are loosely coupled so it's easy to customize + ChannelHeaderViewModelBinding.bind(channelHeaderViewModel, binding.channelHeaderView, this); MessageListViewModelBinding.bind(messageListViewModel, binding.messageListView, this); MessageComposerViewModelBinding.bind(messageComposerViewModel, binding.messageComposerView, this); - // Step 3 - Let both MessageListHeaderView and MessageComposerView know when we open a thread + // Let both ChannelHeaderView and MessageComposerView know when we open a thread messageListViewModel.getMode().observe(this, mode -> { if (mode instanceof MessageMode.MessageThread) { Message parentMessage = ((MessageMode.MessageThread) mode).getParentMessage(); - messageListHeaderViewModel.setActiveThread(parentMessage); + channelHeaderViewModel.setActiveThread(parentMessage); messageComposerViewModel.setMessageMode(new MessageMode.MessageThread(parentMessage)); } else if (mode instanceof MessageMode.Normal) { - messageListHeaderViewModel.resetThread(); + channelHeaderViewModel.resetThread(); messageComposerViewModel.leaveThread(); } }); - // Step 4 - Let the message input know when we are editing a message + // Let the message input know when we are editing a message binding.messageListView.setMessageEditHandler(message -> { messageComposerViewModel.performMessageAction(new Edit(message)); }); - // Step 5 - Handle navigate up state + // Handle navigate up state messageListViewModel.getState().observe(this, state -> { if (state instanceof MessageListViewModel.State.NavigateUp) { finish(); } }); - // Step 6 - Handle back button behaviour correctly when you're in a thread - MessageListHeaderView.OnClickListener backHandler = () -> messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed.INSTANCE); - binding.messageListHeaderView.setBackButtonClickListener(backHandler); + // Handle back button behaviour correctly when you're in a thread + ChannelHeaderView.OnClickListener backHandler = () -> messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed.INSTANCE); + binding.channelHeaderView.setBackButtonClickListener(backHandler); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { diff --git a/samplejava/src/main/java/com/example/chattutorial/ChannelActivity4.java b/samplejava/src/main/java/com/example/chattutorial/ChannelActivity4.java index a47e92f..f9e964d 100644 --- a/samplejava/src/main/java/com/example/chattutorial/ChannelActivity4.java +++ b/samplejava/src/main/java/com/example/chattutorial/ChannelActivity4.java @@ -26,15 +26,15 @@ import io.getstream.chat.android.models.User; import io.getstream.chat.android.ui.common.state.messages.Edit; import io.getstream.chat.android.ui.common.state.messages.MessageMode; -import io.getstream.chat.android.ui.feature.messages.header.MessageListHeaderView; +import io.getstream.chat.android.ui.feature.messages.header.ChannelHeaderView; import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.AttachmentFactoryManager; import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModel; import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModelBinding; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModel; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModelBinding; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModel; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModelBinding; import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel; import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelBinding; -import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelFactory; +import io.getstream.chat.android.ui.viewmodel.messages.ChannelViewModelFactory; public class ChannelActivity4 extends AppCompatActivity { @@ -50,22 +50,24 @@ public static Intent newIntent(Context context, Channel channel) { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Step 0 - inflate binding + // inflate binding ActivityChannel4Binding binding = ActivityChannel4Binding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); + EdgeToEdge.applySystemBarInsetsAsPadding(binding.getRoot()); + String cid = getIntent().getStringExtra(CID_KEY); if (cid == null) { throw new IllegalStateException("Specifying a channel id is required when starting ChannelActivity4"); } - // Step 1 - Create three separate ViewModels for the views so it's easy - // to customize them individually - ViewModelProvider.Factory factory = new MessageListViewModelFactory.Builder(this) + // Create three separate ViewModels for the views so it's easy + // to customize them individually + ViewModelProvider.Factory factory = new ChannelViewModelFactory.Builder(this) .cid(cid) .build(); ViewModelProvider provider = new ViewModelProvider(this, factory); - MessageListHeaderViewModel messageListHeaderViewModel = provider.get(MessageListHeaderViewModel.class); + ChannelHeaderViewModel channelHeaderViewModel = provider.get(ChannelHeaderViewModel.class); MessageListViewModel messageListViewModel = provider.get(MessageListViewModel.class); MessageComposerViewModel messageComposerViewModel = provider.get(MessageComposerViewModel.class); @@ -78,38 +80,38 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { AttachmentFactoryManager attachmentFactoryManager = new AttachmentFactoryManager(imgurAttachmentViewFactories); binding.messageListView.setAttachmentFactoryManager(attachmentFactoryManager); - // Step 2 - Bind the view and ViewModels, they are loosely coupled so it's easy to customize - MessageListHeaderViewModelBinding.bind(messageListHeaderViewModel, binding.messageListHeaderView, this); + // Bind the view and ViewModels, they are loosely coupled so it's easy to customize + ChannelHeaderViewModelBinding.bind(channelHeaderViewModel, binding.channelHeaderView, this); MessageListViewModelBinding.bind(messageListViewModel, binding.messageListView, this); MessageComposerViewModelBinding.bind(messageComposerViewModel, binding.messageComposerView, this); - // Step 3 - Let both MessageListHeaderView and MessageComposerView know when we open a thread + // Let both ChannelHeaderView and MessageComposerView know when we open a thread messageListViewModel.getMode().observe(this, mode -> { if (mode instanceof MessageMode.MessageThread) { Message parentMessage = ((MessageMode.MessageThread) mode).getParentMessage(); - messageListHeaderViewModel.setActiveThread(parentMessage); + channelHeaderViewModel.setActiveThread(parentMessage); messageComposerViewModel.setMessageMode(new MessageMode.MessageThread(parentMessage)); } else if (mode instanceof MessageMode.Normal) { - messageListHeaderViewModel.resetThread(); + channelHeaderViewModel.resetThread(); messageComposerViewModel.leaveThread(); } }); - // Step 4 - Let the message input know when we are editing a message + // Let the message input know when we are editing a message binding.messageListView.setMessageEditHandler(message -> { messageComposerViewModel.performMessageAction(new Edit(message)); }); - // Step 5 - Handle navigate up state + // Handle navigate up state messageListViewModel.getState().observe(this, state -> { if (state instanceof MessageListViewModel.State.NavigateUp) { finish(); } }); - // Step 6 - Handle back button behaviour correctly when you're in a thread - MessageListHeaderView.OnClickListener backHandler = () -> messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed.INSTANCE); - binding.messageListHeaderView.setBackButtonClickListener(backHandler); + // Handle back button behaviour correctly when you're in a thread + ChannelHeaderView.OnClickListener backHandler = () -> messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed.INSTANCE); + binding.channelHeaderView.setBackButtonClickListener(backHandler); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { @Override public void handleOnBackPressed() { diff --git a/samplejava/src/main/java/com/example/chattutorial/EdgeToEdge.java b/samplejava/src/main/java/com/example/chattutorial/EdgeToEdge.java new file mode 100644 index 0000000..25a75cd --- /dev/null +++ b/samplejava/src/main/java/com/example/chattutorial/EdgeToEdge.java @@ -0,0 +1,22 @@ +package com.example.chattutorial; + +import android.view.View; + +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +public final class EdgeToEdge { + + private EdgeToEdge() {} + + public static void applySystemBarInsetsAsPadding(View view) { + ViewCompat.setOnApplyWindowInsetsListener(view, (v, windowInsets) -> { + Insets insets = windowInsets.getInsets( + WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.ime() + ); + v.setPadding(insets.left, insets.top, insets.right, insets.bottom); + return WindowInsetsCompat.CONSUMED; + }); + } +} diff --git a/samplejava/src/main/java/com/example/chattutorial/ImgurAttachmentFactory.java b/samplejava/src/main/java/com/example/chattutorial/ImgurAttachmentFactory.java index aaaa483..5322d84 100644 --- a/samplejava/src/main/java/com/example/chattutorial/ImgurAttachmentFactory.java +++ b/samplejava/src/main/java/com/example/chattutorial/ImgurAttachmentFactory.java @@ -11,8 +11,10 @@ import org.jetbrains.annotations.NotNull; -import coil.Coil; -import coil.request.ImageRequest; +import coil3.SingletonImageLoader; +import coil3.request.ImageRequest; +import coil3.request.ImageRequestsKt; +import coil3.request.ImageRequests_androidKt; import io.getstream.chat.android.models.Attachment; import io.getstream.chat.android.models.Message; import io.getstream.chat.android.ui.feature.messages.list.adapter.MessageListListeners; @@ -25,13 +27,13 @@ public class ImgurAttachmentFactory extends BaseAttachmentFactory { - // Step 1 - Check whether the message contains an Imgur attachment + // Check whether the message contains an Imgur attachment @Override public boolean canHandle(@NonNull Message message) { return containsImgurAttachments(message) != null; } - // Step 2 - Create the ViewHolder that will be used to display the Imgur logo + // Create the ViewHolder that will be used to display the Imgur logo // over Imgur attachments @NonNull @Override @@ -73,14 +75,13 @@ public ImgurAttachmentViewHolder(AttachmentImgurBinding binding, binding.ivMediaThumb.setShapeAppearanceModel(shapeAppearanceModel); if (imgurAttachment != null) { - ImageRequest imageRequest = new ImageRequest.Builder(binding.getRoot().getContext()) - .data(imgurAttachment.getImageUrl()) - .allowHardware(false) - .crossfade(true) - .placeholder(io.getstream.chat.android.ui.R.drawable.stream_ui_picture_placeholder) - .target(binding.ivMediaThumb) - .build(); - Coil.imageLoader(binding.getRoot().getContext()).enqueue(imageRequest); + ImageRequest.Builder requestBuilder = new ImageRequest.Builder(binding.getRoot().getContext()) + .data(imgurAttachment.getImageUrl()); + ImageRequests_androidKt.allowHardware(requestBuilder, false); + ImageRequestsKt.crossfade(requestBuilder, true); + ImageRequests_androidKt.placeholder(requestBuilder, io.getstream.chat.android.ui.R.drawable.stream_ui_picture_placeholder); + ImageRequests_androidKt.target(requestBuilder, binding.ivMediaThumb); + SingletonImageLoader.get(binding.getRoot().getContext()).enqueue(requestBuilder.build()); } } } diff --git a/samplejava/src/main/java/com/example/chattutorial/MainActivity.java b/samplejava/src/main/java/com/example/chattutorial/MainActivity.java index 4bd42d6..8acca29 100644 --- a/samplejava/src/main/java/com/example/chattutorial/MainActivity.java +++ b/samplejava/src/main/java/com/example/chattutorial/MainActivity.java @@ -16,9 +16,6 @@ import io.getstream.chat.android.models.FilterObject; import io.getstream.chat.android.models.Filters; import io.getstream.chat.android.models.User; -import io.getstream.chat.android.offline.plugin.factory.StreamOfflinePluginFactory; -import io.getstream.chat.android.state.plugin.config.StatePluginConfig; -import io.getstream.chat.android.state.plugin.factory.StreamStatePluginFactory; import io.getstream.chat.android.ui.viewmodel.channels.ChannelListViewModel; import io.getstream.chat.android.ui.viewmodel.channels.ChannelListViewModelBinding; import io.getstream.chat.android.ui.viewmodel.channels.ChannelListViewModelFactory; @@ -28,25 +25,18 @@ public final class MainActivity extends AppCompatActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // Step 0 - inflate binding + // Inflate binding for layout ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - // Step 1 - Set up the OfflinePlugin for offline storage - StreamOfflinePluginFactory streamOfflinePluginFactory = new StreamOfflinePluginFactory( - getApplicationContext() - ); - StreamStatePluginFactory streamStatePluginFactory = new StreamStatePluginFactory( - new StatePluginConfig(true, true), this - ); + EdgeToEdge.applySystemBarInsetsAsPadding(binding.getRoot()); - // Step 2 - Set up the client for API calls with the plugin for offline storage + // Set up the client for API calls with offline storage and state management ChatClient client = new ChatClient.Builder("uun7ywwamhs9", getApplicationContext()) - .withPlugins(streamOfflinePluginFactory, streamStatePluginFactory) .logLevel(ChatLogLevel.ALL) // Set to NOTHING in prod .build(); - // Step 3 - Authenticate and connect the user + // Authenticate and connect the user User user = new User.Builder() .withId("tutorial-droid") .withName("Tutorial Droid") @@ -57,7 +47,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { user, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidHV0b3JpYWwtZHJvaWQifQ.WwfBzU1GZr0brt_fXnqKdKhz3oj0rbDUm2DqJO_SS5U" ).enqueue(result -> { - // Step 4 - Set the channel list filter and order + // Set the channel list filter and order // This can be read as requiring only channels whose "type" is "messaging" AND // whose "members" include our "user.id" FilterObject filter = Filters.and( @@ -73,8 +63,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { ChannelListViewModel channelsViewModel = new ViewModelProvider(this, factory).get(ChannelListViewModel.class); - // Step 5 - Connect the ChannelListViewModel to the ChannelListView, loose - // coupling makes it easy to customize + // Connect the ChannelListViewModel to the ChannelListView, loose + // coupling makes it easy to customize ChannelListViewModelBinding.bind(channelsViewModel, binding.channelListView, this); binding.channelListView.setChannelItemClickListener( channel -> startActivity(ChannelActivity4.newIntent(this, channel)) diff --git a/samplejava/src/main/res/layout/activity_channel.xml b/samplejava/src/main/res/layout/activity_channel.xml index db1fa0d..9d2185a 100644 --- a/samplejava/src/main/res/layout/activity_channel.xml +++ b/samplejava/src/main/res/layout/activity_channel.xml @@ -4,8 +4,8 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> - + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> - + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> - + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> @color/black ?attr/colorPrimaryVariant + true + true diff --git a/samplekotlin/build.gradle b/samplekotlin/build.gradle index 78bd1f1..988141f 100644 --- a/samplekotlin/build.gradle +++ b/samplekotlin/build.gradle @@ -4,13 +4,13 @@ plugins { } android { - compileSdk 35 + compileSdk 36 namespace "com.example.chattutorial" defaultConfig { applicationId "com.example.chattutorial" - minSdk 21 - targetSdk 34 + minSdk 24 + targetSdk 36 versionCode 1 versionName "1.0" } @@ -31,11 +31,10 @@ android { } dependencies { - // Add new dependencies - implementation "io.getstream:stream-chat-android-ui-components:6.28.0" - implementation "io.getstream:stream-chat-android-offline:6.28.0" + // Add the dependency + implementation "io.getstream:stream-chat-android-ui-components:7.3.0" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.7" implementation "com.google.android.material:material:1.12.0" implementation "androidx.activity:activity-ktx:1.10.1" - implementation "io.coil-kt:coil:2.7.0" + implementation "io.coil-kt.coil3:coil:3.1.0" } diff --git a/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity.kt b/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity.kt index e09f3b9..c26a6b0 100644 --- a/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity.kt +++ b/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity.kt @@ -11,9 +11,9 @@ import io.getstream.chat.android.models.Channel import io.getstream.chat.android.ui.common.state.messages.Edit import io.getstream.chat.android.ui.common.state.messages.MessageMode import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModel -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModel +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModel import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel -import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelFactory +import io.getstream.chat.android.ui.viewmodel.messages.ChannelViewModelFactory import io.getstream.chat.android.ui.viewmodel.messages.bindView class ChannelActivity : AppCompatActivity() { @@ -23,60 +23,62 @@ class ChannelActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // Step 0 - inflate binding + // inflate binding binding = ActivityChannelBinding.inflate(layoutInflater) setContentView(binding.root) + binding.root.applySystemBarInsetsAsPadding() + val cid = checkNotNull(intent.getStringExtra(CID_KEY)) { "Specifying a channel id is required when starting ChannelActivity" } - // Step 1 - Create three separate ViewModels for the views so it's easy - // to customize them individually - val factory = MessageListViewModelFactory(this, cid) - val messageListHeaderViewModel: MessageListHeaderViewModel by viewModels { factory } + // Create three separate ViewModels for the views so it's easy + // to customize them individually + val factory = ChannelViewModelFactory(this, cid) + val channelHeaderViewModel: ChannelHeaderViewModel by viewModels { factory } val messageListViewModel: MessageListViewModel by viewModels { factory } val messageComposerViewModel: MessageComposerViewModel by viewModels { factory } // TODO set custom Imgur attachment factory - // Step 2 - Bind the view and ViewModels, they are loosely coupled so it's easy to customize - messageListHeaderViewModel.bindView(binding.messageListHeaderView, this) + // Bind the view and ViewModels, they are loosely coupled so it's easy to customize + channelHeaderViewModel.bindView(binding.channelHeaderView, this) messageListViewModel.bindView(binding.messageListView, this) messageComposerViewModel.bindView(binding.messageComposerView, this) - // Step 3 - Let both MessageListHeaderView and MessageComposerView know when we open a thread + // Let both ChannelHeaderView and MessageComposerView know when we open a thread messageListViewModel.mode.observe(this) { mode -> when (mode) { is MessageMode.MessageThread -> { - messageListHeaderViewModel.setActiveThread(mode.parentMessage) + channelHeaderViewModel.setActiveThread(mode.parentMessage) messageComposerViewModel.setMessageMode(MessageMode.MessageThread(mode.parentMessage)) } is MessageMode.Normal -> { - messageListHeaderViewModel.resetThread() + channelHeaderViewModel.resetThread() messageComposerViewModel.leaveThread() } } } - // Step 4 - Let the message input know when we are editing a message + // Let the message input know when we are editing a message binding.messageListView.setMessageEditHandler { message -> messageComposerViewModel.performMessageAction(Edit(message)) } - // Step 5 - Handle navigate up state + // Handle navigate up state messageListViewModel.state.observe(this) { state -> if (state is MessageListViewModel.State.NavigateUp) { finish() } } - // Step 6 - Handle back button behaviour correctly when you're in a thread + // Handle back button behaviour correctly when you're in a thread val backHandler = { messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed) } - binding.messageListHeaderView.setBackButtonClickListener(backHandler) + binding.channelHeaderView.setBackButtonClickListener(backHandler) onBackPressedDispatcher.addCallback(this) { backHandler() } diff --git a/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity2.kt b/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity2.kt index 49a63e8..60a3395 100644 --- a/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity2.kt +++ b/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity2.kt @@ -12,9 +12,9 @@ import io.getstream.chat.android.ui.common.state.messages.Edit import io.getstream.chat.android.ui.common.state.messages.MessageMode import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.AttachmentFactoryManager import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModel -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModel +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModel import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel -import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelFactory +import io.getstream.chat.android.ui.viewmodel.messages.ChannelViewModelFactory import io.getstream.chat.android.ui.viewmodel.messages.bindView class ChannelActivity2 : AppCompatActivity() { @@ -24,18 +24,20 @@ class ChannelActivity2 : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // Step 0 - inflate binding + // inflate binding binding = ActivityChannel2Binding.inflate(layoutInflater) setContentView(binding.root) + binding.root.applySystemBarInsetsAsPadding() + val cid = checkNotNull(intent.getStringExtra(CID_KEY)) { "Specifying a channel id is required when starting ChannelActivity2" } - // Step 1 - Create three separate ViewModels for the views so it's easy - // to customize them individually - val factory = MessageListViewModelFactory(this, cid) - val messageListHeaderViewModel: MessageListHeaderViewModel by viewModels { factory } + // Create three separate ViewModels for the views so it's easy + // to customize them individually + val factory = ChannelViewModelFactory(this, cid) + val channelHeaderViewModel: ChannelHeaderViewModel by viewModels { factory } val messageListViewModel: MessageListViewModel by viewModels { factory } val messageComposerViewModel: MessageComposerViewModel by viewModels { factory } @@ -44,43 +46,43 @@ class ChannelActivity2 : AppCompatActivity() { val attachmentViewFactory = AttachmentFactoryManager(listOf(imgurAttachmentViewFactory)) binding.messageListView.setAttachmentFactoryManager(attachmentViewFactory) - // Step 2 - Bind the view and ViewModels, they are loosely coupled so it's easy to customize - messageListHeaderViewModel.bindView(binding.messageListHeaderView, this) + // Bind the view and ViewModels, they are loosely coupled so it's easy to customize + channelHeaderViewModel.bindView(binding.channelHeaderView, this) messageListViewModel.bindView(binding.messageListView, this) messageComposerViewModel.bindView(binding.messageComposerView, this) - // Step 3 - Let both MessageListHeaderView and MessageComposerView know when we open a thread + // Let both ChannelHeaderView and MessageComposerView know when we open a thread messageListViewModel.mode.observe(this) { mode -> when (mode) { is MessageMode.MessageThread -> { - messageListHeaderViewModel.setActiveThread(mode.parentMessage) + channelHeaderViewModel.setActiveThread(mode.parentMessage) messageComposerViewModel.setMessageMode(MessageMode.MessageThread(mode.parentMessage)) } is MessageMode.Normal -> { - messageListHeaderViewModel.resetThread() + channelHeaderViewModel.resetThread() messageComposerViewModel.leaveThread() } } } - // Step 4 - Let the message input know when we are editing a message + // Let the message input know when we are editing a message binding.messageListView.setMessageEditHandler { message -> messageComposerViewModel.performMessageAction(Edit(message)) } - // Step 5 - Handle navigate up state + // Handle navigate up state messageListViewModel.state.observe(this) { state -> if (state is MessageListViewModel.State.NavigateUp) { finish() } } - // Step 6 - Handle back button behaviour correctly when you're in a thread + // Handle back button behaviour correctly when you're in a thread val backHandler = { messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed) } - binding.messageListHeaderView.setBackButtonClickListener(backHandler) + binding.channelHeaderView.setBackButtonClickListener(backHandler) onBackPressedDispatcher.addCallback(this) { backHandler() } diff --git a/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity3.kt b/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity3.kt index 5239266..e3ddcf1 100644 --- a/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity3.kt +++ b/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity3.kt @@ -12,14 +12,14 @@ import androidx.lifecycle.repeatOnLifecycle import com.example.chattutorial.databinding.ActivityChannel3Binding import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.models.Channel -import io.getstream.chat.android.state.extensions.watchChannelAsState +import io.getstream.chat.android.client.api.state.watchChannelAsState import io.getstream.chat.android.ui.common.state.messages.Edit import io.getstream.chat.android.ui.common.state.messages.MessageMode import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.AttachmentFactoryManager import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModel -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModel +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModel import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel -import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelFactory +import io.getstream.chat.android.ui.viewmodel.messages.ChannelViewModelFactory import io.getstream.chat.android.ui.viewmodel.messages.bindView import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest @@ -32,18 +32,20 @@ class ChannelActivity3 : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // Step 0 - inflate binding + // inflate binding binding = ActivityChannel3Binding.inflate(layoutInflater) setContentView(binding.root) + binding.root.applySystemBarInsetsAsPadding() + val cid = checkNotNull(intent.getStringExtra(CID_KEY)) { "Specifying a channel id is required when starting ChannelActivity3" } - // Step 1 - Create three separate ViewModels for the views so it's easy - // to customize them individually - val factory = MessageListViewModelFactory(this, cid) - val messageListHeaderViewModel: MessageListHeaderViewModel by viewModels { factory } + // Create three separate ViewModels for the views so it's easy + // to customize them individually + val factory = ChannelViewModelFactory(this, cid) + val channelHeaderViewModel: ChannelHeaderViewModel by viewModels { factory } val messageListViewModel: MessageListViewModel by viewModels { factory } val messageComposerViewModel: MessageComposerViewModel by viewModels { factory } @@ -52,43 +54,43 @@ class ChannelActivity3 : AppCompatActivity() { val attachmentViewFactory = AttachmentFactoryManager(listOf(imgurAttachmentViewFactory)) binding.messageListView.setAttachmentFactoryManager(attachmentViewFactory) - // Step 2 - Bind the view and ViewModels, they are loosely coupled so it's easy to customize - messageListHeaderViewModel.bindView(binding.messageListHeaderView, this) + // Bind the view and ViewModels, they are loosely coupled so it's easy to customize + channelHeaderViewModel.bindView(binding.channelHeaderView, this) messageListViewModel.bindView(binding.messageListView, this) messageComposerViewModel.bindView(binding.messageComposerView, this) - // Step 3 - Let both MessageListHeaderView and MessageComposerView know when we open a thread + // Let both ChannelHeaderView and MessageComposerView know when we open a thread messageListViewModel.mode.observe(this) { mode -> when (mode) { is MessageMode.MessageThread -> { - messageListHeaderViewModel.setActiveThread(mode.parentMessage) + channelHeaderViewModel.setActiveThread(mode.parentMessage) messageComposerViewModel.setMessageMode(MessageMode.MessageThread(mode.parentMessage)) } is MessageMode.Normal -> { - messageListHeaderViewModel.resetThread() + channelHeaderViewModel.resetThread() messageComposerViewModel.leaveThread() } } } - // Step 4 - Let the message input know when we are editing a message + // Let the message input know when we are editing a message binding.messageListView.setMessageEditHandler { message -> messageComposerViewModel.performMessageAction(Edit(message)) } - // Step 5 - Handle navigate up state + // Handle navigate up state messageListViewModel.state.observe(this) { state -> if (state is MessageListViewModel.State.NavigateUp) { finish() } } - // Step 6 - Handle back button behaviour correctly when you're in a thread + // Handle back button behaviour correctly when you're in a thread val backHandler = { messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed) } - binding.messageListHeaderView.setBackButtonClickListener(backHandler) + binding.channelHeaderView.setBackButtonClickListener(backHandler) onBackPressedDispatcher.addCallback(this) { backHandler() } diff --git a/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity4.kt b/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity4.kt index 8d810cc..f54bdf4 100644 --- a/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity4.kt +++ b/samplekotlin/src/main/java/com/example/chattutorial/ChannelActivity4.kt @@ -16,9 +16,9 @@ import io.getstream.chat.android.ui.common.state.messages.Edit import io.getstream.chat.android.ui.common.state.messages.MessageMode import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.attachment.AttachmentFactoryManager import io.getstream.chat.android.ui.viewmodel.messages.MessageComposerViewModel -import io.getstream.chat.android.ui.viewmodel.messages.MessageListHeaderViewModel +import io.getstream.chat.android.ui.viewmodel.messages.ChannelHeaderViewModel import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModel -import io.getstream.chat.android.ui.viewmodel.messages.MessageListViewModelFactory +import io.getstream.chat.android.ui.viewmodel.messages.ChannelViewModelFactory import io.getstream.chat.android.ui.viewmodel.messages.bindView class ChannelActivity4 : AppCompatActivity() { @@ -28,18 +28,20 @@ class ChannelActivity4 : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // Step 0 - inflate binding + // inflate binding binding = ActivityChannel4Binding.inflate(layoutInflater) setContentView(binding.root) + binding.root.applySystemBarInsetsAsPadding() + val cid = checkNotNull(intent.getStringExtra(CID_KEY)) { "Specifying a channel id is required when starting ChannelActivity4" } - // Step 1 - Create three separate ViewModels for the views so it's easy - // to customize them individually - val factory = MessageListViewModelFactory(this, cid) - val messageListHeaderViewModel: MessageListHeaderViewModel by viewModels { factory } + // Create three separate ViewModels for the views so it's easy + // to customize them individually + val factory = ChannelViewModelFactory(this, cid) + val channelHeaderViewModel: ChannelHeaderViewModel by viewModels { factory } val messageListViewModel: MessageListViewModel by viewModels { factory } val messageComposerViewModel: MessageComposerViewModel by viewModels { factory } @@ -48,43 +50,43 @@ class ChannelActivity4 : AppCompatActivity() { val attachmentViewFactory = AttachmentFactoryManager(listOf(imgurAttachmentViewFactory)) binding.messageListView.setAttachmentFactoryManager(attachmentViewFactory) - // Step 2 - Bind the view and ViewModels, they are loosely coupled so it's easy to customize - messageListHeaderViewModel.bindView(binding.messageListHeaderView, this) + // Bind the view and ViewModels, they are loosely coupled so it's easy to customize + channelHeaderViewModel.bindView(binding.channelHeaderView, this) messageListViewModel.bindView(binding.messageListView, this) messageComposerViewModel.bindView(binding.messageComposerView, this) - // Step 3 - Let both MessageListHeaderView and MessageComposerView know when we open a thread + // Let both ChannelHeaderView and MessageComposerView know when we open a thread messageListViewModel.mode.observe(this) { mode -> when (mode) { is MessageMode.MessageThread -> { - messageListHeaderViewModel.setActiveThread(mode.parentMessage) + channelHeaderViewModel.setActiveThread(mode.parentMessage) messageComposerViewModel.setMessageMode(MessageMode.MessageThread(mode.parentMessage)) } is MessageMode.Normal -> { - messageListHeaderViewModel.resetThread() + channelHeaderViewModel.resetThread() messageComposerViewModel.leaveThread() } } } - // Step 4 - Let the message input know when we are editing a message + // Let the message input know when we are editing a message binding.messageListView.setMessageEditHandler { message -> messageComposerViewModel.performMessageAction(Edit(message)) } - // Step 5 - Handle navigate up state + // Handle navigate up state messageListViewModel.state.observe(this) { state -> if (state is MessageListViewModel.State.NavigateUp) { finish() } } - // Step 6 - Handle back button behaviour correctly when you're in a thread + // Handle back button behaviour correctly when you're in a thread val backHandler = { messageListViewModel.onEvent(MessageListViewModel.Event.BackButtonPressed) } - binding.messageListHeaderView.setBackButtonClickListener(backHandler) + binding.channelHeaderView.setBackButtonClickListener(backHandler) onBackPressedDispatcher.addCallback(this) { backHandler() } diff --git a/samplekotlin/src/main/java/com/example/chattutorial/EdgeToEdge.kt b/samplekotlin/src/main/java/com/example/chattutorial/EdgeToEdge.kt new file mode 100644 index 0000000..c2f63a9 --- /dev/null +++ b/samplekotlin/src/main/java/com/example/chattutorial/EdgeToEdge.kt @@ -0,0 +1,16 @@ +package com.example.chattutorial + +import android.view.View +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding + +fun View.applySystemBarInsetsAsPadding() { + ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets -> + val insets = windowInsets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime() + ) + view.updatePadding(insets.left, insets.top, insets.right, insets.bottom) + WindowInsetsCompat.CONSUMED + } +} diff --git a/samplekotlin/src/main/java/com/example/chattutorial/ImgurAttachmentFactory.kt b/samplekotlin/src/main/java/com/example/chattutorial/ImgurAttachmentFactory.kt index d1c9d33..2559583 100644 --- a/samplekotlin/src/main/java/com/example/chattutorial/ImgurAttachmentFactory.kt +++ b/samplekotlin/src/main/java/com/example/chattutorial/ImgurAttachmentFactory.kt @@ -2,7 +2,10 @@ package com.example.chattutorial import android.view.LayoutInflater import android.view.ViewGroup -import coil.load +import coil3.load +import coil3.request.allowHardware +import coil3.request.crossfade +import coil3.request.placeholder import com.example.chattutorial.databinding.AttachmentImgurBinding import io.getstream.chat.android.models.Attachment import io.getstream.chat.android.models.Message @@ -13,13 +16,13 @@ import io.getstream.chat.android.ui.feature.messages.list.adapter.viewholder.att /** A custom attachment factory to show an imgur logo if the attachment URL is an imgur image. */ class ImgurAttachmentFactory : AttachmentFactory { - // Step 1 - Check whether the message contains an Imgur attachment + // Check whether the message contains an Imgur attachment override fun canHandle(message: Message): Boolean { val imgurAttachment = message.attachments.firstOrNull { it.isImgurAttachment() } return imgurAttachment != null } - // Step 2 - Create the ViewHolder that will be used to display the Imgur logo + // Create the ViewHolder that will be used to display the Imgur logo // over Imgur attachments override fun createViewHolder( message: Message, diff --git a/samplekotlin/src/main/java/com/example/chattutorial/MainActivity.kt b/samplekotlin/src/main/java/com/example/chattutorial/MainActivity.kt index 2a63ab1..3f30901 100644 --- a/samplekotlin/src/main/java/com/example/chattutorial/MainActivity.kt +++ b/samplekotlin/src/main/java/com/example/chattutorial/MainActivity.kt @@ -9,9 +9,6 @@ import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.logger.ChatLogLevel import io.getstream.chat.android.models.Filters import io.getstream.chat.android.models.User -import io.getstream.chat.android.offline.plugin.factory.StreamOfflinePluginFactory -import io.getstream.chat.android.state.plugin.config.StatePluginConfig -import io.getstream.chat.android.state.plugin.factory.StreamStatePluginFactory import io.getstream.chat.android.ui.viewmodel.channels.ChannelListViewModel import io.getstream.chat.android.ui.viewmodel.channels.ChannelListViewModelFactory import io.getstream.chat.android.ui.viewmodel.channels.bindView @@ -23,27 +20,18 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // Step 0 - inflate binding + // Inflate binding for layout binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - // Step 1 - Set up the OfflinePlugin for offline storage - val offlinePluginFactory = StreamOfflinePluginFactory(appContext = this) - val statePluginFactory = StreamStatePluginFactory( - config = StatePluginConfig( - backgroundSyncEnabled = true, - userPresence = true, - ), - appContext = this, - ) + binding.root.applySystemBarInsetsAsPadding() - // Step 2 - Set up the client for API calls with the plugin for offline storage + // Set up the client for API calls with offline storage and state management val client = ChatClient.Builder("uun7ywwamhs9", applicationContext) - .withPlugins(offlinePluginFactory, statePluginFactory) .logLevel(ChatLogLevel.ALL) // Set to NOTHING in prod .build() - // Step 3 - Authenticate and connect the user + // Authenticate and connect the user val user = User( id = "tutorial-droid", name = "Tutorial Droid", @@ -54,7 +42,7 @@ class MainActivity : AppCompatActivity() { token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidHV0b3JpYWwtZHJvaWQifQ.WwfBzU1GZr0brt_fXnqKdKhz3oj0rbDUm2DqJO_SS5U" ).enqueue { if (it.isSuccess) { - // Step 4 - Set the channel list filter and order + // Set the channel list filter and order // This can be read as requiring only channels whose "type" is "messaging" AND // whose "members" include our "user.id" val filter = Filters.and( @@ -65,8 +53,8 @@ class MainActivity : AppCompatActivity() { ChannelListViewModelFactory(filter, ChannelListViewModel.DEFAULT_SORT) val viewModel: ChannelListViewModel by viewModels { viewModelFactory } - // Step 5 - Connect the ChannelListViewModel to the ChannelListView, loose - // coupling makes it easy to customize + // Connect the ChannelListViewModel to the ChannelListView, loose + // coupling makes it easy to customize viewModel.bindView(binding.channelListView, this) binding.channelListView.setChannelItemClickListener { channel -> startActivity(ChannelActivity4.newIntent(this, channel)) diff --git a/samplekotlin/src/main/res/layout/activity_channel.xml b/samplekotlin/src/main/res/layout/activity_channel.xml index db1fa0d..9d2185a 100644 --- a/samplekotlin/src/main/res/layout/activity_channel.xml +++ b/samplekotlin/src/main/res/layout/activity_channel.xml @@ -4,8 +4,8 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> - + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> - + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> - + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> + app:layout_constraintTop_toBottomOf="@+id/channelHeaderView" /> @color/black ?attr/colorPrimaryVariant + true + true