Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2024-08-08 13:19:17 +01:00
54 changed files with 1558 additions and 736 deletions
@@ -19,6 +19,8 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.internal.ChannelFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.*
@@ -301,9 +303,7 @@ object ChatModel {
else
chat.chatStats
)
if (i > 0) {
chats.add(index = 0, chats.removeAt(i))
}
popChatCollector.addChat(chat.remoteHostId, chat.id)
} else {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem)))
}
@@ -421,6 +421,32 @@ object ChatModel {
}
}
private val popChatCollector = PopChatCollector()
class PopChatCollector {
private val subject = MutableSharedFlow<Unit>()
init {
withLongRunningApi {
subject
.throttleLatest(2000)
.collect {
withChats {
chats.replaceAll(chats.value.sortedByDescending { it.popTs })
}
}
}
}
suspend fun addChat(remoteHostId: Long?, chatId: ChatId) {
val index = getChatIndex(remoteHostId, chatId)
if (index >= 0) {
chats.value[index].popTs = Clock.System.now()
subject.emit(Unit)
}
}
}
fun markChatItemsRead(remoteHostId: Long?, chatInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
val cInfo = chatInfo
val markedRead = markItemsReadInCurrentChat(chatInfo, range)
@@ -671,6 +697,7 @@ object ChatModel {
showingInvitation.value = null
chatModel.chatItems.clear()
chatModel.chatId.value = withId
ModalManager.start.closeModals()
ModalManager.end.closeModals()
}
}
@@ -681,6 +708,7 @@ object ChatModel {
chatModel.chatItems.clear()
chatModel.chatId.value = null
// Close NewChatView
ModalManager.start.closeModals()
ModalManager.center.closeModals()
ModalManager.end.closeModals()
}
@@ -845,6 +873,9 @@ data class Chat(
val chatItems: List<ChatItem>,
val chatStats: ChatStats = ChatStats()
) {
@Transient
var popTs: Instant? = null
val nextSendGrpInv: Boolean
get() = when (chatInfo) {
is ChatInfo.Direct -> chatInfo.contact.nextSendGrpInv
@@ -159,6 +159,7 @@ class AppPreferences {
val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false)
val liveMessageAlertShown = mkBoolPreference(SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN, false)
val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true)
val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false)
val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true)
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
val appUpdateChannel = mkEnumPreference(SHARED_PREFS_APP_UPDATE_CHANNEL, AppUpdatesChannel.DISABLED) { AppUpdatesChannel.entries.firstOrNull { it.name == this } }
@@ -227,6 +228,16 @@ class AppPreferences {
val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, appPlatform.isAndroid)
val hintPreferences: List<Pair<SharedPreference<Boolean>, Boolean>> = listOf(
laNoticeShown to false,
oneHandUICardShown to false,
liveMessageAlertShown to false,
showHiddenProfilesNotice to true,
showMuteProfileAlert to true,
showDeleteConversationNotice to true,
showDeleteContactNotice to true,
)
private fun mkIntPreference(prefName: String, default: Int) =
SharedPreference(
get = fun() = settings.getInt(prefName, default),
@@ -372,6 +383,7 @@ class AppPreferences {
private const val SHARED_PREFS_INCOGNITO = "Incognito"
private const val SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN = "LiveMessageAlertShown"
private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice"
private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown"
private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert"
private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase"
private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase"
@@ -519,7 +531,10 @@ object ChatController {
suspend fun startChatWithTemporaryDatabase(ctrl: ChatCtrl, netCfg: NetCfg): User? {
Log.d(TAG, "startChatWithTemporaryDatabase")
val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl)
apiSetNetworkConfig(netCfg, ctrl)
if (!apiSetNetworkConfig(netCfg, ctrl)) {
Log.e(TAG, "Error setting network config, stopping migration")
return null
}
apiSetAppFilePaths(
getMigrationTempFilesDirectory().absolutePath,
getMigrationTempFilesDirectory().absolutePath,
@@ -5759,7 +5774,7 @@ sealed class DatabaseError {
@Serializable
sealed class SQLiteError {
@Serializable @SerialName("errorNotADatabase") object ErrorNotADatabase: SQLiteError()
@Serializable @SerialName("error") class Error(val error: String): SQLiteError()
@Serializable @SerialName("error") class Error(val dbError: String): SQLiteError()
}
@Serializable
@@ -31,8 +31,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller
@@ -545,20 +544,22 @@ fun ChatInfoLayout(
SectionSpacer()
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
Box(
Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
SearchButton(chat, contact, close, onSearchClicked)
Spacer(Modifier.weight(1f))
AudioCallButton(chat, contact)
Spacer(Modifier.weight(1f))
VideoButton(chat, contact)
Spacer(Modifier.weight(1f))
MuteButton(chat, contact)
Row(
Modifier
.widthIn(max = 460.dp)
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
SearchButton(modifier = Modifier.fillMaxWidth(0.25f), chat, contact, close, onSearchClicked)
AudioCallButton(modifier = Modifier.fillMaxWidth(0.33f), chat, contact)
VideoButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact)
MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, contact)
}
}
SectionSpacer()
@@ -779,9 +780,16 @@ fun LocalAliasEditor(
}
@Composable
fun SearchButton(chat: Chat, contact: Contact, close: () -> Unit, onSearchClicked: () -> Unit) {
fun SearchButton(
modifier: Modifier,
chat: Chat,
contact: Contact,
close: () -> Unit,
onSearchClicked: () -> Unit
) {
val disabled = !contact.ready || chat.chatItems.isEmpty()
InfoViewActionButton(
modifier = modifier,
icon = painterResource(MR.images.ic_search),
title = generalGetString(MR.strings.info_view_search_button),
disabled = disabled,
@@ -796,11 +804,16 @@ fun SearchButton(chat: Chat, contact: Contact, close: () -> Unit, onSearchClicke
}
@Composable
fun MuteButton(chat: Chat, contact: Contact) {
fun MuteButton(
modifier: Modifier,
chat: Chat,
contact: Contact
) {
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
val disabled = !contact.ready || !contact.active
InfoViewActionButton(
modifier = modifier,
icon = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
disabled = disabled,
@@ -812,8 +825,13 @@ fun MuteButton(chat: Chat, contact: Contact) {
}
@Composable
fun AudioCallButton(chat: Chat, contact: Contact) {
fun AudioCallButton(
modifier: Modifier,
chat: Chat,
contact: Contact
) {
CallButton(
modifier = modifier,
chat,
contact,
icon = painterResource(MR.images.ic_call),
@@ -823,8 +841,13 @@ fun AudioCallButton(chat: Chat, contact: Contact) {
}
@Composable
fun VideoButton(chat: Chat, contact: Contact) {
fun VideoButton(
modifier: Modifier,
chat: Chat,
contact: Contact
) {
CallButton(
modifier = modifier,
chat,
contact,
icon = painterResource(MR.images.ic_videocam),
@@ -834,7 +857,14 @@ fun VideoButton(chat: Chat, contact: Contact) {
}
@Composable
fun CallButton(chat: Chat, contact: Contact, icon: Painter, title: String, mediaType: CallMediaType) {
fun CallButton(
modifier: Modifier,
chat: Chat,
contact: Contact,
icon: Painter,
title: String,
mediaType: CallMediaType
) {
val canCall = contact.ready && contact.active && contact.mergedPreferences.calls.enabled.forUser && chatModel.activeCall.value == null
val needToAllowCallsToContact = remember(chat.chatInfo) {
chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.calls) {
@@ -845,6 +875,7 @@ fun CallButton(chat: Chat, contact: Contact, icon: Painter, title: String, media
val allowedCallsByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Calls) }
InfoViewActionButton(
modifier = modifier,
icon = icon,
title = title,
disabled = chatModel.activeCall.value != null,
@@ -907,42 +938,52 @@ private fun showCallsProhibitedAlert() {
)
}
// for ChatInfoView (it has most buttons - 4) we use Spacer(Modifier.weight(1f)) to fit,
// for GroupChat And GroupMemberInfoViews (2 to 3 buttons) we use this as approximately equal to spacing in ChatInfoView
val INFO_VIEW_BUTTONS_PADDING = 36.dp
@Composable
fun InfoViewActionButton(icon: Painter, title: String, disabled: Boolean, disabledLook: Boolean, onClick: () -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
IconButton(
onClick = onClick,
enabled = !disabled
fun InfoViewActionButton(
modifier: Modifier,
icon: Painter,
title: String,
disabled: Boolean,
disabledLook: Boolean,
onClick: () -> Unit
) {
Box(modifier) {
Column(
Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.background(
if (disabledLook) MaterialTheme.colors.secondaryVariant else MaterialTheme.colors.primary,
shape = CircleShape
)
.padding(16.dp)
IconButton(
onClick = onClick,
enabled = !disabled
) {
Icon(
icon,
contentDescription = null,
Modifier.size(24.dp * fontSizeSqrtMultiplier),
tint = if (disabledLook) MaterialTheme.colors.secondary else MaterialTheme.colors.onPrimary
)
Box(
Modifier
.size(56.dp)
.background(
if (disabledLook) MaterialTheme.colors.secondaryVariant else MaterialTheme.colors.primary,
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
icon,
contentDescription = null,
Modifier.size(24.dp * fontSizeSqrtMultiplier),
tint = if (disabledLook) MaterialTheme.colors.secondary else MaterialTheme.colors.onPrimary
)
}
}
Text(
title.capitalize(Locale.current),
Modifier.padding(top = DEFAULT_SPACE_AFTER_ICON),
style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal, fontSize = 12.sp),
color = MaterialTheme.colors.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text(
title.capitalize(Locale.current),
style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.secondary,
modifier = Modifier.padding(top = DEFAULT_SPACE_AFTER_ICON)
)
}
}
@@ -23,8 +23,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.ui.theme.*
@@ -184,10 +183,17 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe
}
@Composable
fun SearchButton(chat: Chat, group: GroupInfo, close: () -> Unit, onSearchClicked: () -> Unit) {
fun SearchButton(
modifier: Modifier,
chat: Chat,
group: GroupInfo,
close: () -> Unit,
onSearchClicked: () -> Unit
) {
val disabled = !group.ready || chat.chatItems.isEmpty()
InfoViewActionButton(
modifier = modifier,
icon = painterResource(MR.images.ic_search),
title = generalGetString(MR.strings.info_view_search_button),
disabled = disabled,
@@ -202,10 +208,15 @@ fun SearchButton(chat: Chat, group: GroupInfo, close: () -> Unit, onSearchClicke
}
@Composable
fun MuteButton(chat: Chat, groupInfo: GroupInfo) {
fun MuteButton(
modifier: Modifier,
chat: Chat,
groupInfo: GroupInfo
) {
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
InfoViewActionButton(
modifier = modifier,
icon = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
disabled = !groupInfo.ready,
@@ -217,8 +228,13 @@ fun MuteButton(chat: Chat, groupInfo: GroupInfo) {
}
@Composable
fun AddGroupMembersButton(chat: Chat, groupInfo: GroupInfo) {
fun AddGroupMembersButton(
modifier: Modifier,
chat: Chat,
groupInfo: GroupInfo
) {
InfoViewActionButton(
modifier = modifier,
icon = if (groupInfo.incognito) painterResource(MR.images.ic_add_link) else painterResource(MR.images.ic_person_add_500),
title = stringResource(MR.strings.action_button_add_members),
disabled = !groupInfo.ready,
@@ -278,20 +294,26 @@ fun GroupChatInfoLayout(
}
SectionSpacer()
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
Box(
Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
SearchButton(chat, groupInfo, close, onSearchClicked)
if (groupInfo.canAddMembers) {
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
AddGroupMembersButton(chat, groupInfo)
Row(
Modifier
.widthIn(max = if (groupInfo.canAddMembers) 320.dp else 230.dp)
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
if (groupInfo.canAddMembers) {
SearchButton(modifier = Modifier.fillMaxWidth(0.33f), chat, groupInfo, close, onSearchClicked)
AddGroupMembersButton(modifier = Modifier.fillMaxWidth(0.5f), chat, groupInfo)
MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, groupInfo)
} else {
SearchButton(modifier = Modifier.fillMaxWidth(0.5f), chat, groupInfo, close, onSearchClicked)
MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, groupInfo)
}
}
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
MuteButton(chat, groupInfo)
}
SectionSpacer()
@@ -327,49 +327,49 @@ fun GroupMemberInfoLayout(
val contactId = member.memberContactId
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
Box(
Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
val knownChat = if (contactId != null) knownDirectChat(contactId) else null
if (knownChat != null) {
val (chat, contact) = knownChat
OpenChatButton(onClick = { openDirectChat(contact.contactId) })
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
AudioCallButton(chat, contact)
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
VideoButton(chat, contact)
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
if (contactId != null) {
OpenChatButton(onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group
} else {
OpenChatButton(onClick = { createMemberContact() })
Row(
Modifier
.widthIn(max = 320.dp)
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
val knownChat = if (contactId != null) knownDirectChat(contactId) else null
if (knownChat != null) {
val (chat, contact) = knownChat
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contact.contactId) })
AudioCallButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact)
VideoButton(modifier = Modifier.fillMaxWidth(1f), chat, contact)
} else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) {
if (contactId != null) {
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group
} else {
OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { createMemberContact() })
}
InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
showSendMessageToEnableCallsAlert()
})
InfoViewActionButton(modifier = Modifier.fillMaxWidth(1f), painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
showSendMessageToEnableCallsAlert()
})
} else { // no known contact chat && directMessages are off
InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.33f), painterResource(MR.images.ic_chat_bubble), generalGetString(MR.strings.info_view_message_button), disabled = false, disabledLook = true, onClick = {
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_send_message_to_member_alert_title))
})
InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title))
})
InfoViewActionButton(modifier = Modifier.fillMaxWidth(1f), painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title))
})
}
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
InfoViewActionButton(painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
showSendMessageToEnableCallsAlert()
})
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
InfoViewActionButton(painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
showSendMessageToEnableCallsAlert()
})
} else { // no known contact chat && directMessages are off
InfoViewActionButton(painterResource(MR.images.ic_chat_bubble), generalGetString(MR.strings.info_view_message_button), disabled = false, disabledLook = true, onClick = {
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_send_message_to_member_alert_title))
})
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
InfoViewActionButton(painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = {
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title))
})
Spacer(Modifier.width(INFO_VIEW_BUTTONS_PADDING))
InfoViewActionButton(painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = {
showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title))
})
}
}
SectionSpacer()
if (member.memberActive) {
@@ -580,8 +580,12 @@ fun RemoveMemberButton(onClick: () -> Unit) {
}
@Composable
fun OpenChatButton(onClick: () -> Unit) {
fun OpenChatButton(
modifier: Modifier,
onClick: () -> Unit
) {
InfoViewActionButton(
modifier = modifier,
icon = painterResource(MR.images.ic_chat_bubble),
title = generalGetString(MR.strings.info_view_message_button),
disabled = false,
@@ -12,6 +12,7 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.font.FontStyle
@@ -30,11 +31,11 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.WhatsNewView
import chat.simplex.common.views.onboarding.shouldShowWhatsNew
import chat.simplex.common.views.usersettings.SettingsView
import chat.simplex.common.platform.*
import chat.simplex.common.views.call.Call
import chat.simplex.common.views.chat.item.CIFileViewScope
import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
@@ -70,10 +71,72 @@ private fun showNewChatSheet(oneHandUI: State<Boolean>) {
}
}
@Composable
fun ToggleChatListCard() {
Column(
modifier = Modifier
.padding(16.dp)
.clip(RoundedCornerShape(18.dp))
) {
Box(
modifier = Modifier
.background(MaterialTheme.appColors.sentMessage)
) {
Box(
modifier = Modifier.fillMaxWidth().matchParentSize().padding(5.dp),
contentAlignment = Alignment.TopEnd
) {
IconButton(
onClick = {
appPrefs.oneHandUICardShown.set(true)
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.one_hand_ui),
text = generalGetString(MR.strings.one_hand_ui_change_instruction),
)
}
) {
Icon(
painterResource(MR.images.ic_close), stringResource(MR.strings.back), tint = MaterialTheme.colors.secondary
)
}
}
Column(
modifier = Modifier
.padding(horizontal = DEFAULT_PADDING)
.padding(top = DEFAULT_PADDING)
) {
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(MR.strings.one_hand_ui_card_title), style = MaterialTheme.typography.h3)
}
Row(
Modifier.fillMaxWidth().padding(top = 6.dp, bottom = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(stringResource(MR.strings.one_hand_ui), Modifier.weight(10f), style = MaterialTheme.typography.body1)
Spacer(Modifier.fillMaxWidth().weight(1f))
SharedPreferenceToggle(
appPrefs.oneHandUI,
enabled = true,
onChange = {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
}
)
}
}
}
}
}
@Composable
fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
val oneHandUI = remember { appPrefs.oneHandUI.state }
LaunchedEffect(Unit) {
if (shouldShowWhatsNew(chatModel)) {
delay(1000L)
@@ -337,7 +400,7 @@ fun SubscriptionStatusIndicator(click: (() -> Unit)) {
val scope = rememberCoroutineScope()
suspend fun setSubsTotal() {
if (chatModel.currentUser.value != null) {
if (chatModel.currentUser.value != null && chatModel.controller.hasChatCtrl() && chatModel.chatRunning.value == true) {
val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId())
if (r != null) {
subs = r.first
@@ -547,6 +610,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
var previousScrollOffset by remember { mutableStateOf(0) }
val keyboardState by getKeyboardState()
val oneHandUI = remember { appPrefs.oneHandUI.state }
val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state }
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
val currentIndex = listState.firstVisibleItemIndex
@@ -610,6 +674,11 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState<TextFieldVal
}
}
}
if (appPlatform.isAndroid && !oneHandUICardShown.value) {
item {
ToggleChatListCard()
}
}
itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat ->
val nextChatSelected = remember(chat.id, chats) { derivedStateOf {
chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value
@@ -16,6 +16,7 @@ import chat.simplex.res.MR
import com.charleskorn.kaml.decodeFromStream
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.serialization.encodeToString
import java.io.*
import java.net.URI
@@ -535,6 +536,13 @@ fun TextUnit.toDp(): Dp {
return Dp(value * LocalDensity.current.fontScale)
}
fun <T> Flow<T>.throttleLatest(delayMillis: Long): Flow<T> = this
.conflate()
.transform {
emit(it)
delay(delayMillis)
}
@Composable
fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) {
DisposableEffect(Unit) {
@@ -516,8 +516,11 @@ suspend fun initTemporaryDatabase(tempDatabaseFile: File, netCfg: NetCfg): Pair<
try {
if (ctrl != null) {
val user = startChatWithTemporaryDatabase(ctrl, netCfg)
return if (user != null) ctrl to user else null
if (user != null) return ctrl to user
chatCloseStore(ctrl)
}
File(tempDatabaseFile.absolutePath + "_chat.db").delete()
File(tempDatabaseFile.absolutePath + "_agent.db").delete()
} catch (e: Throwable) {
Log.e(TAG, "Error while starting chat in temporary database: ${e.stackTraceToString()}")
}
@@ -515,7 +515,9 @@ private fun MutableState<MigrationToState?>.prepareDatabase(
withLongRunningApi {
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg)
if (ctrlAndUser == null) {
state = MigrationToState.DownloadFailed(0, link, archivePath(), netCfg)
// Probably, something wrong with network config or database initialization, let's start from scratch
state = MigrationToState.PasteOrScanLink
MigrationToDeviceState.save(null)
return@withLongRunningApi
}
@@ -250,7 +250,6 @@ private fun NewChatSheetLayout(
icon = it.first,
text = it.second,
click = it.third,
extraPadding = true,
)
}
}
@@ -283,7 +282,7 @@ private fun NewChatSheetLayout(
contentDescription = stringResource(MR.strings.deleted_chats),
tint = MaterialTheme.colors.secondary,
)
TextIconSpaced(extraPadding = true)
TextIconSpaced(false)
Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground)
}
}
@@ -330,14 +329,13 @@ private fun NewChatButton(
text: String,
click: () -> Unit,
textColor: Color = Color.Unspecified,
iconColor: Color = MaterialTheme.colors.secondary,
disabled: Boolean = false,
extraPadding: Boolean = false,
iconColor: Color = MaterialTheme.colors.primary,
disabled: Boolean = false
) {
SectionItemView(click, disabled = disabled) {
Row {
Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else iconColor)
TextIconSpaced(extraPadding)
TextIconSpaced(false)
Text(text, color = if (disabled) MaterialTheme.colors.secondary else textColor)
}
}
@@ -30,7 +30,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
val currentVersion = remember { mutableStateOf(versionDescriptions.lastIndex) }
@Composable
fun featureDescription(icon: Painter, titleId: StringResource, descrId: StringResource, link: String?) {
fun featureDescription(icon: ImageResource?, titleId: StringResource, descrId: StringResource?, link: String?, subfeatures: List<Pair<ImageResource, StringResource>>) {
@Composable
fun linkButton(link: String) {
val uriHandler = LocalUriHandler.current
@@ -47,7 +47,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(bottom = 4.dp)
) {
Icon(icon, stringResource(titleId), tint = MaterialTheme.colors.secondary)
if (icon != null) Icon(painterResource(icon), stringResource(titleId), tint = MaterialTheme.colors.secondary)
Text(
generalGetString(titleId),
maxLines = 2,
@@ -59,7 +59,17 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
linkButton(link)
}
}
Text(generalGetString(descrId), fontSize = 15.sp)
if (descrId != null) Text(generalGetString(descrId), fontSize = 15.sp)
for ((si, sd) in subfeatures) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(bottom = 4.dp)
) {
Icon(painterResource(si), stringResource(sd), tint = MaterialTheme.colors.secondary)
Text(generalGetString(sd), fontSize = 15.sp)
}
}
}
}
@@ -116,7 +126,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
v.features.forEach { feature ->
if (feature.show) {
featureDescription(painterResource(feature.icon), feature.titleId, feature.descrId, feature.link)
featureDescription(feature.icon, feature.titleId, feature.descrId, feature.link, feature.subfeatures)
}
}
@@ -157,9 +167,10 @@ fun ReadMoreButton(url: String) {
}
private data class FeatureDescription(
val icon: ImageResource,
val icon: ImageResource?,
val titleId: StringResource,
val descrId: StringResource,
val descrId: StringResource?,
var subfeatures: List<Pair<ImageResource, StringResource>> = listOf(),
val link: String? = null,
val show: Boolean = true
)
@@ -595,14 +606,24 @@ private val versionDescriptions: List<VersionDescription> = listOf(
post = "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html",
features = listOf(
FeatureDescription(
icon = MR.images.ic_settings_ethernet,
titleId = MR.strings.v5_8_private_routing,
descrId = MR.strings.v6_0_private_routing_descr
icon = null,
titleId = MR.strings.v6_0_new_chat_experience,
descrId = null,
subfeatures = listOf(
MR.images.ic_add_link to MR.strings.v6_0_connect_faster_descr,
MR.images.ic_inventory_2 to MR.strings.v6_0_your_contacts_descr,
MR.images.ic_delete to MR.strings.v6_0_delete_many_messages_descr,
MR.images.ic_match_case to MR.strings.v6_0_increase_font_size
)
),
FeatureDescription(
icon = MR.images.ic_id_card,
titleId = MR.strings.v6_0_your_contacts,
descrId = MR.strings.v6_0_your_contacts_descr
icon = null,
titleId = MR.strings.v6_0_new_media_options,
descrId = null,
subfeatures = listOf(
MR.images.ic_play_arrow_filled to MR.strings.v6_0_chat_list_media,
MR.images.ic_blur_on to MR.strings.v6_0_privacy_blur,
)
),
FeatureDescription(
icon = MR.images.ic_toast,
@@ -611,20 +632,21 @@ private val versionDescriptions: List<VersionDescription> = listOf(
show = appPlatform.isAndroid
),
FeatureDescription(
icon = MR.images.ic_link,
titleId = MR.strings.v6_0_connect_faster,
descrId = MR.strings.v6_0_connect_faster_descr
),
FeatureDescription(
icon = MR.images.ic_delete,
titleId = MR.strings.v6_0_delete_many_messages,
descrId = MR.strings.v6_0_delete_many_messages_descr
icon = MR.images.ic_settings_ethernet,
titleId = MR.strings.v5_8_private_routing,
descrId = MR.strings.v6_0_private_routing_descr
),
FeatureDescription(
icon = MR.images.ic_wifi_tethering,
titleId = MR.strings.v6_0_connection_servers_status,
descrId = MR.strings.v6_0_connection_servers_status_descr
)
),
FeatureDescription(
icon = MR.images.ic_upgrade,
titleId = MR.strings.v6_0_upgrade_app,
descrId = MR.strings.v6_0_upgrade_app_descr,
show = appPlatform.isDesktop
),
),
)
)
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import chat.simplex.common.model.*
@@ -30,9 +31,11 @@ fun DeveloperView(
AppBarTitle(stringResource(MR.strings.settings_developer_tools))
val developerTools = m.controller.appPrefs.developerTools
val devTools = remember { developerTools.state }
val unchangedHints = mutableStateOf(unchangedHintPreferences())
SectionView {
InstallTerminalAppItem(uriHandler)
ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(it, close) }) }
ResetHintsItem(unchangedHints)
SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools)
SectionTextFooter(
generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " +
@@ -330,6 +330,31 @@ fun ChatLockItem(
}
}
@Composable fun ResetHintsItem(unchangedHints: MutableState<Boolean>) {
SectionItemView({
resetHintPreferences()
unchangedHints.value = true
}, disabled = unchangedHints.value) {
Icon(
painter = painterResource(MR.images.ic_lightbulb),
contentDescription = "Lightbulb",
tint = MaterialTheme.colors.secondary,
)
TextIconSpaced()
Text(generalGetString(MR.strings.reset_all_hints), color = if (unchangedHints.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
}
private fun resetHintPreferences() {
for ((pref, def) in appPreferences.hintPreferences) {
pref.set(def)
}
}
fun unchangedHintPreferences(): Boolean = appPreferences.hintPreferences.all { (pref, def) ->
pref.state.value == def
}
@Composable
fun AppVersionItem(showVersion: () -> Unit) {
SectionItemViewWithIcon(showVersion) { AppVersionText() }
@@ -739,6 +739,7 @@
<string name="xftp_servers_other">Other XFTP servers</string>
<string name="subscription_percentage">Show percentage</string>
<string name="install_simplex_chat_for_terminal">Install SimpleX Chat for terminal</string>
<string name="reset_all_hints">Reset all hints</string>
<string name="star_on_github">Star on GitHub</string>
<string name="contribute">Contribute</string>
<string name="rate_the_app">Rate the app</string>
@@ -1294,6 +1295,8 @@
<string name="incompatible_database_version">Incompatible database version</string>
<string name="confirm_database_upgrades">Confirm database upgrades</string>
<string name="one_hand_ui">Reachable chat toolbar</string>
<string name="one_hand_ui_card_title">Toggle chat list:</string>
<string name="one_hand_ui_change_instruction">You can change it in Appearance settings.</string>
<string name="terminal_always_visible">Show console in new window</string>
<string name="chat_list_always_visible">Show chat list in new window</string>
<string name="invalid_migration_confirmation">Invalid migration confirmation</string>
@@ -1974,15 +1977,19 @@
<string name="v5_8_message_delivery">Improved message delivery</string>
<string name="v5_8_message_delivery_descr">With reduced battery usage.</string>
<string name="v5_8_persian_ui">Persian UI</string>
<string name="v6_0_new_chat_experience">New chat experience 🎉</string>
<string name="v6_0_new_media_options">New media options</string>
<string name="v6_0_private_routing_descr">It protects your IP address and connections.</string>
<string name="v6_0_your_contacts">Your contacts your way</string>
<string name="v6_0_your_contacts_descr">- Search contacts when starting chat.\n- Archive contacts to chat later.</string>
<string name="v6_0_reachable_chat_toolbar">Reachable chat toolbar 👋</string>
<string name="v6_0_your_contacts_descr">Archive contacts to chat later.</string>
<string name="v6_0_reachable_chat_toolbar">Reachable chat toolbar</string>
<string name="v6_0_reachable_chat_toolbar_descr">Use the app with one hand.</string>
<string name="v6_0_connect_faster">Connect to your friends faster</string>
<string name="v6_0_connect_faster_descr">Even when they are offline.</string>
<string name="v6_0_delete_many_messages">Moderate like a pro ✋</string>
<string name="v6_0_connect_faster_descr">Connect to your friends faster.</string>
<string name="v6_0_delete_many_messages_descr">Delete up to 20 messages at once.</string>
<string name="v6_0_chat_list_media">Play from the chat list.</string>
<string name="v6_0_privacy_blur">Blur for better privacy.</string>
<string name="v6_0_increase_font_size">Increase font size.</string>
<string name="v6_0_upgrade_app">Upgrade app automatically</string>
<string name="v6_0_upgrade_app_descr">Download new versions from GitHub.</string>
<string name="v6_0_connection_servers_status">Control your network</string>
<string name="v6_0_connection_servers_status_descr">Connection and servers status.</string>
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368">
<path
d="M480-84.5q-33.5 0-56.75-23.25T400-164.5h160q0 33.5-23.25 56.75T480-84.5Zm-160-143V-285h320v57.5H320Zm4.5-119.5q-65-42.5-102.75-106T184-595.87q0-120.39 87.76-208.26 87.77-87.87 208-87.87Q600-892 688-804.13q88 87.87 88 208.26 0 79.37-37.25 142.87T635.5-347h-311Zm21.32-57.5h269.3q47.88-32 75.63-82.51 27.75-50.51 27.75-108.95 0-98.04-70.04-168.29-70.04-70.25-168.37-70.25-98.34 0-168.46 70.12-70.13 70.12-70.13 168.57 0 58.49 28 108.9 28 50.41 76.32 82.41Zm134.18 0Z" />
</svg>

After

Width:  |  Height:  |  Size: 604 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000">
<path
d="M246-382.5 209-277q-2 6.5-8.25 10.75T187-262q-12 0-18.5-9.25t-2-20.75L308-681.5q3-7.5 8.75-11.5t13.25-4h18q7.5 0 13.25 4t8.75 11.5l142.5 389q4.5 11.5-2.5 21t-19.5 9.5q-7.5 0-13.75-3.75t-8.25-11.75l-37-105H246Zm15.5-42h155l-77-217.5H338l-76.5 217.5Zm403 172.5q-50 0-82.5-26t-32.5-69.5q0-47.5 35-75.75t93-28.25q24.5 0 47 4.5t39 13v-20q0-38-20.25-58.25T684-532.5q-17.5 0-34.5 5.25T618-511q-8 5.5-15.75 3T590-517.5q-4.5-6.5-3.25-15T597-547q20-13 42.25-18.75T685-571.5q59.5 0 90.75 30.75T807-450v166.5q0 9-6.25 15.25T786-262q-9 0-15.25-6.25T764.5-283v-21H761q-16 26.5-43.5 39.25t-53 12.75Zm-.5-36.5q36.5 0 68.5-30.25t32-77.75q-16-8-38-13t-43.5-5q-42 0-65.5 17.5T594-348q0 27 20.5 43.25T664-288.5Z" />
</svg>

After

Width:  |  Height:  |  Size: 831 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000">
<path
d="M310-165q-12.5 0-20.5-8.25t-8-20.75q0-12 8-20.25t20.5-8.25h340q12.5 0 20.75 8.25T679-194q0 12.5-8.25 20.75T650-165H310Zm170-165q-12.5 0-20.5-8.25t-8-20.75v-327L349-583.5q-8.5 8.5-20 8.5t-20-8.5q-9-9-9-20.25t9-20.25l151-151q4.5-4.5 9.25-6.75T480-784q6 0 10.75 2.25t9.75 6.75l151 151q8.5 9 8.5 20.25T651.5-584q-9 9-20.25 9T611-584L509-686v327q0 12.5-8.25 20.75T480-330Z" />
</svg>

After

Width:  |  Height:  |  Size: 507 B

+4 -4
View File
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=6.0-beta.2
android.version_code=227
android.version_name=6.0-beta.3
android.version_code=228
desktop.version_name=6.0-beta.2
desktop.version_code=58
desktop.version_name=6.0-beta.3
desktop.version_code=59
kotlin.version=1.9.23
gradle.plugin.version=8.2.0