diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/StreaksReminder.kt b/app/src/main/kotlin/me/rhunk/snapenhance/StreaksReminder.kt index 5bd6b7566..4ba956bab 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/StreaksReminder.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/StreaksReminder.kt @@ -74,7 +74,7 @@ class StreaksReminder( notifyFriendList.forEach { (streaks, friend) -> remoteSideContext.coroutineScope.launch { - val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D) + val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie(friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D) val bitmojiImage = remoteSideContext.imageLoader.execute( ImageRequestHelper.newBitmojiImageRequest(ctx, bitmojiUrl) ) diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt index 686bcb076..d548bf2f6 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -195,6 +195,14 @@ class BridgeService : Service() { ) } + override fun getScopeNotes(id: String): String? { + return remoteSideContext.database.getScopeNotes(id) + } + + override fun setScopeNotes(id: String, content: String?) { + remoteSideContext.database.setScopeNotes(id, content) + } + override fun getScriptingInterface() = remoteSideContext.scriptManager override fun getE2eeInterface() = remoteSideContext.e2eeImplementation diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/storage/AppDatabase.kt b/app/src/main/kotlin/me/rhunk/snapenhance/storage/AppDatabase.kt index a5bbf2ff6..bf2a8998e 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/storage/AppDatabase.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/storage/AppDatabase.kt @@ -102,6 +102,10 @@ class AppDatabase( "repositories" to listOf( "url VARCHAR PRIMARY KEY", ), + "notes" to listOf( + "id CHAR(36) PRIMARY KEY", + "content TEXT", + ), )) } } \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/storage/ScopeNotes.kt b/app/src/main/kotlin/me/rhunk/snapenhance/storage/ScopeNotes.kt new file mode 100644 index 000000000..2f4f79577 --- /dev/null +++ b/app/src/main/kotlin/me/rhunk/snapenhance/storage/ScopeNotes.kt @@ -0,0 +1,32 @@ +package me.rhunk.snapenhance.storage + +import androidx.core.database.getStringOrNull + +fun AppDatabase.getScopeNotes(id: String): String? { + return database.rawQuery("SELECT content FROM notes WHERE id = ?", arrayOf(id)).use { + if (it.moveToNext()) { + it.getStringOrNull(0) + } else { + null + } + } +} + +fun AppDatabase.setScopeNotes(id: String, content: String?) { + if (content == null || content.isEmpty() == true) { + executeAsync { + database.execSQL("DELETE FROM notes WHERE id = ?", arrayOf(id)) + } + return + } + + executeAsync { + database.execSQL("INSERT OR REPLACE INTO notes (id, content) VALUES (?, ?)", arrayOf(id, content)) + } +} + +fun AppDatabase.deleteScopeNotes(id: String) { + executeAsync { + database.execSQL("DELETE FROM notes WHERE id = ?", arrayOf(id)) + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/social/ManageScope.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/social/ManageScope.kt index d56fa2a05..79d318fc0 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/social/ManageScope.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/social/ManageScope.kt @@ -10,18 +10,22 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavBackStackEntry import androidx.navigation.compose.currentBackStackEntryAsState import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import me.rhunk.snapenhance.common.data.FriendStreaks import me.rhunk.snapenhance.common.data.MessagingFriendInfo import me.rhunk.snapenhance.common.data.MessagingGroupInfo import me.rhunk.snapenhance.common.data.MessagingRuleType import me.rhunk.snapenhance.common.data.SocialScope +import me.rhunk.snapenhance.common.ui.AutoClearKeyboardFocus +import me.rhunk.snapenhance.common.ui.EditNoteTextField import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState import me.rhunk.snapenhance.common.ui.rememberAsyncMutableStateList import me.rhunk.snapenhance.common.util.snap.BitmojiSelfie @@ -89,6 +93,9 @@ class ManageScope: Routes.Route() { .verticalScroll(rememberScrollState()) .fillMaxSize() ) { + var bottomComposable by remember { + mutableStateOf(null as (@Composable () -> Unit)?) + } var hasScope by remember { mutableStateOf(null as Boolean?) } @@ -103,7 +110,7 @@ class ManageScope: Routes.Route() { } } friend?.let { - Friend(id, it, streaks) + Friend(id, it, streaks) { bottomComposable = it } } } SocialScope.GROUP -> { @@ -113,13 +120,17 @@ class ManageScope: Routes.Route() { } } group?.let { - Group(it) + Group(it) { bottomComposable = it } } } } if (hasScope == true) { + if (context.config.root.experimental.friendNotes.get()) { + NotesCard(id) + } RulesCard(id) } + bottomComposable?.invoke() if (hasScope == false) { Column( modifier = Modifier.fillMaxSize(), @@ -136,6 +147,33 @@ class ManageScope: Routes.Route() { } } + @Composable + private fun NotesCard( + id: String + ) { + val coroutineScope = rememberCoroutineScope { Dispatchers.IO } + var scopeNotes by rememberAsyncMutableState(null) { + context.database.getScopeNotes(id) + } + + AutoClearKeyboardFocus() + + EditNoteTextField( + modifier = Modifier.padding(8.dp), + primaryColor = Color.White, + translation = context.translation, + content = scopeNotes, + setContent = { scopeNotes = it } + ) + + DisposableEffect(Unit) { + onDispose { + coroutineScope.launch { + context.database.setScopeNotes(id, scopeNotes) + } + } + } + } @Composable private fun RulesCard( @@ -183,7 +221,7 @@ class ManageScope: Routes.Route() { @Composable private fun ContentCard(modifier: Modifier = Modifier, content: @Composable () -> Unit) { - Card( + ElevatedCard( modifier = Modifier .padding(10.dp) .fillMaxWidth() @@ -244,26 +282,105 @@ class ManageScope: Routes.Route() { private fun Friend( id: String, friend: MessagingFriendInfo, - streaks: FriendStreaks? + streaks: FriendStreaks?, + setBottomComposable: ((@Composable () -> Unit)?) -> Unit = {} ) { + LaunchedEffect(Unit) { + setBottomComposable { + Spacer(modifier = Modifier.height(16.dp)) + + if (context.config.root.experimental.e2eEncryption.globalState == true) { + SectionTitle(translation["e2ee_title"]) + var hasSecretKey by rememberAsyncMutableState(defaultValue = false) { + context.e2eeImplementation.friendKeyExists(friend.userId) + } + var importDialog by remember { mutableStateOf(false) } + + if (importDialog) { + Dialog( + onDismissRequest = { importDialog = false } + ) { + dialogs.RawInputDialog(onDismiss = { importDialog = false }, onConfirm = { newKey -> + importDialog = false + runCatching { + val key = Base64.decode(newKey) + if (key.size != 32) { + context.longToast("Invalid key size (must be 32 bytes)") + return@runCatching + } + + context.coroutineScope.launch { + context.e2eeImplementation.storeSharedSecretKey(friend.userId, key) + context.longToast("Successfully imported key") + } + + hasSecretKey = true + }.onFailure { + context.longToast("Failed to import key: ${it.message}") + context.log.error("Failed to import key", it) + } + }) + } + } + + ContentCard { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + if (hasSecretKey) { + OutlinedButton(onClick = { + context.coroutineScope.launch { + val secretKey = Base64.encode(context.e2eeImplementation.getSharedSecretKey(friend.userId) ?: return@launch) + //TODO: fingerprint auth + context.activity!!.startActivity(Intent.createChooser(Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, secretKey) + type = "text/plain" + }, "").apply { + putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf( + Intent().apply { + putExtra(Intent.EXTRA_TEXT, secretKey) + putExtra(Intent.EXTRA_SUBJECT, secretKey) + }) + ) + }) + } + }) { + Text( + text = "Export Base64", + maxLines = 1 + ) + } + } + + OutlinedButton(onClick = { importDialog = true }) { + Text( + text = "Import Base64", + maxLines = 1 + ) + } + } + } + } + } + } Column( modifier = Modifier - .padding(10.dp) + .padding(5.dp) .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { val bitmojiUrl = BitmojiSelfie.getBitmojiSelfie( - friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.THREE_D + friend.selfieId, friend.bitmojiId, BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D ) - BitmojiImage(context = context, url = bitmojiUrl, size = 100) - Spacer(modifier = Modifier.height(16.dp)) + BitmojiImage(context = context, url = bitmojiUrl, size = 120) Text( text = friend.displayName ?: friend.mutableUsername, maxLines = 1, fontSize = 20.sp, fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.height(5.dp)) Text( text = friend.mutableUsername, maxLines = 1, @@ -272,8 +389,6 @@ class ManageScope: Routes.Route() { ) } - Spacer(modifier = Modifier.height(16.dp)) - if (context.config.root.experimental.storyLogger.get()) { Row( modifier = Modifier.fillMaxWidth(), @@ -332,87 +447,14 @@ class ManageScope: Routes.Route() { } } } - Spacer(modifier = Modifier.height(16.dp)) - - if (context.config.root.experimental.e2eEncryption.globalState == true) { - SectionTitle(translation["e2ee_title"]) - var hasSecretKey by rememberAsyncMutableState(defaultValue = false) { - context.e2eeImplementation.friendKeyExists(friend.userId) - } - var importDialog by remember { mutableStateOf(false) } - - if (importDialog) { - Dialog( - onDismissRequest = { importDialog = false } - ) { - dialogs.RawInputDialog(onDismiss = { importDialog = false }, onConfirm = { newKey -> - importDialog = false - runCatching { - val key = Base64.decode(newKey) - if (key.size != 32) { - context.longToast("Invalid key size (must be 32 bytes)") - return@runCatching - } - - context.coroutineScope.launch { - context.e2eeImplementation.storeSharedSecretKey(friend.userId, key) - context.longToast("Successfully imported key") - } - - hasSecretKey = true - }.onFailure { - context.longToast("Failed to import key: ${it.message}") - context.log.error("Failed to import key", it) - } - }) - } - } - - ContentCard { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp) - ) { - if (hasSecretKey) { - OutlinedButton(onClick = { - context.coroutineScope.launch { - val secretKey = Base64.encode(context.e2eeImplementation.getSharedSecretKey(friend.userId) ?: return@launch) - //TODO: fingerprint auth - context.activity!!.startActivity(Intent.createChooser(Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, secretKey) - type = "text/plain" - }, "").apply { - putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf( - Intent().apply { - putExtra(Intent.EXTRA_TEXT, secretKey) - putExtra(Intent.EXTRA_SUBJECT, secretKey) - }) - ) - }) - } - }) { - Text( - text = "Export Base64", - maxLines = 1 - ) - } - } - - OutlinedButton(onClick = { importDialog = true }) { - Text( - text = "Import Base64", - maxLines = 1 - ) - } - } - } - } } } @Composable - private fun Group(group: MessagingGroupInfo) { + private fun Group( + group: MessagingGroupInfo, + setBottomComposable: ((@Composable () -> Unit)?) -> Unit = {} + ) { Column( modifier = Modifier .padding(10.dp) @@ -422,7 +464,6 @@ class ManageScope: Routes.Route() { Text( text = group.name, maxLines = 1, fontSize = 20.sp, fontWeight = FontWeight.Bold ) - Spacer(modifier = Modifier.height(5.dp)) Text( text = translation.format( "participants_text", "count" to group.participantsCount.toString() diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/social/SocialRootSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/social/SocialRootSection.kt index c74694103..f86138416 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/social/SocialRootSection.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/pages/social/SocialRootSection.kt @@ -122,7 +122,7 @@ class SocialRootSection : Routes.Route() { url = BitmojiSelfie.getBitmojiSelfie( friend.selfieId, friend.bitmojiId, - BitmojiSelfie.BitmojiSelfieType.THREE_D + BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D ) ) diff --git a/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl index affe31b1a..185734917 100644 --- a/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl +++ b/common/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -71,6 +71,10 @@ interface BridgeInterface { */ oneway void passGroupsAndFriends(in List groups, in List friends); + @nullable String getScopeNotes(String id); + + oneway void setScopeNotes(String id, String content); + IScripting getScriptingInterface(); E2eeInterface getE2eeInterface(); diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json index acd2d5641..cd58459e3 100644 --- a/common/src/main/assets/lang/en_US.json +++ b/common/src/main/assets/lang/en_US.json @@ -127,7 +127,8 @@ "streaks_expiration_text": "Expires in {eta}", "streaks_expiration_text_expired": "Expired", "reminder_button": "Set Reminder", - "delete_scope_confirm_dialog_title": "Are you sure you want to delete a {scope}?" + "delete_scope_confirm_dialog_title": "Are you sure you want to delete a {scope}?", + "notes_placeholder": "Click to add a note" }, "logged_stories": { "story_failed_to_load": "Failed to load", @@ -1066,6 +1067,10 @@ "name": "Voice Note Auto Play", "description": "Automatically plays the next voice note after the current one finishes" }, + "friend_notes": { + "name": "Friend Notes", + "description": "Allows you to add notes to friends profiles" + }, "cof_experiments": { "name": "COF Experiments", "description": "Enables unreleased/beta Snapchat features" diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt index 941cb6e71..55bf64fa9 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/Experimental.kt @@ -70,6 +70,7 @@ class Experimental : ConfigContainer() { val accountSwitcher = container("account_switcher", AccountSwitcherConfig()) { requireRestart(); addNotices(FeatureNotice.UNSTABLE) } val betterTranscript = container("better_transcript", BetterTranscriptConfig()) { requireRestart() } val voiceNoteAutoPlay = boolean("voice_note_auto_play") { requireRestart() } + val friendNotes = boolean("friend_notes") { requireRestart() } val editMessage = boolean("edit_message") { requireRestart() } val contextMenuFix = boolean("context_menu_fix") { requireRestart() } val cofExperiments = multiple("cof_experiments", *cofExperimentList.toTypedArray()) { requireRestart(); addFlags(ConfigFlag.NO_TRANSLATE); addNotices(FeatureNotice.UNSTABLE) } diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/Components.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/Components.kt new file mode 100644 index 000000000..412cdad8b --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/Components.kt @@ -0,0 +1,47 @@ +package me.rhunk.snapenhance.common.ui + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper + + +@Composable +fun EditNoteTextField( + modifier: Modifier = Modifier, + primaryColor: Color, + translation: LocaleWrapper, + content: String?, + setContent: (String) -> Unit +) { + TextField( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 180.dp) + .then(modifier), + value = content ?: "", + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + cursorColor = primaryColor + ), + onValueChange = { + setContent(it) + }, + shape = MaterialTheme.shapes.medium, + textStyle = LocalTextStyle.current.copy(fontSize = 12.sp, color = primaryColor), + placeholder = { Text(text = translation["manager.sections.manage_scope.notes_placeholder"], fontSize = 12.sp) } + ) +} \ No newline at end of file diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/ComposeViewFactory.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/ComposeViewFactory.kt index a2de20148..b44376c30 100644 --- a/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/ComposeViewFactory.kt +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/ComposeViewFactory.kt @@ -31,8 +31,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch // https://github.com/tberghuis/FloatingCountdownTimer/blob/master/app/src/main/java/xyz/tberghuis/floatingtimer/service/overlayViewFactory.kt -fun createComposeView(context: Context, content: @Composable () -> Unit) = ComposeView(context).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) +fun createComposeView( + context: Context, + viewCompositionStrategy: ViewCompositionStrategy = ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed, + content: @Composable () -> Unit +) = ComposeView(context).apply { + setViewCompositionStrategy(viewCompositionStrategy) val lifecycleOwner = OverlayLifecycleOwner().apply { performRestore(null) handleLifecycleEvent(Lifecycle.Event.ON_CREATE) diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/Keyboard.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/Keyboard.kt new file mode 100644 index 000000000..561ff46c2 --- /dev/null +++ b/common/src/main/kotlin/me/rhunk/snapenhance/common/ui/Keyboard.kt @@ -0,0 +1,47 @@ +package me.rhunk.snapenhance.common.ui + + +import android.view.ViewTreeObserver +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalView +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.compose.runtime.getValue + +@Composable +fun keyboardState(): State { + val keyboardState = remember { mutableStateOf(false) } + val localView = LocalView.current + val viewTreeObserver = localView.viewTreeObserver + + DisposableEffect(viewTreeObserver) { + val listener = ViewTreeObserver.OnGlobalLayoutListener { + keyboardState.value = ViewCompat.getRootWindowInsets(localView) + ?.isVisible(WindowInsetsCompat.Type.ime()) != false + } + viewTreeObserver.addOnGlobalLayoutListener(listener) + onDispose { + viewTreeObserver.takeIf { it.isAlive }?.removeOnGlobalLayoutListener(listener) + } + } + + return keyboardState +} + +@Composable +fun AutoClearKeyboardFocus( + onFocusClear: () -> Unit = {} +) { + val focusManager = LocalFocusManager.current + val keyboardState by keyboardState() + + if (!keyboardState) { + onFocusClear() + focusManager.clearFocus() + } +} diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt index 9a8db5482..23999aa94 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt @@ -251,6 +251,10 @@ class BridgeClient( service.setRule(targetUuid, type.key, state) } + fun getScopeNotes(id: String): String? = safeServiceCall { service.getScopeNotes(id) } + + fun setScopeNotes(id: String, content: String?) = safeServiceCall { service.setScopeNotes(id, content) } + fun getScriptingInterface(): IScripting = safeServiceCall { service.scriptingInterface } fun getE2eeInterface(): E2eeInterface = safeServiceCall { service.e2eeInterface } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventBus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventBus.kt index 04218aab3..5d74b3709 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventBus.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/event/EventBus.kt @@ -1,6 +1,7 @@ package me.rhunk.snapenhance.core.event import me.rhunk.snapenhance.core.ModContext +import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KClass abstract class Event { @@ -15,7 +16,7 @@ interface IListener { class EventBus( val context: ModContext ) { - private val subscribers = mutableMapOf, MutableMap>>() + private val subscribers = ConcurrentHashMap, MutableMap>>() fun subscribe(event: KClass, listener: IListener, priority: Int? = null) { synchronized(subscribers) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt index 07960366a..05b486d3d 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/FeatureManager.kt @@ -137,6 +137,7 @@ class FeatureManager( DisableTelecomFramework(), BetterTranscript(), VoiceNoteOverride(), + FriendNotes(), ) features.values.toList().forEach { feature -> diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt index 0ab5e9c2d..dfec37bea 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/downloader/MediaDownloader.kt @@ -79,7 +79,7 @@ class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleTyp else UUID.randomUUID().toString() ).longHashCode().absoluteValue.toString(16) - val iconUrl = BitmojiSelfie.getBitmojiSelfie(friendInfo?.bitmojiSelfieId, friendInfo?.bitmojiAvatarId, BitmojiSelfie.BitmojiSelfieType.THREE_D) + val iconUrl = BitmojiSelfie.getBitmojiSelfie(friendInfo?.bitmojiSelfieId, friendInfo?.bitmojiAvatarId, BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D) val downloadLogging by context.config.downloader.logging if (downloadLogging.contains("started")) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/FriendNotes.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/FriendNotes.kt new file mode 100644 index 000000000..1c2fdeaff --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/ui/FriendNotes.kt @@ -0,0 +1,83 @@ +package me.rhunk.snapenhance.core.features.impl.ui + +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.unit.dp +import me.rhunk.snapenhance.common.ui.AutoClearKeyboardFocus +import me.rhunk.snapenhance.common.ui.EditNoteTextField +import me.rhunk.snapenhance.common.ui.createComposeView +import me.rhunk.snapenhance.common.ui.rememberAsyncMutableState +import me.rhunk.snapenhance.core.event.events.impl.AddViewEvent +import me.rhunk.snapenhance.core.features.Feature +import me.rhunk.snapenhance.core.ui.getComposerContext +import me.rhunk.snapenhance.core.util.ktx.getObjectFieldOrNull + +class FriendNotes: Feature("Friend Notes") { + override fun init() { + if (!context.config.experimental.friendNotes.get()) return + + context.event.subscribe(AddViewEvent::class) { event -> + if (!event.viewClassName.endsWith("UnifiedProfileFlatlandProfileViewTopViewFrameLayout")) return@subscribe + + val viewGroup = (event.view as? ViewGroup) ?: return@subscribe + viewGroup.post { + val composerRootView = viewGroup.getChildAt(0) ?: return@post + val composerContext = composerRootView.getComposerContext() ?: return@post + val userId = composerContext.viewModel?.getObjectFieldOrNull("_userId")?.toString() ?: return@post + + if (userId == context.database.myUserId) return@post + + viewGroup.removeView(composerRootView) + + val manageNotesView = createComposeView(viewGroup.context, ViewCompositionStrategy.DisposeOnDetachedFromWindow) { + val primaryColor = remember { Color(this@FriendNotes.context.userInterface.colorPrimary) } + var isFetched by remember { mutableStateOf(false) } + var scopeNotes by rememberAsyncMutableState(null) { + this@FriendNotes.context.bridgeClient.getScopeNotes(userId).also { + isFetched = true + } + } + + DisposableEffect(Unit) { + onDispose { + runCatching { + if (!isFetched) return@runCatching + context.bridgeClient.setScopeNotes(userId, scopeNotes) + }.onFailure { + context.log.error("Failed to save notes", it) + } + } + } + + AutoClearKeyboardFocus() + + EditNoteTextField( + modifier = Modifier.padding(top = 8.dp), + primaryColor = primaryColor, + translation = context.translation, + content = scopeNotes, + setContent = { scopeNotes = it } + ) + } + + val linearLayout = LinearLayout(viewGroup.context).apply { + orientation = LinearLayout.VERTICAL + + addView(composerRootView) + addView(manageNotesView) + } + + viewGroup.addView(linearLayout) + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt index 57bb799a5..27ef66fcf 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/ui/menu/impl/FriendFeedInfoMenu.kt @@ -89,7 +89,7 @@ class FriendFeedInfoMenu : AbstractMenu() { BitmojiSelfie.getBitmojiSelfie( profile.bitmojiSelfieId.toString(), profile.bitmojiAvatarId.toString(), - BitmojiSelfie.BitmojiSelfieType.THREE_D + BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D )!! ) }