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")