diff --git a/buildSrc/src/main/java/com/pubnub/components/buildsrc/Dependencies.kt b/buildSrc/src/main/java/com/pubnub/components/buildsrc/Dependencies.kt
index 9998ad8..20dc87e 100644
--- a/buildSrc/src/main/java/com/pubnub/components/buildsrc/Dependencies.kt
+++ b/buildSrc/src/main/java/com/pubnub/components/buildsrc/Dependencies.kt
@@ -73,6 +73,8 @@ object Libs {
const val appcompat = "androidx.appcompat:appcompat:1.4.2"
+ const val fragment = "androidx.fragment:fragment-ktx:1.5.5"
+
const val navigation = "androidx.navigation:navigation-compose:2.4.0"
const val splashscreen = "androidx.core:core-splashscreen:1.0.0"
@@ -195,6 +197,10 @@ object Libs {
private const val version = "2.9.0"
const val retrofit = "com.squareup.retrofit2:retrofit:$version"
}
+
+ object Github {
+ const val faker = "io.github.serpro69:kotlin-faker:1.12.0"
+ }
}
object Urls {
diff --git a/gradle.properties b/gradle.properties
index 9285846..5c0eb48 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -25,4 +25,7 @@ android.enableR8.fullMode=true
org.gradle.unsafe.configuration-cache=false
# PubNub keys
PUBNUB_PUBLISH_KEY="myPublishKey"
-PUBNUB_SUBSCRIBE_KEY="mySubscribeKey"
\ No newline at end of file
+PUBNUB_SUBSCRIBE_KEY="mySubscribeKey"
+# YouTube key
+# https://developers.google.com/youtube/android/player/register
+YOUTUBE_KEY="muYoutubeKey"
\ No newline at end of file
diff --git a/live-event/.gitignore b/live-event/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/live-event/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/live-event/.idea/.gitignore b/live-event/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/live-event/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/live-event/.idea/gradle.xml b/live-event/.idea/gradle.xml
new file mode 100644
index 0000000..630989d
--- /dev/null
+++ b/live-event/.idea/gradle.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/live-event/.idea/misc.xml b/live-event/.idea/misc.xml
new file mode 100644
index 0000000..471f0ee
--- /dev/null
+++ b/live-event/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/live-event/.idea/modules.xml b/live-event/.idea/modules.xml
new file mode 100644
index 0000000..9f1b555
--- /dev/null
+++ b/live-event/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/live-event/README.md b/live-event/README.md
new file mode 100644
index 0000000..0398617
--- /dev/null
+++ b/live-event/README.md
@@ -0,0 +1,21 @@
+# Live Event Chat
+
+
+`live-events` is an app that simulates live events. You can switch between multiple channels, send messages, view channel occupancy and the participant list. The app comes with the light and dark themes.
+
+## Prerequisites
+
+This application uses [PubNub Kotlin SDK](https://github.com/pubnub/kotlin) (>= 7.3.2) for chat
+components and [Jetpack Compose](https://developer.android.com/jetpack/compose) as the UI Toolkit.
+
+To use the app, you need:
+
+* [Android Studio](https://developer.android.com/studio) (>= Dolphin 2021.3.1)
+* PubNub account on [Admin Portal](https://dashboard.pubnub.com/)
+
+## Features
+
+The `live-event` app showcases these components and features:
+
+* [MessageInput](https://www.pubnub.com/docs/chat/components/android/ui-components#messageinput)
+* [MessageList](https://www.pubnub.com/docs/chat/components/android/ui-components#messagelist)
diff --git a/live-event/build.gradle.kts b/live-event/build.gradle.kts
new file mode 100644
index 0000000..b495d8f
--- /dev/null
+++ b/live-event/build.gradle.kts
@@ -0,0 +1,85 @@
+import com.pubnub.components.buildsrc.Libs
+
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+}
+
+android {
+ namespace = "com.pubnub.components.example.live_event"
+ compileSdk = Libs.Build.Android.compileSdk
+
+ defaultConfig {
+ applicationId = "com.pubnub.components.example.live_event"
+ minSdk = Libs.Build.Android.minSdk
+ targetSdk = Libs.Build.Android.targetSdk
+ versionCode = Libs.Build.Android.versionCode
+ versionName = Libs.Build.Android.versionName
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ vectorDrawables {
+ useSupportLibrary = true
+ }
+
+ buildConfigField("String", "PUBLISH_KEY", project.property("PUBNUB_PUBLISH_KEY") as String)
+ buildConfigField(
+ "String",
+ "SUBSCRIBE_KEY",
+ project.property("PUBNUB_SUBSCRIBE_KEY") as String
+ )
+ buildConfigField("String", "YOUTUBE_KEY", project.property("YOUTUBE_KEY") as String)
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = Libs.Build.Kotlin.jvmTarget
+ }
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = Libs.AndroidX.Compose.compilerVersion
+ }
+}
+
+dependencies {
+ implementation(fileTree("libs") { include("*.jar") })
+ implementation(Libs.PubNub.Components.chat)
+ api(platform(Libs.PubNub.bom))
+ implementation(Libs.PubNub.kotlin)
+ implementation(Libs.PubNub.memberships)
+
+ implementation(Libs.AndroidX.core)
+ implementation(Libs.AndroidX.appcompat)
+ implementation(Libs.AndroidX.fragment)
+ implementation(platform(Libs.AndroidX.Compose.bom))
+ implementation(Libs.AndroidX.Compose.ui)
+ implementation(Libs.AndroidX.Compose.material)
+ implementation(Libs.AndroidX.Compose.toolingPreview)
+ implementation(Libs.AndroidX.Lifecycle.runtime)
+ implementation(Libs.AndroidX.Activity.activityCompose)
+ implementation(Libs.Coil.coil)
+ implementation(Libs.JakeWharton.timber)
+ implementation(Libs.Accompanist.navigation)
+ implementation(Libs.Accompanist.placeholder)
+ implementation(Libs.Github.faker)
+
+ testImplementation(Libs.JUnit.junit)
+ androidTestImplementation(platform(Libs.AndroidX.Compose.bom))
+ androidTestImplementation(Libs.AndroidX.Test.Ext.junit)
+ androidTestImplementation(Libs.AndroidX.Test.espressoCore)
+ androidTestImplementation(Libs.AndroidX.Compose.uiTest)
+ debugImplementation(Libs.AndroidX.Compose.tooling)
+}
diff --git a/live-event/gradle/wrapper/gradle-wrapper.jar b/live-event/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7454180
Binary files /dev/null and b/live-event/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/live-event/gradle/wrapper/gradle-wrapper.properties b/live-event/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e750102
--- /dev/null
+++ b/live-event/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/live-event/gradlew b/live-event/gradlew
new file mode 100644
index 0000000..e69de29
diff --git a/live-event/gradlew.bat b/live-event/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/live-event/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/live-event/libs/YouTubeAndroidPlayerApi.jar b/live-event/libs/YouTubeAndroidPlayerApi.jar
new file mode 100644
index 0000000..0acbebd
Binary files /dev/null and b/live-event/libs/YouTubeAndroidPlayerApi.jar differ
diff --git a/live-event/proguard-rules.pro b/live-event/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/live-event/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/live-event/src/main/AndroidManifest.xml b/live-event/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3b5b62e
--- /dev/null
+++ b/live-event/src/main/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/live-event/src/main/java/com/google/android/youtube/player/YouTubePlayerSupportFragmentXKt.kt b/live-event/src/main/java/com/google/android/youtube/player/YouTubePlayerSupportFragmentXKt.kt
new file mode 100644
index 0000000..6074010
--- /dev/null
+++ b/live-event/src/main/java/com/google/android/youtube/player/YouTubePlayerSupportFragmentXKt.kt
@@ -0,0 +1,121 @@
+package com.google.android.youtube.player
+
+import android.os.Bundle
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import com.google.android.youtube.player.internal.ab
+
+// source : https://gist.github.com/medyo/f226b967213c3b8ec6f6bebb5338a492#file-youtubeplayersupportfragmentx-java
+
+open class YouTubePlayerSupportFragmentXKt : Fragment(), YouTubePlayer.Provider {
+
+ private val a: A = A()
+
+ private var b: Bundle? = null
+
+ private var c: YouTubePlayerView? = null
+
+ private var d: String? = null
+
+ private var e: YouTubePlayer.OnInitializedListener? = null
+
+ private val f = false
+
+ @Suppress("DEPRECATION")
+ private fun a() {
+ if (c != null && e != null) {
+ c?.a(f)
+ c?.a(this.activity, this, d, e, b)
+ b = null
+ e = null
+ }
+ }
+
+ override fun initialize(p0: String?, p1: YouTubePlayer.OnInitializedListener?) {
+ this.d = ab.a(p0, "Developer key cannot be null or empty")
+ this.e = p1
+ this.a()
+ }
+
+ override fun onCreate(var1: Bundle?) {
+ super.onCreate(var1)
+ b = var1?.getBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE")
+ }
+
+ override fun onCreateView(var1: LayoutInflater, var2: ViewGroup?, var3: Bundle?): View? {
+ c = YouTubePlayerView(this.activity, null as AttributeSet?, 0, a)
+ this.a()
+ return c
+ }
+
+ override fun onStart() {
+ super.onStart()
+ c?.a()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ c?.b()
+ }
+
+ override fun onPause() {
+ c?.c()
+ super.onPause()
+ }
+
+ override fun onSaveInstanceState(var1: Bundle) {
+ super.onSaveInstanceState(var1)
+ val var2 = if (c != null && c is YouTubePlayerView) {
+ (c as YouTubePlayerView).e()
+ } else b
+ var1.putBundle("YouTubePlayerSupportFragment.KEY_PLAYER_VIEW_STATE", var2)
+ }
+
+ override fun onStop() {
+ c?.d()
+ super.onStop()
+ }
+
+ override fun onDestroyView() {
+ this.activity?.let {
+ c?.c(it.isFinishing)
+ c = null
+ }
+
+ super.onDestroyView()
+ }
+
+ override fun onDestroy() {
+ if (c != null && c is YouTubePlayerView) {
+ val var1 = this.activity
+ (c as YouTubePlayerView).b(var1 == null || var1.isFinishing)
+ }
+ super.onDestroy()
+ }
+
+ companion object {
+ fun newInstance(): YouTubePlayerSupportFragmentXKt {
+ return YouTubePlayerSupportFragmentXKt()
+ }
+ }
+
+ private class A : YouTubePlayerView.b {
+
+ override fun a(
+ p0: YouTubePlayerView?,
+ p1: String?,
+ p2: YouTubePlayer.OnInitializedListener?
+ ) {
+ val fragment = newInstance()
+ fragment.initialize(p1, fragment.e)
+ }
+
+ override fun a(p0: YouTubePlayerView?) {
+ // do nothing
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/LiveEventApplication.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/LiveEventApplication.kt
new file mode 100644
index 0000000..898e35b
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/LiveEventApplication.kt
@@ -0,0 +1,95 @@
+package com.pubnub.components.example.live_event
+
+import android.app.Application
+import android.util.Log
+import androidx.room.RoomDatabase
+import androidx.sqlite.db.SupportSQLiteDatabase
+import com.pubnub.components.DefaultDatabase
+import com.pubnub.components.data.Database
+import com.pubnub.components.data.channel.DBChannel
+import com.pubnub.components.data.member.DBMember
+import com.pubnub.components.data.membership.DBMembership
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+class LiveEventApplication : Application() {
+
+ companion object {
+ lateinit var database: DefaultDatabase
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ database = Database.initialize(applicationContext) { it.prepopulate() }
+ }
+
+ private fun RoomDatabase.Builder.prepopulate(): RoomDatabase.Builder =
+ addCallback(
+ object : RoomDatabase.Callback() {
+ override fun onOpen(db: SupportSQLiteDatabase) {
+ super.onOpen(db)
+ prepopulateMembers()
+ }
+
+ override fun onCreate(db: SupportSQLiteDatabase) {
+ super.onCreate(db)
+ prepopulateChannels()
+ }
+ }
+ )
+
+ @OptIn(DelicateCoroutinesApi::class)
+ private fun prepopulateMembers() {
+ Log.e("TAG", "Database prepopulate members")
+ // insert the data on the IO Thread
+ GlobalScope.launch(Dispatchers.IO) {
+ with(database) {
+ val channelArray = arrayOf(Settings.channelId)
+
+ // Creates user objects with uuid
+ val members = arrayOf(
+ DBMember(
+ id = Settings.userId,
+ name = Settings.userId,
+ profileUrl = "https://picsum.photos/seed/${Settings.userId}/200"
+ )
+ )
+
+ // Creates a membership so that the user could subscribe to channels
+ val memberships: Array =
+ channelArray.flatMap { channel ->
+ members.map { member ->
+ DBMembership(
+ channelId = channel,
+ memberId = member.id
+ )
+ }
+ }.toTypedArray()
+
+ // Fills the database with member and memberships data
+ memberDao().insertOrUpdate(*members)
+ membershipDao().insertOrUpdate(*memberships)
+ }
+ }
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ private fun prepopulateChannels() {
+ Log.e("TAG", "Database prepopulate channels")
+ // insert the data on the IO Thread
+ GlobalScope.launch(Dispatchers.IO) {
+ with(database) {
+ val channelArray = arrayOf(Settings.channelId)
+
+ // Fills the database with channels data
+ val channels: Array =
+ channelArray.map { id -> DBChannel(id, "Channel #$id") }
+ .toTypedArray()
+
+ channelDao().insertOrUpdate(*channels)
+ }
+ }
+ }
+}
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/MainActivity.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/MainActivity.kt
new file mode 100644
index 0000000..3d75f32
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/MainActivity.kt
@@ -0,0 +1,64 @@
+package com.pubnub.components.example.live_event
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.core.view.WindowCompat
+import com.google.accompanist.navigation.animation.AnimatedNavHost
+import com.google.accompanist.navigation.animation.composable
+import com.google.accompanist.navigation.animation.rememberAnimatedNavController
+import com.pubnub.api.PNConfiguration
+import com.pubnub.api.PubNub
+import com.pubnub.api.UserId
+import com.pubnub.api.enums.PNLogVerbosity
+import com.pubnub.components.example.live_event.ui.navigation.Screen
+import com.pubnub.components.example.live_event.ui.theme.AppTheme
+import com.pubnub.components.example.live_event.ui.view.Chat
+
+@OptIn(ExperimentalAnimationApi::class)
+class MainActivity : AppCompatActivity() {
+
+ private lateinit var pubNub: PubNub
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ initializePubNub()
+
+ setContent {
+ val navController = rememberAnimatedNavController()
+ AppTheme(pubNub = pubNub, database = LiveEventApplication.database) {
+ AnimatedNavHost(navController, startDestination = Screen.Channel.route) {
+ composable(
+ route = Screen.Channel.route,
+ arguments = Screen.Channel.arguments,
+ ) { navBackStackEntry ->
+ val channelId = navBackStackEntry.arguments?.getString("channelId")
+ ?: Settings.channelId
+ Chat.View(channelId)
+ }
+ }
+ }
+ }
+ }
+
+ override fun onDestroy() {
+ destroyPubNub()
+ super.onDestroy()
+ }
+
+ private fun initializePubNub() {
+ pubNub = PubNub(
+ PNConfiguration(UserId(Settings.userId)).apply {
+ publishKey = BuildConfig.PUBLISH_KEY
+ subscribeKey = BuildConfig.SUBSCRIBE_KEY
+ logVerbosity = PNLogVerbosity.BODY
+ }
+ )
+ }
+
+ private fun destroyPubNub() {
+ pubNub.destroy()
+ }
+}
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/Settings.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/Settings.kt
new file mode 100644
index 0000000..90832cc
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/Settings.kt
@@ -0,0 +1,11 @@
+package com.pubnub.components.example.live_event
+
+import com.pubnub.framework.data.ChannelId
+import com.pubnub.framework.data.UserId
+import io.github.serpro69.kfaker.Faker
+
+object Settings {
+ private val faker: Faker = Faker()
+ const val channelId: ChannelId = "Default"
+ val userId: UserId = faker.name.name().replace(' ', '_')
+}
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/navigation/Screen.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/navigation/Screen.kt
new file mode 100644
index 0000000..1e977f9
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/navigation/Screen.kt
@@ -0,0 +1,17 @@
+package com.pubnub.components.example.live_event.ui.navigation
+
+import androidx.navigation.NamedNavArgument
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+
+sealed class Screen(
+ val route: String,
+ val arguments: List = emptyList(),
+) {
+ object Channel : Screen(
+ route = "channel/{channelId}",
+ arguments = listOf(navArgument("channelId") { type = NavType.StringType }),
+ ) {
+ fun createRoute(channelId: String) = "channel/$channelId"
+ }
+}
\ No newline at end of file
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/renderer/EventMessageRenderer.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/renderer/EventMessageRenderer.kt
new file mode 100644
index 0000000..55a432b
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/renderer/EventMessageRenderer.kt
@@ -0,0 +1,225 @@
+package com.pubnub.components.example.live_event.ui.renderer
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.LocalIndication
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.indication
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.LocalContentAlpha
+import androidx.compose.material.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.tooling.preview.Preview
+import com.google.accompanist.placeholder.placeholder
+import com.pubnub.components.chat.provider.PubNubPreview
+import com.pubnub.components.chat.ui.component.member.ProfileImage
+import com.pubnub.components.chat.ui.component.message.LocalMessageListTheme
+import com.pubnub.components.chat.ui.component.message.messageFormatter
+import com.pubnub.components.chat.ui.component.message.reaction.Reaction
+import com.pubnub.components.chat.ui.component.message.reaction.ReactionUi
+import com.pubnub.components.chat.ui.component.message.reaction.renderer.DefaultReactionsPickerRenderer
+import com.pubnub.components.chat.ui.component.message.reaction.renderer.ReactionsRenderer
+import com.pubnub.components.chat.ui.component.message.renderer.GroupMessageRenderer
+import com.pubnub.components.chat.ui.component.message.renderer.MessageRenderer
+import com.pubnub.components.example.live_event.ui.util.InteractionHelper
+import com.pubnub.framework.data.MessageId
+import com.pubnub.framework.data.UserId
+import com.pubnub.framework.util.Timetoken
+import kotlinx.coroutines.DelicateCoroutinesApi
+import java.util.*
+
+@OptIn(
+ ExperimentalFoundationApi::class,
+ DelicateCoroutinesApi::class
+)
+
+object EventMessageRenderer : MessageRenderer {
+
+ @Composable
+ override fun Message(
+ messageId: MessageId,
+ currentUserId: UserId,
+ userId: UserId,
+ profileUrl: String,
+ online: Boolean?,
+ title: String,
+ message: AnnotatedString?,
+ timetoken: Timetoken,
+ reactions: List,
+ onMessageSelected: (() -> Unit)?,
+ onReactionSelected: ((Reaction) -> Unit)?,
+ reactionsPickerRenderer: ReactionsRenderer,
+ ) {
+ ChatMessage(
+ currentUserId = currentUserId,
+ userId = userId,
+ profileUrl = profileUrl,
+ online = online,
+ title = title,
+ message = message,
+ timetoken = timetoken,
+ reactions = reactions,
+ onMessageSelected = onMessageSelected?.let { { it() } },
+ onReactionSelected = onReactionSelected,
+ reactionsPicker = reactionsPickerRenderer,
+ )
+ }
+
+ @Composable
+ override fun Placeholder() {
+ MessagePlaceholder()
+ }
+
+ @Composable
+ override fun Separator(text: String) {
+ }
+
+ @Composable
+ fun ChatMessage(
+ currentUserId: UserId,
+ userId: UserId,
+ profileUrl: String?,
+ online: Boolean?,
+ @Suppress("UNUSED_PARAMETER") title: String,
+ message: AnnotatedString?,
+ @Suppress("UNUSED_PARAMETER") timetoken: Timetoken,
+ placeholder: Boolean = false,
+ @Suppress("UNUSED_PARAMETER") reactions: List = emptyList(),
+ onMessageSelected: (() -> Unit)? = null,
+ @Suppress("UNUSED_PARAMETER") onReactionSelected: ((Reaction) -> Unit)? = null,
+ @Suppress("UNUSED_PARAMETER") reactionsPicker: ReactionsRenderer = DefaultReactionsPickerRenderer,
+ ) {
+ val theme = if (currentUserId == userId) LocalMessageListTheme.current.message
+ else LocalMessageListTheme.current.messageOwn
+
+ // region Placeholders
+ val messagePlaceholder = Modifier.placeholder(
+ visible = placeholder,
+ color = Color.LightGray,
+ shape = theme.shape.shape,
+ ).let { if (placeholder) it.fillMaxWidth(0.8f) else it }
+
+ val imagePlaceholder = Modifier.placeholder(
+ visible = placeholder,
+ color = Color.LightGray,
+ shape = CircleShape,
+ )
+ // endregion
+
+ // Ripple region
+ val interactionSource = remember { MutableInteractionSource() }
+ val interactionHelper = remember { InteractionHelper(interactionSource) }
+ // endregion
+
+ val localFocusManager = LocalFocusManager.current
+
+ Row(
+ modifier = Modifier
+ .indication(interactionSource, LocalIndication.current)
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onPress = { offset ->
+ localFocusManager.clearFocus()
+ interactionHelper.emitPressEvent(this, offset)
+ },
+ onLongPress = { offset ->
+ onMessageSelected?.let { onMessageSelected() }
+ interactionHelper.emitReleaseEvent(offset)
+ },
+ )
+ }
+ .then(theme.modifier),
+ verticalAlignment = theme.verticalAlignment,
+ ) {
+ Box(modifier = theme.profileImage.modifier) {
+ ProfileImage(
+ modifier = imagePlaceholder,
+ imageUrl = profileUrl,
+ isOnline = online,
+ )
+ }
+
+ Column {
+ val body: @Composable() () -> Unit = {
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
+
+ if (message != null && message.isNotBlank()) {
+ GroupMessageRenderer.ChatText(
+ message = message,
+ theme = theme.text,
+ placeholderModifier = theme.text.modifier.then(
+ messagePlaceholder.then(
+ Modifier.padding(
+ theme.shape.padding
+ )
+ )
+ ),
+ modifier = theme.text.modifier,
+ onLongPress = { offset ->
+ onMessageSelected?.let { onMessageSelected() }
+ interactionHelper.emitReleaseEvent(offset)
+ },
+ onPress = { offset ->
+ localFocusManager.clearFocus()
+ interactionHelper.emitPressEvent(this, offset)
+ }
+ )
+ }
+ }
+ }
+
+ // workaround for placeholders...
+ if (placeholder) body()
+ else Surface(
+ color = theme.shape.tint,
+ shape = theme.shape.shape,
+ modifier = theme.shape.modifier
+ ) { body() }
+ }
+ }
+ }
+
+ @Composable
+ fun MessagePlaceholder(
+ ) {
+ ChatMessage(
+ currentUserId = "a",
+ userId = "b",
+ profileUrl = "",
+ online = false,
+ title = "Lorem ipsum dolor",
+ message = messageFormatter(text = "Test message"),
+ timetoken = 0L,
+ placeholder = true,
+ reactionsPicker = DefaultReactionsPickerRenderer,
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun MessagePreview(
+) {
+ PubNubPreview {
+ EventMessageRenderer.ChatMessage(
+ currentUserId = "a",
+ userId = "b",
+ profileUrl = "",
+ online = false,
+ title = "Lorem ipsum dolor",
+ message = messageFormatter(text = "Test message"),
+ timetoken = 0L,
+ placeholder = true,
+ reactionsPicker = DefaultReactionsPickerRenderer,
+ )
+ }
+}
\ No newline at end of file
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/renderer/MessageListWorkaround.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/renderer/MessageListWorkaround.kt
new file mode 100644
index 0000000..d5f3164
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/renderer/MessageListWorkaround.kt
@@ -0,0 +1,80 @@
+package com.pubnub.components.example.live_event.ui.renderer
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyItemScope
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.paging.PagingData
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.collectAsLazyPagingItems
+import androidx.paging.compose.items
+import com.pubnub.components.chat.ui.component.menu.React
+import com.pubnub.components.chat.ui.component.message.LocalMessageListTheme
+import com.pubnub.components.chat.ui.component.message.MessageListContent
+import com.pubnub.components.chat.ui.component.message.MessageUi
+import com.pubnub.components.chat.ui.component.message.reaction.renderer.DefaultReactionsPickerRenderer
+import com.pubnub.components.chat.ui.component.presence.Presence
+import com.pubnub.components.chat.ui.component.provider.LocalUser
+import com.pubnub.components.example.live_event.R
+import com.pubnub.framework.data.UserId
+import kotlinx.coroutines.flow.Flow
+
+@Composable
+fun MessageList(
+ messages: Flow>,
+ modifier: Modifier = Modifier,
+ presence: Presence? = null,
+ onMessageSelected: ((MessageUi.Data) -> Unit)? = null,
+ onReactionSelected: ((React) -> Unit)? = null,
+ useStickyHeader: Boolean = false,
+ headerContent: @Composable LazyItemScope.() -> Unit = {},
+ footerContent: @Composable LazyItemScope.() -> Unit = {},
+ itemContent: @Composable LazyListScope.(MessageUi?, UserId) -> Unit = { message, currentUser ->
+ MessageListContent(
+ message,
+ currentUser,
+ EventMessageRenderer,
+ DefaultReactionsPickerRenderer,
+ useStickyHeader,
+ presence,
+ onMessageSelected,
+ onReactionSelected
+ )
+ },
+) {
+
+ val theme = LocalMessageListTheme.current
+ val context = LocalContext.current
+ val currentUser = LocalUser.current
+
+ val lazyMessages: LazyPagingItems = messages.collectAsLazyPagingItems()
+
+ Box(modifier = modifier) {
+ val lazyListState =
+ rememberLazyListState(initialFirstVisibleItemIndex = lazyMessages.itemCount)
+
+ LazyColumn(
+ state = lazyListState,
+ reverseLayout = true,
+ verticalArrangement = theme.arrangement,
+ modifier = theme.modifier.semantics {
+ contentDescription = context.getString(R.string.message_list)
+ }) {
+ item {
+ headerContent()
+ }
+ items(lazyMessages) { message ->
+ this@LazyColumn.itemContent(message, currentUser)
+ }
+ item {
+ footerContent()
+ }
+ }
+ }
+}
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Color.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Color.kt
new file mode 100644
index 0000000..4d99c83
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Color.kt
@@ -0,0 +1,8 @@
+package com.pubnub.components.example.live_event.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Cultured = Color(0xFFF4F4F4)
+val PhilippineSilver = Color(0xFFB4B4B4)
+val Gunmetal = Color(0xFF2A2A39)
+val RaisinBlack = Color(0xFF1C1C28)
\ No newline at end of file
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Shape.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Shape.kt
new file mode 100644
index 0000000..6b7775b
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Shape.kt
@@ -0,0 +1,11 @@
+package com.pubnub.components.example.live_event.ui.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Shapes
+import androidx.compose.ui.unit.dp
+
+val Shapes = Shapes(
+ small = RoundedCornerShape(4.dp),
+ medium = RoundedCornerShape(4.dp),
+ large = RoundedCornerShape(0.dp)
+)
\ No newline at end of file
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Theme.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Theme.kt
new file mode 100644
index 0000000..6600d5b
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Theme.kt
@@ -0,0 +1,39 @@
+package com.pubnub.components.example.live_event.ui.theme
+
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import com.pubnub.api.PubNub
+import com.pubnub.components.DefaultDatabase
+import com.pubnub.components.asPubNub
+import com.pubnub.components.chat.provider.ChatProvider
+import com.pubnub.components.data.Database
+
+private val DarkColorPalette = darkColors(
+ primary = PhilippineSilver,
+ primaryVariant = RaisinBlack,
+ secondary = Cultured,
+ background = Gunmetal,
+ surface = RaisinBlack,
+)
+
+@Composable
+fun AppTheme(
+ pubNub: PubNub,
+ database: DefaultDatabase = Database.initialize(LocalContext.current),
+ content: @Composable() () -> Unit,
+) {
+ val colors = DarkColorPalette
+
+ MaterialTheme(
+ colors = colors,
+ typography = Typography,
+ shapes = Shapes,
+ ) {
+
+ ChatProvider(pubNub, database.asPubNub()) {
+ content()
+ }
+ }
+}
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Type.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Type.kt
new file mode 100644
index 0000000..b8c96c5
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/theme/Type.kt
@@ -0,0 +1,28 @@
+package com.pubnub.components.example.live_event.ui.theme
+
+import androidx.compose.material.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ body1 = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp
+ )
+ /* Other default text styles to override
+ button = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ fontSize = 14.sp
+ ),
+ caption = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/util/InteractionHelper.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/util/InteractionHelper.kt
new file mode 100644
index 0000000..0ee6cac
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/util/InteractionHelper.kt
@@ -0,0 +1,34 @@
+package com.pubnub.components.example.live_event.ui.util
+
+import androidx.compose.foundation.gestures.PressGestureScope
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.ui.geometry.Offset
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+
+@OptIn(DelicateCoroutinesApi::class)
+class InteractionHelper(
+ private val interactionSource: MutableInteractionSource,
+ private val coroutineScope: CoroutineScope = GlobalScope,
+) {
+
+ suspend fun emitPressEvent(scope: PressGestureScope, offset: Offset) {
+ val press = PressInteraction.Press(offset)
+ interactionSource.emit(press)
+ scope.tryAwaitRelease()
+ interactionSource.emit(PressInteraction.Release(press))
+ }
+
+ fun emitReleaseEvent(offset: Offset) {
+ coroutineScope.launch {
+ interactionSource.emit(
+ PressInteraction.Release(
+ PressInteraction.Press(offset)
+ )
+ )
+ }
+ }
+}
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/util/Keyboard.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/util/Keyboard.kt
new file mode 100644
index 0000000..e29dd74
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/util/Keyboard.kt
@@ -0,0 +1,28 @@
+package com.pubnub.components.example.live_event.ui.util
+
+import android.view.View
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.ime
+import androidx.compose.runtime.*
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+
+object Keyboard {
+
+ @Composable
+ fun asState(view: View = LocalView.current): State {
+ val keyboardState = remember { mutableStateOf(false) }
+ LaunchedEffect(view) {
+ ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
+ keyboardState.value = insets.isVisible(WindowInsetsCompat.Type.ime())
+ insets
+ }
+ }
+ return keyboardState
+ }
+
+ @Composable
+ fun isVisible(): Boolean = WindowInsets.ime.getBottom(LocalDensity.current) > 0
+}
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/Chat.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/Chat.kt
new file mode 100644
index 0000000..9ec0210
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/Chat.kt
@@ -0,0 +1,185 @@
+package com.pubnub.components.example.live_event.ui.view
+
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.paging.PagingData
+import com.pubnub.components.chat.ui.component.common.*
+import com.pubnub.components.chat.ui.component.input.LocalMessageInputTheme
+import com.pubnub.components.chat.ui.component.input.MessageInput
+import com.pubnub.components.chat.ui.component.menu.Copy
+import com.pubnub.components.chat.ui.component.menu.React
+import com.pubnub.components.chat.ui.component.message.LocalMessageListTheme
+import com.pubnub.components.chat.ui.component.message.MessageUi
+import com.pubnub.components.chat.ui.component.presence.Presence
+import com.pubnub.components.chat.ui.component.provider.LocalChannel
+import com.pubnub.components.chat.viewmodel.message.MessageViewModel
+import com.pubnub.components.example.live_event.R
+import com.pubnub.framework.data.ChannelId
+import kotlinx.coroutines.flow.Flow
+import java.util.*
+
+object Chat {
+
+ @Composable
+ internal fun Content(
+ messages: Flow>,
+ presence: Presence? = null,
+ onMessageSelected: (MessageUi.Data) -> Unit,
+ onReactionSelected: ((React) -> Unit)? = null,
+ ) {
+ val localFocusManager = LocalFocusManager.current
+ Column(
+ modifier = Modifier
+ .systemBarsPadding()
+ .imePadding()
+ .fillMaxSize()
+ .pointerInput(Unit) {
+ detectTapGestures(onTap = {
+ localFocusManager.clearFocus()
+ })
+ }
+ ) {
+ VideoContent()
+ Header()
+ com.pubnub.components.example.live_event.ui.renderer.MessageList(
+ messages = messages,
+ presence = presence,
+ onMessageSelected = onMessageSelected,
+ onReactionSelected = onReactionSelected,
+ modifier = Modifier
+ .fillMaxSize()
+ .weight(1f, true),
+ )
+ MessageInput(
+ typingIndicatorEnabled = true,
+ placeholder = stringResource(id = R.string.type_your_message)
+ )
+ }
+ }
+
+ @Composable
+ fun View(
+ channelId: ChannelId,
+ ) {
+ // region Content data
+ val messageViewModel: MessageViewModel = MessageViewModel.default()
+ val messages = remember(channelId) { messageViewModel.getAll(channelId) }
+ // endregion
+
+ var menuVisible by remember { mutableStateOf(false) }
+ var selectedMessage by remember { mutableStateOf(null) }
+
+ val onDismiss: () -> Unit = { menuVisible = false }
+ CompositionLocalProvider(
+ LocalChannel provides channelId,
+ LocalMessageListTheme provides EventMessageListTheme,
+ LocalMessageInputTheme provides EventMessageInputTheme,
+ ) {
+ Menu(
+ visible = menuVisible,
+ message = selectedMessage,
+ onDismiss = onDismiss,
+ onAction = { action ->
+ when (action) {
+ is Copy -> {
+ messageViewModel.copy(AnnotatedString(action.message.text))
+ }
+ else -> {}
+ }
+ onDismiss()
+ }
+ )
+
+ Content(
+ messages = messages,
+ onMessageSelected = {
+ selectedMessage = it
+ menuVisible = true
+ },
+ )
+ }
+ }
+
+ @Composable
+ fun Header() {
+ Text(
+ text = "Stream Chat".uppercase(Locale.getDefault()),
+ textAlign = TextAlign.Center,
+ style = TextStyle(
+ fontSize = 14.sp,
+ fontWeight = FontWeight.W700,
+ color = MaterialTheme.colors.onSurface,
+ ),
+ modifier = Modifier
+ .fillMaxWidth()
+ .background(MaterialTheme.colors.surface)
+ .padding(12.dp),
+ )
+ }
+
+ private val EventMessageListTheme
+ @Composable get() =
+ ThemeDefaults.messageList(
+ message = EventMessageTheme,
+ messageOwn = EventMessageTheme,
+ arrangement = Arrangement.spacedBy(4.dp),
+ )
+
+
+ private val EventMessageTheme
+ @Composable get() =
+ ThemeDefaults.message(
+ shape = ShapeThemeDefaults.messageBackground(
+ color = Color.Transparent,
+ padding = PaddingValues(2.dp),
+ ),
+ profileImage = ThemeDefaults.profileImage(
+ modifier = Modifier
+ .padding(8.dp, 4.dp, 4.dp, 4.dp)
+ .size(16.dp),
+ ),
+ modifier = Modifier.fillMaxWidth(),
+ )
+
+
+ private val EventMessageInputTheme
+ @Composable get() =
+ ThemeDefaults.messageInput(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth(),
+ button = ButtonThemeDefaults.button(
+ text = TextThemeDefaults.text(
+ fontSize = 12.sp,
+ ),
+ // TODO: replace the icon after Components fix
+ ),
+ input = InputThemeDefaults.input(
+ // TODO: override the textStyle after Components fix
+ )
+ )
+}
+
+
+@Composable
+@Preview
+private fun ChatPreview() {
+ Chat.View("channel.lobby")
+}
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/Menu.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/Menu.kt
new file mode 100644
index 0000000..a5eac26
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/Menu.kt
@@ -0,0 +1,33 @@
+package com.pubnub.components.example.live_event.ui.view
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.pubnub.components.chat.ui.component.menu.BottomMenu
+import com.pubnub.components.chat.ui.component.menu.MenuAction
+import com.pubnub.components.chat.ui.component.menu.React
+import com.pubnub.components.chat.ui.component.message.MessageUi
+import com.pubnub.components.chat.ui.component.message.reaction.renderer.DefaultReactionsPickerRenderer
+
+@Composable
+fun Menu(
+ visible: Boolean,
+ message: MessageUi.Data?,
+ onAction: (MenuAction) -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ BottomMenu(
+ message = message,
+ headerContent = {
+ DefaultReactionsPickerRenderer.ReactionsPicker { reaction ->
+ message?.let { onAction(React(reaction, message)) }
+ }
+ },
+ onAction = { action ->
+ onAction(action)
+ },
+ onDismiss = onDismiss,
+ visible = visible && message != null,
+ modifier = modifier,
+ )
+}
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/Video.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/Video.kt
new file mode 100644
index 0000000..50a30bc
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/Video.kt
@@ -0,0 +1,25 @@
+package com.pubnub.components.example.live_event.ui.view
+
+import androidx.compose.animation.*
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import com.pubnub.components.example.live_event.BuildConfig
+import com.pubnub.components.example.live_event.ui.util.Keyboard
+
+@Composable
+fun VideoContent() {
+
+ val isVisible by Keyboard.asState()
+ AnimatedVisibility(
+ visible = !isVisible,
+ enter = slideInVertically() + expandVertically(),
+ exit = shrinkVertically() + slideOutVertically(),
+ ) {
+ Box(Modifier.aspectRatio(16 / 9f)) {
+ YouTubeView(apiKey = BuildConfig.YOUTUBE_KEY, videoId = "eVS6EfrSA3I")
+ }
+ }
+}
diff --git a/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/YouTube.kt b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/YouTube.kt
new file mode 100644
index 0000000..133b2e7
--- /dev/null
+++ b/live-event/src/main/java/com/pubnub/components/example/live_event/ui/view/YouTube.kt
@@ -0,0 +1,71 @@
+package com.pubnub.components.example.live_event.ui.view
+
+import android.content.pm.ActivityInfo
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.fragment.app.FragmentContainerView
+import androidx.fragment.app.commit
+import com.google.android.youtube.player.YouTubeInitializationResult
+import com.google.android.youtube.player.YouTubePlayer
+import com.google.android.youtube.player.YouTubePlayerSupportFragmentXKt
+import com.pubnub.components.example.live_event.R
+import timber.log.Timber
+
+@Composable
+fun YouTubeView(apiKey: String, videoId: String) {
+ val context = LocalContext.current
+ AndroidView(
+ factory = {
+ val fragmentManager = (context as AppCompatActivity).supportFragmentManager
+ val view = FragmentContainerView(it).apply {
+ id = R.id.fragment_container_view_tag
+ }
+ val fragment = YouTubePlayerSupportFragmentXKt().apply {
+ initialize(
+ apiKey,
+ object : YouTubePlayer.OnInitializedListener {
+ override fun onInitializationFailure(
+ provider: YouTubePlayer.Provider,
+ result: YouTubeInitializationResult
+ ) {
+ Toast.makeText(
+ context,
+ context.getString(R.string.error_init),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ override fun onInitializationSuccess(
+ provider: YouTubePlayer.Provider,
+ player: YouTubePlayer,
+ wasRestored: Boolean
+ ) {
+ player.setOnFullscreenListener { isFullscreen ->
+ val newOrientation = if (isFullscreen)
+ ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ else
+ ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ activity?.requestedOrientation = newOrientation
+ }
+ if (!wasRestored) {
+ player.cueVideo(videoId)
+ }
+ }
+ },
+ )
+ }
+ fragmentManager.commit {
+ setReorderingAllowed(true)
+ add(R.id.fragment_container_view_tag, fragment)
+ }
+ view
+ },
+ modifier = Modifier.fillMaxSize()
+ )
+}
diff --git a/live-event/src/main/res/drawable-v24/ic_launcher_foreground.xml b/live-event/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/live-event/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/live-event/src/main/res/drawable/ic_launcher_background.xml b/live-event/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/live-event/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/live-event/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/live-event/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/live-event/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/live-event/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/live-event/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/live-event/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/live-event/src/main/res/mipmap-hdpi/ic_launcher.webp b/live-event/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/live-event/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/live-event/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/live-event/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/live-event/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/live-event/src/main/res/mipmap-mdpi/ic_launcher.webp b/live-event/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/live-event/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/live-event/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/live-event/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/live-event/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/live-event/src/main/res/mipmap-xhdpi/ic_launcher.webp b/live-event/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/live-event/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/live-event/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/live-event/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/live-event/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/live-event/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/live-event/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/live-event/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/live-event/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/live-event/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/live-event/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/live-event/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/live-event/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/live-event/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/live-event/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/live-event/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/live-event/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/live-event/src/main/res/values/colors.xml b/live-event/src/main/res/values/colors.xml
new file mode 100644
index 0000000..c8524cd
--- /dev/null
+++ b/live-event/src/main/res/values/colors.xml
@@ -0,0 +1,5 @@
+
+
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/live-event/src/main/res/values/strings.xml b/live-event/src/main/res/values/strings.xml
new file mode 100644
index 0000000..4cf2ef3
--- /dev/null
+++ b/live-event/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ Live Event
+ Type your message
+
\ No newline at end of file
diff --git a/live-event/src/main/res/values/themes.xml b/live-event/src/main/res/values/themes.xml
new file mode 100644
index 0000000..7304a4d
--- /dev/null
+++ b/live-event/src/main/res/values/themes.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/live-event/src/main/res/values/youtube.xml b/live-event/src/main/res/values/youtube.xml
new file mode 100644
index 0000000..f1be2eb
--- /dev/null
+++ b/live-event/src/main/res/values/youtube.xml
@@ -0,0 +1,4 @@
+
+
+ Error initializing video
+
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 3b3eb2e..2b0cd42 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -2,3 +2,4 @@ rootProject.name = "PubNub Chat Components Examples"
include(":getting-started")
include(":getting-started-with-reactions")
include(":telehealth-example")
+include(":live-event")