Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions core-ui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ dependencies {
implementation libs.exoPlayerUi
implementation libs.urlcleaner
implementation libs.katex

// Modern emoji support
implementation libs.emoji2
implementation libs.emoji2Views
implementation libs.emoji2ViewsHelper
implementation libs.emoji2Bundled

implementation libs.compose
implementation libs.composeFoundation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package com.anytypeio.anytype.core_ui.features.emoji

import android.content.Context
import android.content.SharedPreferences
import com.anytypeio.anytype.core_ui.BuildConfig
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

/**
* Configuration manager for emoji rendering preferences.
* Allows users and the app to control emoji rendering behavior.
*/

class EmojiConfiguration(private val context: Context) {

companion object {
private const val PREFS_NAME = "emoji_preferences"
private const val KEY_MODERN_EMOJI_ENABLED = "modern_emoji_enabled"
private const val KEY_RENDERING_MODE = "rendering_mode"
private const val KEY_AUTO_FALLBACK_ENABLED = "auto_fallback_enabled"
private const val KEY_PERFORMANCE_MODE = "performance_mode"
private const val KEY_ACCESSIBILITY_MODE = "accessibility_mode"
}

private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private val _configurationFlow = MutableStateFlow(getCurrentConfiguration())

/**
* Flow of current emoji configuration
*/
val configurationFlow: Flow<Configuration> = _configurationFlow.asStateFlow()

/**
* Get current configuration
*/
fun getCurrentConfiguration(): Configuration {
return Configuration(
modernEmojiEnabled = prefs.getBoolean(KEY_MODERN_EMOJI_ENABLED, false),
renderingMode = parseRenderingMode(prefs.getString(KEY_RENDERING_MODE, null)),
autoFallbackEnabled = prefs.getBoolean(KEY_AUTO_FALLBACK_ENABLED, true),
performanceMode = prefs.getBoolean(KEY_PERFORMANCE_MODE, false),
accessibilityMode = prefs.getBoolean(KEY_ACCESSIBILITY_MODE, false)
)
}

/**
* Enable or disable modern emoji rendering
*/
fun setModernEmojiEnabled(enabled: Boolean) {
prefs.edit().putBoolean(KEY_MODERN_EMOJI_ENABLED, enabled).apply()
updateFlow()
}

/**
* Set preferred rendering mode
*/
fun setRenderingMode(mode: EmojiRenderingMode) {
prefs.edit().putString(KEY_RENDERING_MODE, mode.name).apply()
updateFlow()
}

/**
* Set auto-fallback preference
*/
fun setAutoFallbackEnabled(enabled: Boolean) {
prefs.edit().putBoolean(KEY_AUTO_FALLBACK_ENABLED, enabled).apply()
updateFlow()
}

/**
* Enable performance mode (prioritizes speed over quality)
*/
fun setPerformanceMode(enabled: Boolean) {
prefs.edit().putBoolean(KEY_PERFORMANCE_MODE, enabled).apply()
updateFlow()
}

/**
* Enable accessibility mode (ensures maximum compatibility)
*/
fun setAccessibilityMode(enabled: Boolean) {
prefs.edit().putBoolean(KEY_ACCESSIBILITY_MODE, enabled).apply()
updateFlow()
}

/**
* Reset all preferences to defaults
*/
fun resetToDefaults() {
prefs.edit().clear().apply()
updateFlow()
}

private fun updateFlow() {
_configurationFlow.value = getCurrentConfiguration()
}

private fun parseRenderingMode(value: String?): EmojiRenderingMode {
return try {
value?.let { EmojiRenderingMode.valueOf(it) } ?: getDefaultRenderingMode()
} catch (e: IllegalArgumentException) {
getDefaultRenderingMode()
}
}

private fun getDefaultRenderingMode(): EmojiRenderingMode {
return when {
android.os.Build.VERSION.SDK_INT >= 28 -> EmojiRenderingMode.EMOJI_COMPAT_BUNDLED
android.os.Build.VERSION.SDK_INT >= 23 -> EmojiRenderingMode.EMOJI_COMPAT_BUNDLED
else -> EmojiRenderingMode.LEGACY_PNG
}
}

data class Configuration(
val modernEmojiEnabled: Boolean,
val renderingMode: EmojiRenderingMode,
val autoFallbackEnabled: Boolean,
val performanceMode: Boolean,
val accessibilityMode: Boolean
) {

/**
* Get the effective rendering mode considering all settings
*/
fun getEffectiveRenderingMode(): EmojiRenderingMode {
return when {
!modernEmojiEnabled -> EmojiRenderingMode.LEGACY_PNG
accessibilityMode -> EmojiRenderingMode.LEGACY_PNG // Most compatible
performanceMode -> EmojiRenderingMode.NATIVE // Fastest
else -> renderingMode
}
}

/**
* Convert to EmojiRenderer.Config
*/
fun toRendererConfig(): EmojiRenderer.Config {
return EmojiRenderer.Config(
useModernRenderer = modernEmojiEnabled,
renderingMode = getEffectiveRenderingMode(),
enableFallback = autoFallbackEnabled
)
}
}
}

/**
* Emoji feature configuration that can be controlled remotely or via build variants
*/
object EmojiFeatureConfig {

/**
* Whether modern emoji features are available in this build
*/
fun isModernEmojiAvailable(): Boolean {
// Could be controlled by:
// - Build variant (enable in debug/beta)
// - Feature flags
// - Remote config
return true // For now, always available
}

/**
* Whether to show emoji rendering options in settings
*/
fun showEmojiSettings(): Boolean {
return isModernEmojiAvailable() &&
(BuildConfig.DEBUG || isEmojiExperimentEnabled())
}

/**
* Check if emoji experiments are enabled
*/
private fun isEmojiExperimentEnabled(): Boolean {
// This could check:
// - Remote config flags
// - User is in beta group
// - Device is registered for experiments
return false
}

/**
* Get recommended configuration for current build/user
*/
fun getRecommendedConfiguration(): EmojiConfiguration.Configuration {
return EmojiConfiguration.Configuration(
modernEmojiEnabled = when {
BuildConfig.DEBUG -> true // Enable in debug builds
isEmojiExperimentEnabled() -> true // Enable for beta users
else -> false // Disabled in production for now
},
renderingMode = EmojiRenderingMode.EMOJI_COMPAT_BUNDLED,
autoFallbackEnabled = true,
performanceMode = false,
accessibilityMode = false
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package com.anytypeio.anytype.core_ui.features.emoji

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.presentation.objects.ObjectIcon

/**
* Migration helper for gradually introducing modern emoji rendering.
* This component can be used as a drop-in replacement for existing EmojiIconView
* while providing the ability to enable modern rendering via configuration.
*/

/**
* Unified emoji icon component that switches between old and new implementations
* based on configuration. This allows for A/B testing and gradual rollout.
*/
@Composable
fun UnifiedEmojiIconView(
modifier: Modifier = Modifier,
icon: ObjectIcon.Basic.Emoji,
backgroundSize: Dp,
iconWithoutBackgroundMaxSize: Dp,
backgroundColor: Int = R.color.shape_tertiary,
forceModern: Boolean? = null // Override for testing
) {
val context = LocalContext.current
val emojiConfig = remember { EmojiConfiguration(context) }
val configuration by emojiConfig.configurationFlow.collectAsState(
initial = EmojiConfiguration.Configuration(
modernEmojiEnabled = false,
renderingMode = EmojiRenderingMode.LEGACY_PNG,
autoFallbackEnabled = true,
performanceMode = false,
accessibilityMode = false
)
)

// Determine which renderer to use
val useModern = forceModern ?: (configuration.modernEmojiEnabled &&
EmojiFeatureConfig.isModernEmojiAvailable())

EmojiRenderer.EmojiIcon(
modifier = modifier,
icon = icon,
backgroundSize = backgroundSize,
iconWithoutBackgroundMaxSize = iconWithoutBackgroundMaxSize,
backgroundColor = backgroundColor,
config = EmojiRenderer.Config(
useModernRenderer = useModern,
renderingMode = configuration.getEffectiveRenderingMode(),
enableFallback = configuration.autoFallbackEnabled
)
)
}

/**
* Demo component to showcase different emoji rendering modes
* This can be used in development screens to test emoji rendering
*/
@Composable
fun EmojiRenderingDemo(
modifier: Modifier = Modifier,
emoji: String = "πŸ˜€",
backgroundSize: Dp,
iconWithoutBackgroundMaxSize: Dp
) {
val testIcon = ObjectIcon.Basic.Emoji(
unicode = emoji,
fallback = ObjectIcon.TypeIcon.Fallback.DEFAULT
)

// Show all rendering modes for comparison
EmojiRenderingMode.values().forEach { mode ->
UnifiedEmojiIconView(
modifier = modifier,
icon = testIcon,
backgroundSize = backgroundSize,
iconWithoutBackgroundMaxSize = iconWithoutBackgroundMaxSize,
forceModern = mode != EmojiRenderingMode.LEGACY_PNG
)
}
}

/**
* Performance testing helper
*/
object EmojiPerformanceTester {

data class TestResult(
val mode: EmojiRenderingMode,
val initTime: Long,
val renderTime: Long,
val memoryUsage: Long,
val success: Boolean,
val error: String? = null
)

/**
* Test performance of different emoji rendering modes
*/
suspend fun runPerformanceTest(
context: android.content.Context,
testEmojis: List<String> = listOf("πŸ˜€", "πŸ‘", "πŸŽ‰", "❀️", "πŸ”₯")
): List<TestResult> {
val results = mutableListOf<TestResult>()

for (mode in EmojiRenderingMode.values()) {
try {
val startTime = System.currentTimeMillis()
val provider = ModernEmojiProviderImpl(context, mode)
val initSuccess = provider.initialize()
val initTime = System.currentTimeMillis() - startTime

if (initSuccess) {
val renderStartTime = System.currentTimeMillis()
var renderSuccess = true

// Test rendering each emoji
for (emoji in testEmojis) {
try {
provider.process(emoji)
} catch (e: Exception) {
renderSuccess = false
break
}
}

val renderTime = System.currentTimeMillis() - renderStartTime

results.add(TestResult(
mode = mode,
initTime = initTime,
renderTime = renderTime,
memoryUsage = estimateMemoryUsage(mode),
success = renderSuccess
))
} else {
results.add(TestResult(
mode = mode,
initTime = initTime,
renderTime = -1,
memoryUsage = -1,
success = false,
error = "Initialization failed"
))
}
} catch (e: Exception) {
results.add(TestResult(
mode = mode,
initTime = -1,
renderTime = -1,
memoryUsage = -1,
success = false,
error = e.message
))
}
}

return results
}

private fun estimateMemoryUsage(mode: EmojiRenderingMode): Long {
return when (mode) {
EmojiRenderingMode.NATIVE -> 1024L
EmojiRenderingMode.EMOJI_COMPAT_BUNDLED -> 1024L * 100L
EmojiRenderingMode.EMOJI_COMPAT_DOWNLOADABLE -> 1024L * 150L
EmojiRenderingMode.LEGACY_PNG -> 1024L * 1024L * 20L
}
}
}
Loading
Loading