feat: friend notes
This commit is contained in:
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -102,6 +102,10 @@ class AppDatabase(
|
||||
"repositories" to listOf(
|
||||
"url VARCHAR PRIMARY KEY",
|
||||
),
|
||||
"notes" to listOf(
|
||||
"id CHAR(36) PRIMARY KEY",
|
||||
"content TEXT",
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -122,7 +122,7 @@ class SocialRootSection : Routes.Route() {
|
||||
url = BitmojiSelfie.getBitmojiSelfie(
|
||||
friend.selfieId,
|
||||
friend.bitmojiId,
|
||||
BitmojiSelfie.BitmojiSelfieType.THREE_D
|
||||
BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -71,6 +71,10 @@ interface BridgeInterface {
|
||||
*/
|
||||
oneway void passGroupsAndFriends(in List<String> groups, in List<String> friends);
|
||||
|
||||
@nullable String getScopeNotes(String id);
|
||||
|
||||
oneway void setScopeNotes(String id, String content);
|
||||
|
||||
IScripting getScriptingInterface();
|
||||
|
||||
E2eeInterface getE2eeInterface();
|
||||
|
||||
@@ -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",
|
||||
@@ -1062,6 +1063,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"
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Boolean> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -137,6 +137,7 @@ class FeatureManager(
|
||||
DisableTelecomFramework(),
|
||||
BetterTranscript(),
|
||||
VoiceNoteOverride(),
|
||||
FriendNotes(),
|
||||
)
|
||||
|
||||
features.values.toList().forEach { feature ->
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
|
||||
BitmojiSelfie.getBitmojiSelfie(
|
||||
profile.bitmojiSelfieId.toString(),
|
||||
profile.bitmojiAvatarId.toString(),
|
||||
BitmojiSelfie.BitmojiSelfieType.THREE_D
|
||||
BitmojiSelfie.BitmojiSelfieType.NEW_THREE_D
|
||||
)!!
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user