From 34cf672bc60bbd7464567fd65545b78e034c0aa0 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 16 Aug 2023 21:21:12 +0400 Subject: [PATCH 01/14] core: show count and average time for slow queries (#2939) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 10 ++++++++-- src/Simplex/Chat/Controller.hs | 3 ++- src/Simplex/Chat/View.hs | 7 ++++++- stack.yaml | 2 +- 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/cabal.project b/cabal.project index 0b27046572..e44ae5eec7 100644 --- a/cabal.project +++ b/cabal.project @@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: e586bef57a1391d8bdedc2afa645926931549e16 + tag: cf2a17b80ce5736a8b3b02016e3f466f781f259d source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index c2530c26c1..2444eaaaf1 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."e586bef57a1391d8bdedc2afa645926931549e16" = "00804ck1xka37j5gwaiyd3a8vflv8z1hmip1wyynkvr7naxblvzh"; + "https://github.com/simplex-chat/simplexmq.git"."cf2a17b80ce5736a8b3b02016e3f466f781f259d" = "0yq7kaidnlv9rxl080jv89p8awap046flqzglb71kwy1h1klvyri"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 82861967c1..726d82f2f4 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -79,6 +79,7 @@ import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection) +import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client (defaultNetworkConfig) @@ -498,7 +499,12 @@ processChatCommand = \case agentQueries <- slowQueries $ agentClientStore smpAgent pure CRSlowSQLQueries {chatQueries, agentQueries} where - slowQueries st = liftIO $ map (uncurry SlowSQLQuery . first SQL.fromQuery) . sortOn snd . M.assocs <$> withConnection st (readTVarIO . DB.slow) + slowQueries st = + liftIO $ + map (uncurry SlowSQLQuery . first SQL.fromQuery) + . sortOn (timeAvg . snd) + . M.assocs + <$> withConnection st (readTVarIO . DB.slow) APIGetChats userId withPCC -> withUserId userId $ \user -> CRApiChats user <$> withStoreCtx' (Just "APIGetChats, getChatPreviews") (\db -> getChatPreviews db user withPCC) APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of @@ -3982,7 +3988,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta content withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) - toView $ CRReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} + toView $ CRReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} whenContactNtfs user ct $ showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group" where diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 3b741168e6..5fd5b71f24 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -60,6 +60,7 @@ import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType, CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, UserProtocol, XFTPServerWithAuth) import Simplex.Messaging.TMap (TMap) +import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import Simplex.Messaging.Transport (simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors) @@ -805,7 +806,7 @@ data SendFileMode data SlowSQLQuery = SlowSQLQuery { query :: Text, - duration :: Int64 + queryStats :: SlowQueryStats } deriving (Show, Generic) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 2db39bccee..ef3b4d1e07 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -47,6 +47,7 @@ import qualified Simplex.FileTransfer.Protocol as XFTP import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..)) import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -248,7 +249,11 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRNtfMessages {} -> [] CRSQLResult rows -> map plain rows CRSlowSQLQueries {chatQueries, agentQueries} -> - let viewQuery SlowSQLQuery {query, duration} = sShow duration <> " ms: " <> plain (T.unwords $ T.lines query) + let viewQuery SlowSQLQuery {query, queryStats = SlowQueryStats {count, timeMax, timeAvg}} = + "count: " <> sShow count + <> (" :: max: " <> sShow timeMax <> " ms") + <> (" :: avg: " <> sShow timeAvg <> " ms") + <> (" :: " <> plain (T.unwords $ T.lines query)) in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries) CRDebugLocks {chatLockName, agentLocks} -> [ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName, diff --git a/stack.yaml b/stack.yaml index 30dedb1b45..93775b32a6 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: e586bef57a1391d8bdedc2afa645926931549e16 + commit: cf2a17b80ce5736a8b3b02016e3f466f781f259d - github: kazu-yamamoto/http2 commit: b5a1b7200cf5bc7044af34ba325284271f6dff25 # - ../direct-sqlcipher From 6cf9f0303ba26c1d6d3cd9e01a109300d821902f Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 16 Aug 2023 20:50:27 +0300 Subject: [PATCH 02/14] desktop: handling keyboard in auth screen (#2938) * desktop: handling keyboard in auth screen * numpad support * numPadEnter * padding --- .../common/platform/Resources.android.kt | 10 ++-- .../kotlin/chat/simplex/common/App.kt | 1 - .../chat/simplex/common/platform/Resources.kt | 6 +-- .../common/views/chatlist/UserPicker.kt | 4 +- .../common/views/helpers/AlertManager.kt | 16 ++++-- .../simplex/common/views/helpers/Section.kt | 4 +- .../simplex/common/views/helpers/Utils.kt | 8 +-- .../common/views/localauth/PasscodeView.kt | 54 ++++++++++++++++--- .../common/views/localauth/PasswordEntry.kt | 8 +-- .../common/views/newchat/NewChatSheet.kt | 2 +- .../common/platform/Resources.desktop.kt | 19 +++---- 11 files changed, 89 insertions(+), 43 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt index 226e97d4dc..e15d1f9268 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt @@ -39,14 +39,14 @@ private val sharedPreferencesThemes: SharedPreferences by lazy { androidAppConte actual val settings: Settings by lazy { SharedPreferencesSettings(sharedPreferences) } actual val settingsThemes: Settings by lazy { SharedPreferencesSettings(sharedPreferencesThemes) } -actual fun screenOrientation(): ScreenOrientation = when (mainActivity.get()?.resources?.configuration?.orientation) { - Configuration.ORIENTATION_PORTRAIT -> ScreenOrientation.PORTRAIT - Configuration.ORIENTATION_LANDSCAPE -> ScreenOrientation.LANDSCAPE - else -> ScreenOrientation.UNDEFINED +actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.resources?.configuration?.orientation) { + Configuration.ORIENTATION_PORTRAIT -> WindowOrientation.PORTRAIT + Configuration.ORIENTATION_LANDSCAPE -> WindowOrientation.LANDSCAPE + else -> WindowOrientation.UNDEFINED } @Composable -actual fun screenWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp +actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp actual fun desktopExpandWindowToWidth(width: Dp) {} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index dda00fc43c..cb386be7a3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -311,7 +311,6 @@ fun DesktopScreen(settingsState: SettingsViewState) { scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } } ModalManager.fullscreen.showInView() - ModalManager.fullscreen.showPasscodeInView() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt index c7799e592b..2ee668fb23 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt @@ -19,14 +19,14 @@ expect fun isInNightMode(): Boolean expect val settings: Settings expect val settingsThemes: Settings -enum class ScreenOrientation { +enum class WindowOrientation { UNDEFINED, PORTRAIT, LANDSCAPE } -expect fun screenOrientation(): ScreenOrientation +expect fun windowOrientation(): WindowOrientation @Composable -expect fun screenWidth(): Dp +expect fun windowWidth(): Dp expect fun desktopExpandWindowToWidth(width: Dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 139b23e818..8c7dc2c605 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -93,7 +93,7 @@ fun UserPicker( } } val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() } - val maxWidth = with(LocalDensity.current) { screenWidth() * density } + val maxWidth = with(LocalDensity.current) { windowWidth() * density } Box(Modifier .fillMaxSize() .offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else xOffset, 0) } @@ -201,7 +201,7 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues fun UserProfileRow(u: User) { Row( Modifier - .widthIn(max = screenWidth() * 0.7f) + .widthIn(max = windowWidth() * 0.7f) .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 49a7ad748f..f5fbd0150e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -8,6 +8,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign @@ -159,13 +160,22 @@ class AlertManager { title = alertTitle(title), text = alertText(text), buttons = { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } Row( Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center ) { - TextButton(onClick = { - hideAlert() - }) { Text(confirmText, color = Color.Unspecified) } + TextButton( + onClick = { + hideAlert() + }, + Modifier.focusRequester(focusRequester) + ) { + Text(confirmText, color = Color.Unspecified) + } } }, shape = RoundedCornerShape(corner = CornerSize(25.dp)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index e59653cef1..6adbfed76c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -12,7 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* -import chat.simplex.common.platform.screenWidth +import chat.simplex.common.platform.windowWidth import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.SettingsActionItemWithContent @@ -238,7 +238,7 @@ fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color @Composable fun InfoRowEllipsis(title: String, value: String, onClick: () -> Unit) { SectionItemViewSpaceBetween(onClick) { - val screenWidthDp = screenWidth() + val screenWidthDp = windowWidth() Text(title) Text( value, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index c07459a3da..a6746da694 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -306,10 +306,10 @@ fun IntSize.Companion.Saver(): Saver = Saver( fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) { DisposableEffect(Unit) { always() - val orientation = screenOrientation() + val orientation = windowOrientation() onDispose { whenDispose() - if (orientation == screenOrientation()) { + if (orientation == windowOrientation()) { whenGone() } } @@ -320,10 +320,10 @@ fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {} fun DisposableEffectOnRotate(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenRotate: () -> Unit) { DisposableEffect(Unit) { always() - val orientation = screenOrientation() + val orientation = windowOrientation() onDispose { whenDispose() - if (orientation != screenOrientation()) { + if (orientation != windowOrientation()) { whenRotate() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt index 3e35bfb80a..4784951ad0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt @@ -6,10 +6,11 @@ import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.* +import androidx.compose.ui.input.key.* import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.unit.dp -import chat.simplex.common.platform.ScreenOrientation -import chat.simplex.common.platform.screenOrientation +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.SimpleButton import chat.simplex.common.views.helpers.* @@ -25,9 +26,43 @@ fun PasscodeView( submit: () -> Unit, cancel: () -> Unit, ) { + val focusRequester = remember { FocusRequester() } + + @Composable + fun Modifier.handleKeyboard(): Modifier { + val numbers = remember { + arrayOf( + Key.Zero, Key.One, Key.Two, Key.Three, Key.Four, Key.Five, Key.Six, Key.Seven, Key.Eight, Key.Nine, + Key.NumPad0, Key.NumPad1, Key.NumPad2, Key.NumPad3, Key.NumPad4, Key.NumPad5, Key.NumPad6, Key.NumPad7, Key.NumPad8, Key.NumPad9 + ) + } + return onPreviewKeyEvent { + if (it.key in numbers && it.type == KeyEventType.KeyDown) { + if (passcode.value.length < 16) { + passcode.value += numbers.indexOf(it.key) % 10 + } + true + } else if (it.key == Key.Backspace && it.type == KeyEventType.KeyDown && (it.isCtrlPressed || it.isMetaPressed)) { + passcode.value = "" + true + } else if (it.key == Key.Backspace && it.type == KeyEventType.KeyDown) { + passcode.value = passcode.value.dropLast(1) + true + } else if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) { + if ((submitEnabled?.invoke(passcode.value) != false && passcode.value.length >= 4)) { + submit() + } + true + } else { + false + } + } + } + @Composable fun VerticalLayout() { Column( + Modifier.handleKeyboard().focusRequester(focusRequester), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly ) { @@ -38,7 +73,7 @@ fun PasscodeView( } } PasscodeEntry(passcode, true) - Row { + Row(Modifier.heightIn(min = 70.dp), verticalAlignment = Alignment.CenterVertically) { SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), click = cancel) Spacer(Modifier.size(20.dp)) SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit) @@ -48,9 +83,9 @@ fun PasscodeView( @Composable fun HorizontalLayout() { - Row(Modifier.padding(horizontal = DEFAULT_PADDING), horizontalArrangement = Arrangement.Center) { + Row(Modifier.padding(horizontal = DEFAULT_PADDING).handleKeyboard().focusRequester(focusRequester), horizontalArrangement = Arrangement.Center) { Column( - Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4), + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween ) { @@ -64,7 +99,7 @@ fun PasscodeView( } Column( - Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4), + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween ) { @@ -90,9 +125,14 @@ fun PasscodeView( } } - if (screenOrientation() == ScreenOrientation.PORTRAIT) { + if (windowOrientation() == WindowOrientation.PORTRAIT || appPlatform.isDesktop) { VerticalLayout() } else { HorizontalLayout() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + // Disallow to steal a focus by clicking on buttons or using Tab + focusRequester.captureFocus() + } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasswordEntry.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasswordEntry.kt index ad7c26deb7..f76b82c31e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasswordEntry.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasswordEntry.kt @@ -1,7 +1,6 @@ package chat.simplex.common.views.localauth -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -16,6 +15,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.* +import chat.simplex.common.platform.appPlatform import chat.simplex.res.MR @Composable @@ -39,7 +39,7 @@ fun PasscodeEntry( fun PasscodeView(password: MutableState) { var showPasscode by rememberSaveable { mutableStateOf(false) } Text( - if (password.value.isEmpty()) " " else remember(password.value, showPasscode) { splitPassword(showPasscode, password.value) }, + if (password.value.isEmpty()) "" else remember(password.value, showPasscode) { splitPassword(showPasscode, password.value) }, Modifier.padding(vertical = 10.dp).clickable { showPasscode = !showPasscode }, style = MaterialTheme.typography.body1 ) @@ -47,7 +47,7 @@ fun PasscodeView(password: MutableState) { @Composable private fun BoxWithConstraintsScope.VerticalPasswordGrid(password: MutableState) { - val s = minOf(maxWidth, maxHeight) / 4 - 1.dp + val s = if (appPlatform.isAndroid) minOf(maxWidth, maxHeight) / 4 - 1.dp else minOf(minOf(maxWidth, maxHeight) / 4 - 1.dp, 100.dp) Column(Modifier.width(IntrinsicSize.Min)) { DigitsRow(s, 1, 2, 3, password) Divider() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 171465d86d..8ec54e344f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -97,7 +97,7 @@ private fun NewChatSheetLayout( } } val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp - val maxWidth = with(LocalDensity.current) { screenWidth() * density } + val maxWidth = with(LocalDensity.current) { windowWidth() * density } Column( Modifier .fillMaxSize() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt index 5aeddd2996..b758988227 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -36,18 +36,15 @@ private val settingsThemesProps = actual val settings: Settings = PropertiesSettings(settingsProps) { settingsProps.store(settingsFile.writer(), "") } actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { settingsThemesProps.store(settingsThemesFile.writer(), "") } -actual fun screenOrientation(): ScreenOrientation = ScreenOrientation.UNDEFINED - -@Composable // LALAL -actual fun screenWidth(): Dp { - return java.awt.Toolkit.getDefaultToolkit().screenSize.width.dp - /*var width by remember { mutableStateOf(java.awt.Toolkit.getDefaultToolkit().screenSize.width.also { println("LALAL $it") }) } - SideEffect { - if (width != java.awt.Toolkit.getDefaultToolkit().screenSize.width) - width = java.awt.Toolkit.getDefaultToolkit().screenSize.width +actual fun windowOrientation(): WindowOrientation = + if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) { + WindowOrientation.LANDSCAPE + } else { + WindowOrientation.PORTRAIT } - return width*/ -}// LALAL java.awt.Desktop.getDesktop() + +@Composable +actual fun windowWidth(): Dp = simplexWindowState.windowState.size.width actual fun desktopExpandWindowToWidth(width: Dp) { if (simplexWindowState.windowState.size.width >= width) return From a5940962c7bc01fa1a69120c3046159fa17e9e05 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 17 Aug 2023 15:04:17 +0400 Subject: [PATCH 03/14] ios: show chat previews & save last draft toggles (#2940) * ios: show chat previews & save last draft toggles * better view, toggle name --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../Chat/ComposeMessage/ComposeView.swift | 18 +++++++++++------- .../Views/ChatList/ChatPreviewView.swift | 9 ++++++++- .../Views/UserSettings/PrivacySettings.swift | 14 ++++++++++++++ .../Views/UserSettings/SettingsView.swift | 4 ++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index e2a6cd1d20..674f31bf71 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -255,6 +255,8 @@ struct ComposeView: View { // this is a workaround to fire an explicit event in certain cases @State private var stopPlayback: Bool = false + @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true + var body: some View { VStack(spacing: 0) { contextItemView() @@ -445,7 +447,15 @@ struct ComposeView: View { } else if (composeState.inProgress) { clearCurrentDraft() } else if !composeState.empty { - saveCurrentDraft() + if case .recording = composeState.voiceMessageRecordingState { + finishVoiceMessageRecording() + if let fileName = composeState.voiceMessageRecordingFileName { + chatModel.filesToDelete.insert(getAppFilePath(fileName)) + } + } + if saveLastDraft { + saveCurrentDraft() + } } else { cancelCurrentVoiceRecording() clearCurrentDraft() @@ -864,12 +874,6 @@ struct ComposeView: View { } private func saveCurrentDraft() { - if case .recording = composeState.voiceMessageRecordingState { - finishVoiceMessageRecording() - if let fileName = composeState.voiceMessageRecordingFileName { - chatModel.filesToDelete.insert(getAppFilePath(fileName)) - } - } chatModel.draft = composeState chatModel.draftChatId = chat.id } diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 4ddf75a1b3..7d38cd0d11 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -15,6 +15,8 @@ struct ChatPreviewView: View { @Environment(\.colorScheme) var colorScheme var darkGreen = Color(red: 0, green: 0.5, blue: 0) + @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true + var body: some View { let cItem = chat.chatItems.last return HStack(spacing: 8) { @@ -103,12 +105,17 @@ struct ChatPreviewView: View { private func chatPreviewLayout(_ text: Text) -> some View { ZStack(alignment: .topTrailing) { - text + let t = text .lineLimit(2) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .topLeading) .padding(.leading, 8) .padding(.trailing, 36) + if showChatPreviews { + t + } else { + t.privacySensitive(!showChatPreviews).redacted(reason: .privacy) + } let s = chat.chatStats if s.unreadCount > 0 || s.unreadChat { unreadCountText(s.unreadCount) diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index ecbacf0830..c793206c7f 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -13,6 +13,8 @@ struct PrivacySettings: View { @EnvironmentObject var m: ChatModel @AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true @AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true + @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true + @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @@ -70,6 +72,18 @@ struct PrivacySettings: View { settingsRow("network") { Toggle("Send link previews", isOn: $useLinkPreviews) } + settingsRow("message") { + Toggle("Show last messages", isOn: $showChatPreviews) + } + settingsRow("rectangle.and.pencil.and.ellipsis") { + Toggle("Message draft", isOn: $saveLastDraft) + } + .onChange(of: saveLastDraft) { saveDraft in + if !saveDraft { + m.draft = nil + m.draftChatId = nil + } + } settingsRow("link") { Picker("SimpleX links", selection: $simplexLinkMode) { ForEach(SimpleXLinkMode.values) { mode in diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 72cfaaac48..d23380d6b0 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -30,6 +30,8 @@ let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents" let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode" +let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews" +let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft" let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen" let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet" let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls" @@ -65,6 +67,8 @@ let appDefaults: [String: Any] = [ DEFAULT_PRIVACY_ACCEPT_IMAGES: true, DEFAULT_PRIVACY_LINK_PREVIEWS: true, DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue, + DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true, + DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true, DEFAULT_PRIVACY_PROTECT_SCREEN: false, DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false, DEFAULT_EXPERIMENTAL_CALLS: false, From 590644a359235009629d4c00121e08e7630780a8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:52:03 +0100 Subject: [PATCH 04/14] ios: show draft even when messages in the list are hidden (#2944) --- .../ios/Shared/Views/ChatList/ChatPreviewView.swift | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 7d38cd0d11..eccf0b5b42 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -103,19 +103,16 @@ struct ChatPreviewView: View { .kerning(-2) } - private func chatPreviewLayout(_ text: Text) -> some View { + private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View { ZStack(alignment: .topTrailing) { - let t = text + text .lineLimit(2) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .topLeading) .padding(.leading, 8) .padding(.trailing, 36) - if showChatPreviews { - t - } else { - t.privacySensitive(!showChatPreviews).redacted(reason: .privacy) - } + .privacySensitive(!showChatPreviews && !draft) + .redacted(reason: .privacy) let s = chat.chatStats if s.unreadCount > 0 || s.unreadChat { unreadCountText(s.unreadCount) @@ -177,7 +174,7 @@ struct ChatPreviewView: View { @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View { if chatModel.draftChatId == chat.id, let draft = chatModel.draft { - chatPreviewLayout(messageDraft(draft)) + chatPreviewLayout(messageDraft(draft), draft: true) } else if let cItem = cItem { chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem)) } else { From faea5e90acd21983fbf9037a72a5887429dc7ff5 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 17 Aug 2023 15:56:43 +0400 Subject: [PATCH 05/14] android: members connected aggregated item; group layout (#2934) --- apps/ios/Shared/Model/ChatModel.swift | 2 +- .../Chat/ChatItem/CIRcvDecryptionError.swift | 21 ++--- apps/ios/SimpleXChat/ChatTypes.swift | 5 -- .../chat/simplex/common/model/ChatModel.kt | 25 ++++++ .../simplex/common/views/chat/ChatView.kt | 88 ++++++++++++++----- .../simplex/common/views/chat/ComposeView.kt | 7 ++ .../common/views/chat/ContextItemView.kt | 34 +++++-- .../simplex/common/views/chat/SendMsgView.kt | 8 +- .../common/views/chat/item/CIEventView.kt | 32 +------ .../views/chat/item/CIRcvDecryptionError.kt | 10 --- .../common/views/chat/item/ChatItemView.kt | 79 ++++++++++++++--- .../common/views/chat/item/DeletedItemView.kt | 3 +- .../common/views/chat/item/FramedItemView.kt | 50 +++++++---- .../views/chat/item/IntegrityErrorItemView.kt | 7 +- .../views/chat/item/MarkedDeletedItemView.kt | 3 +- .../common/views/chat/item/TextItemView.kt | 9 -- .../commonMain/resources/MR/base/strings.xml | 4 + 17 files changed, 252 insertions(+), 135 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 390e25bed1..229408a4e2 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -487,7 +487,7 @@ final class ChatModel: ObservableObject { guard var i = getChatItemIndex(ci) else { return [] } var ns: [String] = [] while i < reversedChatItems.count, let m = reversedChatItems[i].memberConnected { - ns.append(m.chatViewName) + ns.append(m.displayName) i += 1 } return ns diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index f4f8a52eb2..2e0a19eada 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -16,7 +16,6 @@ struct CIRcvDecryptionError: View { var msgDecryptError: MsgDecryptError var msgCount: UInt32 var chatItem: ChatItem - var showMember = false @State private var alert: CIRcvDecryptionErrorAlert? enum CIRcvDecryptionErrorAlert: Identifiable { @@ -106,9 +105,6 @@ struct CIRcvDecryptionError: View { ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading, spacing: 2) { HStack { - if showMember, let member = chatItem.memberDisplayName { - Text(member).fontWeight(.medium) + Text(": ") - } Text(chatItem.content.text) .foregroundColor(.red) .italic() @@ -137,20 +133,13 @@ struct CIRcvDecryptionError: View { } private func decryptionErrorItem(_ onClick: @escaping (() -> Void)) -> some View { - func text() -> Text { - Text(chatItem.content.text) - .foregroundColor(.red) - .italic() - + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, transparent: true) - } return ZStack(alignment: .bottomTrailing) { HStack { - if showMember, let member = chatItem.memberDisplayName { - Text(member).fontWeight(.medium) + Text(": ") + text() - } else { - text() - } + Text(chatItem.content.text) + .foregroundColor(.red) + .italic() + + Text(" ") + + ciMetaText(chatItem.meta, chatTTL: nil, transparent: true) } .padding(.horizontal, 12) CIMetaView(chatItem: chatItem) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 5bdd03ace6..de52328ca4 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2533,17 +2533,12 @@ public enum CIContent: Decodable, ItemContent { public var showMemberName: Bool { switch self { - case .sndMsgContent: return true case .rcvMsgContent: return true - case .sndDeleted: return true case .rcvDeleted: return true - case .sndCall: return true case .rcvCall: return true case .rcvIntegrityError: return true case .rcvDecryptionError: return true case .rcvGroupInvitation: return true - case .sndChatPreference: return true - case .sndModerated: return true case .rcvModerated: return true case .invalidJSON: return true default: return false diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 9762ec72a2..a0c31496e6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1385,6 +1385,18 @@ data class ChatItem ( else -> false } + val memberConnected: GroupMember? get() = + when (chatDir) { + is CIDirection.GroupRcv -> when (content) { + is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) { + is RcvGroupEvent.MemberConnected -> chatDir.groupMember + else -> null + } + else -> null + } + else -> null + } + fun memberToModerate(chatInfo: ChatInfo): Pair? { return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) { val m = chatInfo.groupInfo.membership @@ -1838,6 +1850,19 @@ sealed class CIContent: ItemContent { is InvalidJSON -> "invalid data" } + val showMemberName: Boolean get() = + when (this) { + is RcvMsgContent -> true + is RcvDeleted -> true + is RcvCall -> true + is RcvIntegrityError -> true + is RcvDecryptionError -> true + is RcvGroupInvitation -> true + is RcvModerated -> true + is InvalidJSON -> true + else -> false + } + companion object { fun featureText(feature: Feature, enabled: String, param: Int?): String = if (feature.hasParam) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 749afe8fa6..78b379df74 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -17,11 +17,11 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.text.* import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* @@ -718,7 +718,7 @@ fun BoxWithConstraintsScope.ChatItemsList( } ) LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) { - itemsIndexed(reversedChatItems, key = { _, item -> item.id}) { i, cItem -> + itemsIndexed(reversedChatItems, key = { _, item -> item.id }) { i, cItem -> CompositionLocalProvider( // Makes horizontal and vertical scrolling to coexist nicely. // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view @@ -760,27 +760,73 @@ fun BoxWithConstraintsScope.ChatItemsList( if (chat.chatInfo is ChatInfo.Group) { if (cItem.chatDir is CIDirection.GroupRcv) { val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null - val member = cItem.chatDir.groupMember - val showMember = showMemberImage(member, prevItem) - Row(Modifier.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp).then(swipeableModifier)) { - if (showMember) { - Box( - Modifier - .clip(CircleShape) - .clickable { - showMemberInfo(chat.chatInfo.groupInfo, member) - } - ) { - MemberImage(member) + val nextItem = if (i - 1 >= 0) reversedChatItems[i - 1] else null + fun getConnectedMemberNames(): List { + val ns = mutableListOf() + var idx = i + while (idx < reversedChatItems.size) { + val m = reversedChatItems[idx].memberConnected + if (m != null) { + ns.add(m.displayName) + } else { + break + } + idx++ + } + return ns + } + if (cItem.memberConnected != null && nextItem?.memberConnected != null) { + // memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView + Box(Modifier.size(0.dp)) {} + } else { + val member = cItem.chatDir.groupMember + if (showMemberImage(member, prevItem)) { + Column( + Modifier + .padding(top = 8.dp) + .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.Start + ) { + if (cItem.content.showMemberName) { + Text( + member.displayName, + Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp), + style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + ) + } + Row( + swipeableModifier, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Box( + Modifier + .clip(CircleShape) + .clickable { + showMemberInfo(chat.chatInfo.groupInfo, member) + } + ) { + MemberImage(member) + } + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames) + } + } + } else { + Row( + Modifier + .padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp) + .then(swipeableModifier) + ) { + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames) } - Spacer(Modifier.size(4.dp)) - } else { - Spacer(Modifier.size(42.dp)) } - ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) } } else { - Box(Modifier.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp).then(swipeableModifier)) { + Box( + Modifier + .padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp) + .then(swipeableModifier) + ) { ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) } } @@ -973,9 +1019,11 @@ fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean { (prevItem.chatDir is CIDirection.GroupRcv && prevItem.chatDir.groupMember.groupMemberId != member.groupMemberId) } +val MEMBER_IMAGE_SIZE: Dp = 38.dp + @Composable fun MemberImage(member: GroupMember) { - ProfileImage(38.dp, member.memberProfile.image) + ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image) } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 9be98525a1..234880a41d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -16,6 +16,8 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.Indigo +import chat.simplex.common.ui.theme.isSystemInDarkTheme import chat.simplex.common.views.chat.item.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @@ -753,6 +755,10 @@ fun ComposeView( } val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) } + val sendButtonColor = + if (chat.chatInfo.incognito) + if (isSystemInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F) + else MaterialTheme.colors.primary SendMsgView( composeState, showVoiceRecordIcon = true, @@ -764,6 +770,7 @@ fun ComposeView( allowVoiceToContact = ::allowVoiceToContact, userIsObserver = userIsObserver.value, userCanSend = userCanSend.value, + sendButtonColor = sendButtonColor, timedMessageAllowed = timedMessageAllowed, customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, sendMessage = { ttl -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt index 9a4d4c3775..49203c7cfb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt @@ -11,7 +11,9 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.* import chat.simplex.common.model.* @@ -27,6 +29,17 @@ fun ContextItemView( val sent = contextItem.chatDir.sent val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + + @Composable + fun msgContentView(lines: Int) { + MarkdownText( + contextItem.text, contextItem.formattedText, + maxLines = lines, + linkMode = SimplexLinkMode.DESCRIPTION, + modifier = Modifier.fillMaxWidth(), + ) + } + Row( Modifier .padding(top = 8.dp) @@ -49,12 +62,21 @@ fun ContextItemView( contentDescription = stringResource(MR.strings.icon_descr_context), tint = MaterialTheme.colors.secondary, ) - MarkdownText( - contextItem.text, contextItem.formattedText, - sender = contextItem.memberDisplayName, senderBold = true, maxLines = 3, - linkMode = SimplexLinkMode.DESCRIPTION, - modifier = Modifier.fillMaxWidth(), - ) + val sender = contextItem.memberDisplayName + if (sender != null) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + sender, + style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + ) + msgContentView(lines = 2) + } + } else { + msgContentView(lines = 3) + } } IconButton(onClick = cancelContextItem) { Icon( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 0d07fe6268..205f18c46a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -41,6 +41,7 @@ fun SendMsgView( allowedVoiceByPrefs: Boolean, userIsObserver: Boolean, userCanSend: Boolean, + sendButtonColor: Color = MaterialTheme.colors.primary, allowVoiceToContact: () -> Unit, timedMessageAllowed: Boolean = false, customDisappearingMessageTimePref: SharedPreference? = null, @@ -194,12 +195,12 @@ fun SendMsgView( val menuItems = MenuItems() if (menuItems.isNotEmpty()) { - SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true } + SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !disabled, sendMessage) { showDropdown.value = true } DefaultDropdownMenu(showDropdown) { menuItems.forEach { composable -> composable() } } } else { - SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) + SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !disabled, sendMessage) } } } @@ -449,6 +450,7 @@ private fun SendMsgButton( icon: Painter, sizeDp: Animatable, alpha: Animatable, + sendButtonColor: Color, enabled: Boolean, sendMessage: (Int?) -> Unit, onLongClick: (() -> Unit)? = null @@ -476,7 +478,7 @@ private fun SendMsgButton( .padding(4.dp) .alpha(alpha.value) .clip(CircleShape) - .background(if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) + .background(if (enabled) sendButtonColor else MaterialTheme.colors.secondary) .padding(3.dp) ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt index 09d0bd4d04..508116f7e3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt @@ -8,43 +8,19 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.common.model.ChatItem import chat.simplex.common.ui.theme.* @Composable -fun CIEventView(ci: ChatItem) { - @Composable - fun chatEventTextView(text: AnnotatedString) { - Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)) - } +fun CIEventView(text: AnnotatedString) { Row( Modifier.padding(horizontal = 6.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically ) { - val memberDisplayName = ci.memberDisplayName - if (memberDisplayName != null) { - chatEventTextView( - buildAnnotatedString { - withStyle(chatEventStyle) { append(memberDisplayName) } - append(" ") - }.plus(chatEventText(ci)) - ) - } else { - chatEventTextView(chatEventText(ci)) - } + Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)) } } - -val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary) - -fun chatEventText(ci: ChatItem): AnnotatedString = - buildAnnotatedString { - withStyle(chatEventStyle) { append(ci.content.text + " " + ci.timestampText) } - } - @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode" @@ -52,8 +28,6 @@ fun chatEventText(ci: ChatItem): AnnotatedString = @Composable fun CIEventViewPreview() { SimpleXTheme { - CIEventView( - ChatItem.getGroupEventSample() - ) + CIEventView(buildAnnotatedString { append("event happened") }) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt index 2ce52f9712..2918d885b1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt @@ -32,7 +32,6 @@ fun CIRcvDecryptionError( syncMemberConnection: (GroupInfo, GroupMember) -> Unit, findModelChat: (String) -> Chat?, findModelMember: (String) -> GroupMember?, - showMember: Boolean ) { LaunchedEffect(Unit) { if (cInfo is ChatInfo.Direct) { @@ -46,7 +45,6 @@ fun CIRcvDecryptionError( fun BasicDecryptionErrorItem() { DecryptionErrorItem( ci, - showMember, onClick = { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.decryption_error), @@ -64,7 +62,6 @@ fun CIRcvDecryptionError( if (modelContactStats.ratchetSyncAllowed) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.fix_connection_question), @@ -78,7 +75,6 @@ fun CIRcvDecryptionError( } else if (!modelContactStats.ratchetSyncSupported) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.fix_connection_not_supported_by_contact), @@ -103,7 +99,6 @@ fun CIRcvDecryptionError( if (modelMemberStats.ratchetSyncAllowed) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.fix_connection_question), @@ -117,7 +112,6 @@ fun CIRcvDecryptionError( } else if (!modelMemberStats.ratchetSyncSupported) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.fix_connection_not_supported_by_group_member), @@ -140,7 +134,6 @@ fun CIRcvDecryptionError( @Composable fun DecryptionErrorItemFixButton( ci: ChatItem, - showMember: Boolean, onClick: () -> Unit, syncSupported: Boolean ) { @@ -159,7 +152,6 @@ fun DecryptionErrorItemFixButton( ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp) @@ -189,7 +181,6 @@ fun DecryptionErrorItemFixButton( @Composable fun DecryptionErrorItem( ci: ChatItem, - showMember: Boolean, onClick: () -> Unit ) { val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage @@ -204,7 +195,6 @@ fun DecryptionErrorItem( ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) } }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 0597e2d131..2857d6acc8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.* -import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight @@ -29,13 +29,22 @@ import kotlinx.datetime.Clock // TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code) +val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary) + +fun chatEventText(ci: ChatItem): AnnotatedString = + chatEventText(ci.content.text, ci.timestampText) + +fun chatEventText(eventText: String, ts: String): AnnotatedString = + buildAnnotatedString { + withStyle(chatEventStyle) { append("$eventText $ts") } + } + @Composable fun ChatItemView( cInfo: ChatInfo, cItem: ChatItem, composeState: MutableState, imageProvider: (() -> ImageGalleryProvider)? = null, - showMember: Boolean = false, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, deleteMessage: (Long, CIDeleteMode) -> Unit, @@ -53,6 +62,7 @@ fun ChatItemView( findModelMember: (String) -> GroupMember?, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, + getConnectedMemberNames: (() -> List)? = null, ) { val uriHandler = LocalUriHandler.current val sent = cItem.chatDir.sent @@ -95,7 +105,8 @@ fun ChatItemView( ReactionIcon(r.reaction.text, fontSize = 12.sp) if (r.totalReacted > 1) { Spacer(Modifier.width(4.dp)) - Text("${r.totalReacted}", + Text( + "${r.totalReacted}", fontSize = 11.5.sp, fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal, color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, @@ -116,7 +127,7 @@ fun ChatItemView( ) { @Composable fun framedItemView() { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) + FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) } fun deleteMessageQuestionText(): String { @@ -246,7 +257,7 @@ fun ChatItemView( fun ContentItem() { val mc = cItem.content.msgContent if (cItem.meta.itemDeleted != null && !revealed.value) { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL) MarkedDeletedItemDropdownMenu() } else { if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { @@ -265,7 +276,7 @@ fun ChatItemView( } @Composable fun DeletedItem() { - DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) + DeletedItemView(cItem, cInfo.timedMessagesTTL) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) @@ -276,9 +287,48 @@ fun ChatItemView( CICallItemView(cInfo, cItem, status, duration, acceptCall) } + fun eventItemViewText(): AnnotatedString { + val memberDisplayName = cItem.memberDisplayName + return if (memberDisplayName != null) { + buildAnnotatedString { + withStyle(chatEventStyle) { append(memberDisplayName) } + append(" ") + }.plus(chatEventText(cItem)) + } else { + chatEventText(cItem) + } + } + + @Composable fun EventItemView() { + CIEventView(eventItemViewText()) + } + + fun membersConnectedText(): String? { + return if (getConnectedMemberNames != null) { + val ns = getConnectedMemberNames() + when { + ns.size > 3 -> String.format(generalGetString(MR.strings.rcv_group_event_n_members_connected), ns[0], ns[1], ns.size - 2) + ns.size == 3 -> String.format(generalGetString(MR.strings.rcv_group_event_3_members_connected), ns[0], ns[1], ns[2]) + ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1]) + else -> null + } + } else { + null + } + } + + fun membersConnectedItemText(): AnnotatedString { + val t = membersConnectedText() + return if (t != null) { + chatEventText(t, cItem.timestampText) + } else { + eventItemViewText() + } + } + @Composable fun ModeratedItem() { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage) @@ -292,14 +342,17 @@ fun ChatItemView( is CIContent.RcvDeleted -> DeletedItem() is CIContent.SndCall -> CallItem(c.status, c.duration) is CIContent.RcvCall -> CallItem(c.status, c.duration) - is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember) - is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, showMember = showMember) + is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL) + is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember) is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) - is CIContent.RcvGroupEventContent -> CIEventView(cItem) - is CIContent.SndGroupEventContent -> CIEventView(cItem) - is CIContent.RcvConnEventContent -> CIEventView(cItem) - is CIContent.SndConnEventContent -> CIEventView(cItem) + is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) { + is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText()) + else -> EventItemView() + } + is CIContent.SndGroupEventContent -> EventItemView() + is CIContent.RcvConnEventContent -> EventItemView() + is CIContent.SndConnEventContent -> EventItemView() is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor) is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor) is CIContent.RcvChatPreference -> { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt index 3d8d9f7940..2d949e1737 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt @@ -16,7 +16,7 @@ import chat.simplex.common.model.ChatItem import chat.simplex.common.ui.theme.* @Composable -fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { +fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { val sent = ci.chatDir.sent val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage @@ -30,7 +30,6 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(ci.content.text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index faa7dfdbf2..0767862a4a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -23,6 +23,7 @@ import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.base64ToBitmap +import chat.simplex.common.views.chat.MEMBER_IMAGE_SIZE import chat.simplex.res.MR import kotlin.math.min @@ -32,7 +33,6 @@ fun FramedItemView( ci: ChatItem, uriHandler: UriHandler? = null, imageProvider: (() -> ImageGalleryProvider)? = null, - showMember: Boolean = false, linkMode: SimplexLinkMode, showMenu: MutableState, receiveFile: (Long) -> Unit, @@ -49,17 +49,38 @@ fun FramedItemView( @Composable fun Color.toQuote(): Color = if (isInDarkTheme()) lighter(0.12f) else darker(0.12f) + @Composable + fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) { + MarkdownText( + qi.text, + qi.formattedText, + maxLines = lines, + overflow = TextOverflow.Ellipsis, + style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface), + linkMode = linkMode + ) + } + @Composable fun ciQuotedMsgView(qi: CIQuote) { Box( Modifier.padding(vertical = 6.dp, horizontal = 12.dp), contentAlignment = Alignment.TopStart ) { - MarkdownText( - qi.text, qi.formattedText, sender = qi.sender(membership()), senderBold = true, maxLines = 3, - style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface), - linkMode = linkMode - ) + val sender = qi.sender(membership()) + if (sender != null) { + Column( + horizontalAlignment = Alignment.Start + ) { + Text( + sender, + style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + ) + ciQuotedMsgTextView(qi, lines = 2) + } + } else { + ciQuotedMsgTextView(qi, lines = 3) + } } } @@ -156,7 +177,7 @@ fun FramedItemView( fun ciFileView(ci: ChatItem, text: String) { CIFileView(ci.file, ci.meta.itemEdited, receiveFile) if (text != "" || ci.meta.isLive) { - CIMarkdownText(ci, chatTTL, showMember, linkMode = linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler) } } @@ -207,7 +228,7 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler) } } is MsgContent.MCVideo -> { @@ -215,29 +236,29 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler) } } is MsgContent.MCVoice -> { CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile) if (mc.text != "") { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler) } } is MsgContent.MCFile -> ciFileView(ci, mc.text) is MsgContent.MCUnknown -> if (ci.file == null) { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) } else { ciFileView(ci, mc.text) } is MsgContent.MCLink -> { ChatItemLinkView(mc.preview) Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) } } - else -> CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick) + else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) } } } @@ -253,7 +274,6 @@ fun FramedItemView( fun CIMarkdownText( ci: ChatItem, chatTTL: Int?, - showMember: Boolean, linkMode: SimplexLinkMode, uriHandler: UriHandler?, onLinkLongClick: (link: String) -> Unit = {} @@ -261,7 +281,7 @@ fun CIMarkdownText( Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text MarkdownText( - text, if (text.isEmpty()) emptyList() else ci.formattedText, if (showMember) ci.memberDisplayName else null, + text, if (text.isEmpty()) emptyList() else ci.formattedText, meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode, uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt index 7cd996ec25..582730b8f5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt @@ -24,8 +24,8 @@ import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @Composable -fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { - CIMsgError(ci, timedMessagesTTL, showMember) { +fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?) { + CIMsgError(ci, timedMessagesTTL) { when (msgError) { is MsgErrorType.MsgSkipped -> AlertManager.shared.showAlertMsg( @@ -50,7 +50,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT } @Composable -fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false, onClick: () -> Unit) { +fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), @@ -63,7 +63,6 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt index 8ced77289c..84675a09b4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt @@ -20,7 +20,7 @@ import chat.simplex.res.MR import kotlinx.datetime.Clock @Composable -fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { +fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( @@ -47,7 +47,6 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool private fun MarkedDeletedText(text: String) { Text( buildAnnotatedString { - // appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index 24aad7fdd8..af7d32d52d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -25,15 +25,6 @@ import kotlinx.coroutines.* val reserveTimestampStyle = SpanStyle(color = Color.Transparent) val boldFont = SpanStyle(fontWeight = FontWeight.Medium) -fun appendGroupMember(b: AnnotatedString.Builder, chatItem: ChatItem, groupMemberBold: Boolean) { - if (chatItem.chatDir is CIDirection.GroupRcv) { - val name = chatItem.chatDir.groupMember.memberProfile.displayName - if (groupMemberBold) b.withStyle(boldFont) { append(name) } - else b.append(name) - b.append(": ") - } -} - fun appendSender(b: AnnotatedString.Builder, sender: String?, senderBold: Boolean) { if (sender != null) { if (senderBold) b.withStyle(boldFont) { append(sender) } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index dcc421182f..5449dd6a86 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1103,6 +1103,10 @@ you left group profile updated + %s and %s connected + %s, %s and %s connected + %s, %s and %d other members connected + changed address for you changing address… From 1d8a370c5856a53f75f828157843d7f46c4553a6 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 17 Aug 2023 18:04:16 +0400 Subject: [PATCH 06/14] android: show chat previews & save last draft toggles (#2941) * android: show chat previews & save last draft toggles * wip * Revert "wip" This reverts commit 01b570913fc8b8ed426fe7bc1e3be104041e6077. * comment * add to model * toggle text and icon * show draft --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../chat/simplex/common/model/ChatModel.kt | 1 + .../chat/simplex/common/model/SimpleXAPI.kt | 4 ++ .../simplex/common/views/chat/ComposeView.kt | 7 ++- .../views/chatlist/ChatListNavLinkView.kt | 7 ++- .../common/views/chatlist/ChatPreviewView.kt | 53 ++++++++++--------- .../views/usersettings/PrivacySettings.kt | 18 +++++++ .../commonMain/resources/MR/base/strings.xml | 2 + .../resources/MR/images/ic_chat_bubble.svg | 1 + 8 files changed, 64 insertions(+), 29 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index a0c31496e6..7d7fb96481 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -80,6 +80,7 @@ object ChatModel { } val performLA by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) } val showAdvertiseLAUnavailableAlert = mutableStateOf(false) + val showChatPreviews by lazy { mutableStateOf(ChatController.appPrefs.privacyShowChatPreviews.get()) } // current WebRTC call val callManager = CallManager(this) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 519f62909d..363b2740c2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -91,6 +91,8 @@ class AppPreferences { }, set = fun(mode: SimplexLinkMode) { _simplexLinkMode.set(mode.name) } ) + val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true) + val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) @@ -244,6 +246,8 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline" private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews" private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode" + private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews" + private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 234880a41d..6c66ee2b9b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -232,6 +232,7 @@ fun ComposeView( val pendingLinkUrl = rememberSaveable { mutableStateOf(null) } val cancelledLinks = rememberSaveable { mutableSetOf() } val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() + val saveLastDraft = chatModel.controller.appPrefs.privacySaveLastDraft.get() val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) } val recState: MutableState = remember { mutableStateOf(RecordingState.NotStarted) } @@ -742,8 +743,10 @@ fun ComposeView( if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) { composeState.value = cs.copy(preview = cs.preview.copy(finished = true)) } - chatModel.draft.value = composeState.value - chatModel.draftChatId.value = prevChatId + if (saveLastDraft) { + chatModel.draft.value = composeState.value + chatModel.draftChatId.value = prevChatId + } composeState.value = ComposeState(useLinkPreviews = useLinkPreviews) } else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) { composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 18df5e8502..4b305be293 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -44,11 +44,12 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { showMenu.value = false delay(500L) } + val showChatPreviews = chatModel.showChatPreviews.value when (chat.chatInfo) { is ChatInfo.Direct -> { val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact) ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) }, + chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) }, click = { directChatAction(chat.chatInfo, chatModel) }, dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) }, showMenu, @@ -57,7 +58,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } is ChatInfo.Group -> ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) }, + chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) }, click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) }, dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) }, showMenu, @@ -668,6 +669,7 @@ fun PreviewChatListNavLinkDirect() { ), chatStats = Chat.ChatStats() ), + true, null, null, null, @@ -707,6 +709,7 @@ fun PreviewChatListNavLinkGroup() { ), chatStats = Chat.ChatStats() ), + true, null, null, null, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index bd030438e9..95467111e5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -31,6 +31,7 @@ import dev.icerock.moko.resources.ImageResource @Composable fun ChatPreviewView( chat: Chat, + showChatPreviews: Boolean, chatModelDraft: ComposeState?, chatModelDraftChatId: ChatId?, currentUserProfileDisplayName: String?, @@ -140,32 +141,34 @@ fun ChatPreviewView( fun chatPreviewText() { val ci = chat.chatItems.lastOrNull() if (ci != null) { - val (text: CharSequence, inlineTextContent) = when { - chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) } - ci.meta.itemDeleted == null -> ci.text to null - else -> generalGetString(MR.strings.marked_deleted_description) to null - } - val formattedText = when { - chatModelDraftChatId == chat.id && chatModelDraft != null -> null - ci.meta.itemDeleted == null -> ci.formattedText - else -> null - } - MarkdownText( - text, - formattedText, - sender = when { + if (showChatPreviews || (chatModelDraftChatId == chat.id && chatModelDraft != null)) { + val (text: CharSequence, inlineTextContent) = when { + chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) } + ci.meta.itemDeleted == null -> ci.text to null + else -> generalGetString(MR.strings.marked_deleted_description) to null + } + val formattedText = when { chatModelDraftChatId == chat.id && chatModelDraft != null -> null - cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName + ci.meta.itemDeleted == null -> ci.formattedText else -> null - }, - linkMode = linkMode, - senderBold = true, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp), - inlineContent = inlineTextContent, - modifier = Modifier.fillMaxWidth(), - ) + } + MarkdownText( + text, + formattedText, + sender = when { + chatModelDraftChatId == chat.id && chatModelDraft != null -> null + cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName + else -> null + }, + linkMode = linkMode, + senderBold = true, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp), + inlineContent = inlineTextContent, + modifier = Modifier.fillMaxWidth(), + ) + } } else { when (cInfo) { is ChatInfo.Direct -> @@ -336,6 +339,6 @@ fun unreadCountStr(n: Int): String { @Composable fun PreviewChatPreviewView() { SimpleXTheme { - ChatPreviewView(Chat.sampleData, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION) + ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index a46decb3d0..81d56a3816 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -66,6 +66,24 @@ fun PrivacySettingsView( SectionView(stringResource(MR.strings.settings_section_title_chats)) { SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) + SettingsPreferenceItem( + painterResource(MR.images.ic_chat_bubble), + stringResource(MR.strings.privacy_show_last_messages), + chatModel.controller.appPrefs.privacyShowChatPreviews, + onChange = { showPreviews -> + chatModel.showChatPreviews.value = showPreviews + } + ) + SettingsPreferenceItem( + painterResource(MR.images.ic_edit_note), + stringResource(MR.strings.privacy_message_draft), + chatModel.controller.appPrefs.privacySaveLastDraft, + onChange = { saveDraft -> + if (!saveDraft) { + chatModel.draft.value = null + chatModel.draftChatId.value = null + } + }) SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { simplexLinkMode.set(it) chatModel.simplexLinkMode.value = it diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 5449dd6a86..ea6d13a355 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -852,6 +852,8 @@ Protect app screen Auto-accept images Send link previews + Show last messages + Message draft App data backup Enable lock Lock mode diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg new file mode 100644 index 0000000000..132c514e92 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg @@ -0,0 +1 @@ + \ No newline at end of file From 107b6e1aec9e4b6acb31550bc30c3aa7fe9efe39 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:06:42 +0300 Subject: [PATCH 07/14] multiplatform: mark read fix (#2932) Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../kotlin/chat/simplex/common/views/chat/ChatView.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 78b379df74..fbe5746c68 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -66,11 +66,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { launch { snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() - .collect { - if (activeChat.value?.id != chatModel.chatId.value && chatModel.chatId.value != null) { + .collect { chatId -> + if (activeChat.value?.id != chatId && chatId != null) { // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly // Also for situation when chatId changes after clicking in notification, etc - activeChat.value = chatModel.getChat(chatModel.chatId.value!!) + activeChat.value = chatModel.getChat(chatId) } markUnreadChatAsRead(activeChat, chatModel) } @@ -842,7 +842,7 @@ fun BoxWithConstraintsScope.ChatItemsList( } } - if (cItem.isRcvNew) { + if (cItem.isRcvNew && chat.id == ChatModel.chatId.value) { LaunchedEffect(cItem.id) { scope.launch { delay(600) From 63ca7a34ffe8546c9e958a526992a3a7283cb614 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:20:27 +0300 Subject: [PATCH 08/14] desktop: clickable links in quoted messages (#2943) --- .../chat/simplex/common/views/chat/item/FramedItemView.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 0767862a4a..9a28272805 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.platform.appPlatform import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.base64ToBitmap @@ -57,7 +58,8 @@ fun FramedItemView( maxLines = lines, overflow = TextOverflow.Ellipsis, style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface), - linkMode = linkMode + linkMode = linkMode, + uriHandler = if (appPlatform.isDesktop) uriHandler else null ) } From ea397049f8804db1fab22758c9f1ebfca1668687 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:26:11 +0300 Subject: [PATCH 09/14] multiplatform: ChatView enhancements (#2945) * multiplatform: ChatView enhancements * removed unused code --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../simplex/common/platform/RecAndPlay.kt | 2 +- .../simplex/common/views/chat/ChatView.kt | 26 ++++++++++--------- .../views/chatlist/ChatListNavLinkView.kt | 4 +-- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt index be2c87a2fc..bbc5cbe667 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt @@ -14,7 +14,7 @@ interface RecorderInterface { fun stop(): Int } -expect class RecorderNative: RecorderInterface +expect class RecorderNative(): RecorderInterface interface AudioPlayerInterface { fun play( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index fbe5746c68..57713a27a5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -105,7 +105,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { // Having activeChat reloaded on every change in it is inefficient (UI lags) val unreadCount = remember { derivedStateOf { - chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }?.chatStats?.unreadCount ?: 0 + chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0 } } val clipboard = LocalClipboardManager.current @@ -974,17 +974,19 @@ fun BoxWithConstraintsScope.FloatingButtons( onLongClick = { showDropDown.value = true } ) - DefaultDropdownMenu(showDropDown, offset = DpOffset(maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) { - ItemAction( - generalGetString(MR.strings.mark_read), - painterResource(MR.images.ic_check), - onClick = { - markRead( - CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), - bottomUnreadCount - ) - showDropDown.value = false - }) + Box { + DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) { + ItemAction( + generalGetString(MR.strings.mark_read), + painterResource(MR.images.ic_check), + onClick = { + markRead( + CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), + bottomUnreadCount + ) + showDropDown.value = false + }) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 4b305be293..3886fc8c29 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -104,7 +104,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { fun directChatAction(chatInfo: ChatInfo, chatModel: ChatModel) { if (chatInfo.ready) { - withApi { openChat(chatInfo, chatModel) } + withBGApi { openChat(chatInfo, chatModel) } } else { pendingContactAlertDialog(chatInfo, chatModel) } @@ -114,7 +114,7 @@ fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) { when (groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(groupInfo, chatModel) GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert() - else -> withApi { openChat(ChatInfo.Group(groupInfo), chatModel) } + else -> withBGApi { openChat(ChatInfo.Group(groupInfo), chatModel) } } } From f1c86d20c9827dce452e184a0bd00582d3e2a239 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 17 Aug 2023 18:25:52 +0300 Subject: [PATCH 10/14] desktop: cursor on hover over clickable link (#2946) * desktop: cursor on hover over clickable link * simplex link --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../common/views/chat/item/TextItemView.kt | 33 +++++++++++++++---- .../common/views/helpers/GestureDetector.kt | 11 +++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index af7d32d52d..64855e3195 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -1,26 +1,28 @@ package chat.simplex.common.views.chat.item -import androidx.compose.foundation.text.* +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.* import androidx.compose.ui.platform.* import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.* +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.sp import chat.simplex.common.model.* -import chat.simplex.common.platform.Log -import chat.simplex.common.platform.TAG +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.helpers.* import kotlinx.coroutines.* +import java.awt.* val reserveTimestampStyle = SpanStyle(color = Color.Transparent) val boldFont = SpanStyle(fontWeight = FontWeight.Medium) @@ -156,7 +158,8 @@ fun MarkdownText ( else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) } } if (hasLinks && uriHandler != null) { - ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, + val icon = remember { mutableStateOf(PointerIcon.Default) } + ClickableText(annotatedText, style = style, modifier = modifier.pointerHoverIcon(icon.value), maxLines = maxLines, overflow = overflow, onLongClick = { offset -> annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) .firstOrNull()?.let { annotation -> onLinkLongClick(annotation.item) } @@ -179,6 +182,15 @@ fun MarkdownText ( uriHandler.openVerifiedSimplexUri(annotation.item) } }, + onHover = { offset -> + icon.value = annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) + .firstOrNull()?.let { + PointerIcon.Hand + } ?: annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) + .firstOrNull()?.let { + PointerIcon.Hand + } ?: PointerIcon.Default + }, shouldConsumeEvent = { offset -> annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset).any() annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset).any() @@ -202,6 +214,7 @@ fun ClickableText( onTextLayout: (TextLayoutResult) -> Unit = {}, onClick: (Int) -> Unit, onLongClick: (Int) -> Unit = {}, + onHover: (Int) -> Unit = {}, shouldConsumeEvent: (Int) -> Boolean ) { val layoutResult = remember { mutableStateOf(null) } @@ -225,6 +238,14 @@ fun ClickableText( consume } ) + }.pointerInput(onHover) { + if (appPlatform.isDesktop) { + detectCursorMove { pos -> + layoutResult.value?.let { layoutResult -> + onHover(layoutResult.getOffsetForPosition(pos)) + } + } + } } BasicText( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GestureDetector.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GestureDetector.kt index 4df48ca0af..9252e8b032 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GestureDetector.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GestureDetector.kt @@ -92,6 +92,17 @@ suspend fun PointerInputScope.detectGesture( } } +suspend fun PointerInputScope.detectCursorMove(onMove: (Offset) -> Unit = {},) = coroutineScope { + forEachGesture { + awaitPointerEventScope { + val event = awaitPointerEvent() + if (event.type == PointerEventType.Move) { + onMove(event.changes[0].position) + } + } + } +} + private suspend fun AwaitPointerEventScope.consumeUntilUp() { do { val event = awaitPointerEvent() From 45b7d09f835247f7a095f4e6e17679c1f6e59ebc Mon Sep 17 00:00:00 2001 From: "M. Sarmad Qadeer" Date: Thu, 17 Aug 2023 20:27:55 +0500 Subject: [PATCH 11/14] website: lowercase the website urls (#2893) --- website/.eleventy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/.eleventy.js b/website/.eleventy.js index 6c629af1b8..fbfc0d71c8 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -349,7 +349,7 @@ module.exports = function (ty) { if (parsed.path.startsWith("../../blog")) { parsed.path = parsed.path.replace("../../blog", "/blog") } - parsed.path = parsed.path.replace(/\.md$/, ".html") + parsed.path = parsed.path.replace(/\.md$/, ".html").toLowerCase() return uri.serialize(parsed) } }).use(markdownItAnchor, { From 01a95f88bb55bebc3bbc16932e857ffdfbaff5ef Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Aug 2023 18:21:05 +0100 Subject: [PATCH 12/14] ios: more responsive UI, especially during app start (#2942) * ios: more responsive UI, especially during app start * move terminal items to actor * fix for iOS 15/16 --- apps/ios/Shared/Model/ChatModel.swift | 40 +- apps/ios/Shared/Model/SimpleXAPI.swift | 452 ++++++++++-------- apps/ios/Shared/Views/TerminalView.swift | 23 +- .../Views/UserSettings/SettingsView.swift | 4 + 4 files changed, 320 insertions(+), 199 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 229408a4e2..d83f7b2b4d 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -11,6 +11,38 @@ import Combine import SwiftUI import SimpleXChat +actor TerminalItems { + private var terminalItems: [TerminalItem] = [] + + static let shared = TerminalItems() + + func items() -> [TerminalItem] { + terminalItems + } + + func add(_ item: TerminalItem) async { + addTermItem(&terminalItems, item) + let m = ChatModel.shared + if m.showingTerminal { + await MainActor.run { + addTermItem(&m.terminalItems, item) + } + } + } + + func addCommand(_ start: Date, _ cmd: ChatCommand, _ resp: ChatResponse) async { + addTermItem(&terminalItems, .cmd(start, cmd)) + addTermItem(&terminalItems, .resp(.now, resp)) + } +} + +private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) { + if items.count >= 200 { + items.removeFirst() + } + items.append(item) +} + final class ChatModel: ObservableObject { @Published var onboardingStage: OnboardingStage? @Published var setDeliveryReceipts = false @@ -33,6 +65,7 @@ final class ChatModel: ObservableObject { @Published var chatToTop: String? @Published var groupMembers: [GroupMember] = [] // items in the terminal view + @Published var showingTerminal = false @Published var terminalItems: [TerminalItem] = [] @Published var userAddress: UserContactLink? @Published var chatItemTTL: ChatItemTTL = .none @@ -592,13 +625,6 @@ final class ChatModel: ObservableObject { func contactNetworkStatus(_ contact: Contact) -> NetworkStatus { networkStatuses[contact.activeConn.agentConnId] ?? .unknown } - - func addTerminalItem(_ item: TerminalItem) { - if terminalItems.count >= 500 { - terminalItems.removeFirst() - } - terminalItems.append(item) - } } struct NTFContactRequest { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 62600b2825..e30217af2a 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -94,9 +94,8 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = if case let .response(_, json) = resp { logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") } - DispatchQueue.main.async { - ChatModel.shared.addTerminalItem(.cmd(start, cmd.obfuscated)) - ChatModel.shared.addTerminalItem(.resp(.now, resp)) + Task { + await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp) } return resp } @@ -321,12 +320,18 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6 let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl) let r: ChatResponse if type == .direct { - var cItem: ChatItem! - let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } }) + var cItem: ChatItem? = nil + let endTask = beginBGTask({ + if let cItem = cItem { + DispatchQueue.main.async { + chatModel.messageDelivery.removeValue(forKey: cItem.id) + } + } + }) r = await chatSendCmd(cmd, bgTask: false) if case let .newChatItem(_, aChatItem) = r { cItem = aChatItem.chatItem - chatModel.messageDelivery[cItem.id] = endTask + chatModel.messageDelivery[aChatItem.chatItem.id] = endTask return cItem } if let networkErrorAlert = networkErrorAlert(r) { @@ -804,7 +809,7 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws { func receiveFile(user: User, fileId: Int64, auto: Bool = false) async { if let chatItem = await apiReceiveFile(fileId: fileId, auto: auto) { - DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) } + await chatItemSimpleUpdate(user, chatItem) } } @@ -842,7 +847,7 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil, auto: Bool = false) asyn func cancelFile(user: User, fileId: Int64) async { if let chatItem = await apiCancelFile(fileId: fileId) { - DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) } + await chatItemSimpleUpdate(user, chatItem) cleanupFile(chatItem) } } @@ -1244,38 +1249,50 @@ class ChatReceiver { } func processReceivedMsg(_ res: ChatResponse) async { + Task { + await TerminalItems.shared.add(.resp(.now, res)) + } let m = ChatModel.shared - await MainActor.run { - m.addTerminalItem(.resp(.now, res)) - logger.debug("processReceivedMsg: \(res.responseType)") - switch res { - case let .newContactConnection(user, connection): - if active(user) { + logger.debug("processReceivedMsg: \(res.responseType)") + switch res { + case let .newContactConnection(user, connection): + if active(user) { + await MainActor.run { m.updateContactConnection(connection) } - case let .contactConnectionDeleted(user, connection): - if active(user) { + } + case let .contactConnectionDeleted(user, connection): + if active(user) { + await MainActor.run { m.removeChat(connection.id) } - case let .contactConnected(user, contact, _): - if active(user) && contact.directOrUsed { + } + case let .contactConnected(user, contact, _): + if active(user) && contact.directOrUsed { + await MainActor.run { m.updateContact(contact) m.dismissConnReqView(contact.activeConn.id) m.removeChat(contact.activeConn.id) } - if contact.directOrUsed { - NtfManager.shared.notifyContactConnected(user, contact) - } + } + if contact.directOrUsed { + NtfManager.shared.notifyContactConnected(user, contact) + } + await MainActor.run { m.setContactNetworkStatus(contact, .connected) - case let .contactConnecting(user, contact): - if active(user) && contact.directOrUsed { + } + case let .contactConnecting(user, contact): + if active(user) && contact.directOrUsed { + await MainActor.run { m.updateContact(contact) m.dismissConnReqView(contact.activeConn.id) m.removeChat(contact.activeConn.id) } - case let .receivedContactRequest(user, contactRequest): - if active(user) { - let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest) + } + case let .receivedContactRequest(user, contactRequest): + if active(user) { + let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest) + await MainActor.run { if m.hasChat(contactRequest.id) { m.updateChatInfo(cInfo) } else { @@ -1285,234 +1302,285 @@ func processReceivedMsg(_ res: ChatResponse) async { )) } } - NtfManager.shared.notifyContactRequest(user, contactRequest) - case let .contactUpdated(user, toContact): - if active(user) && m.hasChat(toContact.id) { + } + NtfManager.shared.notifyContactRequest(user, contactRequest) + case let .contactUpdated(user, toContact): + if active(user) && m.hasChat(toContact.id) { + await MainActor.run { let cInfo = ChatInfo.direct(contact: toContact) m.updateChatInfo(cInfo) } - case let .contactsMerged(user, intoContact, mergedContact): - if active(user) && m.hasChat(mergedContact.id) { + } + case let .contactsMerged(user, intoContact, mergedContact): + if active(user) && m.hasChat(mergedContact.id) { + await MainActor.run { if m.chatId == mergedContact.id { m.chatId = intoContact.id } m.removeChat(mergedContact.id) } - case let .contactsSubscribed(_, contactRefs): - updateContactsStatus(contactRefs, status: .connected) - case let .contactsDisconnected(_, contactRefs): - updateContactsStatus(contactRefs, status: .disconnected) - case let .contactSubError(user, contact, chatError): + } + case let .contactsSubscribed(_, contactRefs): + await updateContactsStatus(contactRefs, status: .connected) + case let .contactsDisconnected(_, contactRefs): + await updateContactsStatus(contactRefs, status: .disconnected) + case let .contactSubError(user, contact, chatError): + await MainActor.run { if active(user) { m.updateContact(contact) } processContactSubError(contact, chatError) - case let .contactSubSummary(user, contactSubscriptions): + } + case let .contactSubSummary(_, contactSubscriptions): + await MainActor.run { for sub in contactSubscriptions { - if active(user) { - m.updateContact(sub.contact) - } +// no need to update contact here, and it is slow +// if active(user) { +// m.updateContact(sub.contact) +// } if let err = sub.contactError { processContactSubError(sub.contact, err) } else { m.setContactNetworkStatus(sub.contact, .connected) } } - case let .newChatItem(user, aChatItem): - let cInfo = aChatItem.chatInfo - let cItem = aChatItem.chatItem + } + case let .newChatItem(user, aChatItem): + let cInfo = aChatItem.chatInfo + let cItem = aChatItem.chatItem + await MainActor.run { if active(user) { m.addChatItem(cInfo, cItem) } else if cItem.isRcvNew && cInfo.ntfsEnabled { m.increaseUnreadCounter(user: user) } - if let file = cItem.autoReceiveFile() { - Task { - await receiveFile(user: user, fileId: file.fileId, auto: true) - } + } + if let file = cItem.autoReceiveFile() { + Task { + await receiveFile(user: user, fileId: file.fileId, auto: true) } - if cItem.showNotification { + } + if cItem.showNotification { + NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) + } + case let .chatItemStatusUpdated(user, aChatItem): + let cInfo = aChatItem.chatInfo + let cItem = aChatItem.chatItem + if !cItem.isDeletedContent { + let added = active(user) ? await MainActor.run { m.upsertChatItem(cInfo, cItem) } : true + if added && cItem.showNotification { NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) } - case let .chatItemStatusUpdated(user, aChatItem): - let cInfo = aChatItem.chatInfo - let cItem = aChatItem.chatItem - if !cItem.isDeletedContent { - let added = active(user) ? m.upsertChatItem(cInfo, cItem) : true - if added && cItem.showNotification { - NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) - } + } + if let endTask = m.messageDelivery[cItem.id] { + switch cItem.meta.itemStatus { + case .sndSent: endTask() + case .sndErrorAuth: endTask() + case .sndError: endTask() + default: () } - if let endTask = m.messageDelivery[cItem.id] { - switch cItem.meta.itemStatus { - case .sndSent: endTask() - case .sndErrorAuth: endTask() - case .sndError: endTask() - default: () - } - } - case let .chatItemUpdated(user, aChatItem): - chatItemSimpleUpdate(user, aChatItem) - case let .chatItemReaction(user, _, r): - if active(user) { + } + case let .chatItemUpdated(user, aChatItem): + await chatItemSimpleUpdate(user, aChatItem) + case let .chatItemReaction(user, _, r): + if active(user) { + await MainActor.run { m.updateChatItem(r.chatInfo, r.chatReaction.chatItem) } - case let .chatItemDeleted(user, deletedChatItem, toChatItem, _): - if !active(user) { - if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled { + } + case let .chatItemDeleted(user, deletedChatItem, toChatItem, _): + if !active(user) { + if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled { + await MainActor.run { m.decreaseUnreadCounter(user: user) } - return } + return + } + await MainActor.run { if let toChatItem = toChatItem { _ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem) } else { m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem) } - case let .receivedGroupInvitation(user, groupInfo, _, _): - if active(user) { + } + case let .receivedGroupInvitation(user, groupInfo, _, _): + if active(user) { + await MainActor.run { m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated // NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation? } - case let .userAcceptedGroupSent(user, groupInfo, hostContact): - if !active(user) { return } + } + case let .userAcceptedGroupSent(user, groupInfo, hostContact): + if !active(user) { return } + await MainActor.run { m.updateGroup(groupInfo) if let hostContact = hostContact { m.dismissConnReqView(hostContact.activeConn.id) m.removeChat(hostContact.activeConn.id) } - case let .joinedGroupMemberConnecting(user, groupInfo, _, member): - if active(user) { + } + case let .joinedGroupMemberConnecting(user, groupInfo, _, member): + if active(user) { + await MainActor.run { _ = m.upsertGroupMember(groupInfo, member) } - case let .deletedMemberUser(user, groupInfo, _): // TODO update user member - if active(user) { + } + case let .deletedMemberUser(user, groupInfo, _): // TODO update user member + if active(user) { + await MainActor.run { m.updateGroup(groupInfo) } - case let .deletedMember(user, groupInfo, _, deletedMember): - if active(user) { + } + case let .deletedMember(user, groupInfo, _, deletedMember): + if active(user) { + await MainActor.run { _ = m.upsertGroupMember(groupInfo, deletedMember) } - case let .leftMember(user, groupInfo, member): - if active(user) { + } + case let .leftMember(user, groupInfo, member): + if active(user) { + await MainActor.run { _ = m.upsertGroupMember(groupInfo, member) } - case let .groupDeleted(user, groupInfo, _): // TODO update user member - if active(user) { + } + case let .groupDeleted(user, groupInfo, _): // TODO update user member + if active(user) { + await MainActor.run { m.updateGroup(groupInfo) } - case let .userJoinedGroup(user, groupInfo): - if active(user) { + } + case let .userJoinedGroup(user, groupInfo): + if active(user) { + await MainActor.run { m.updateGroup(groupInfo) } - case let .joinedGroupMember(user, groupInfo, member): - if active(user) { + } + case let .joinedGroupMember(user, groupInfo, member): + if active(user) { + await MainActor.run { _ = m.upsertGroupMember(groupInfo, member) } - case let .connectedToGroupMember(user, groupInfo, member, memberContact): - if active(user) { + } + case let .connectedToGroupMember(user, groupInfo, member, memberContact): + if active(user) { + await MainActor.run { _ = m.upsertGroupMember(groupInfo, member) } - if let contact = memberContact { + } + if let contact = memberContact { + await MainActor.run { m.setContactNetworkStatus(contact, .connected) } - case let .groupUpdated(user, toGroup): - if active(user) { + } + case let .groupUpdated(user, toGroup): + if active(user) { + await MainActor.run { m.updateGroup(toGroup) } - case let .memberRole(user, groupInfo, _, _, _, _): - if active(user) { + } + case let .memberRole(user, groupInfo, _, _, _, _): + if active(user) { + await MainActor.run { m.updateGroup(groupInfo) } - case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE - chatItemSimpleUpdate(user, aChatItem) - case let .rcvFileStart(user, aChatItem): - chatItemSimpleUpdate(user, aChatItem) - case let .rcvFileComplete(user, aChatItem): - chatItemSimpleUpdate(user, aChatItem) - case let .rcvFileSndCancelled(user, aChatItem, _): - chatItemSimpleUpdate(user, aChatItem) - cleanupFile(aChatItem) - case let .rcvFileProgressXFTP(user, aChatItem, _, _): - chatItemSimpleUpdate(user, aChatItem) - case let .rcvFileError(user, aChatItem): - chatItemSimpleUpdate(user, aChatItem) - cleanupFile(aChatItem) - case let .sndFileStart(user, aChatItem, _): - chatItemSimpleUpdate(user, aChatItem) - case let .sndFileComplete(user, aChatItem, _): - chatItemSimpleUpdate(user, aChatItem) - cleanupDirectFile(aChatItem) - case let .sndFileRcvCancelled(user, aChatItem, _): - chatItemSimpleUpdate(user, aChatItem) - cleanupDirectFile(aChatItem) - case let .sndFileProgressXFTP(user, aChatItem, _, _, _): - chatItemSimpleUpdate(user, aChatItem) - case let .sndFileCompleteXFTP(user, aChatItem, _): - chatItemSimpleUpdate(user, aChatItem) - cleanupFile(aChatItem) - case let .sndFileError(user, aChatItem): - chatItemSimpleUpdate(user, aChatItem) - cleanupFile(aChatItem) - case let .callInvitation(invitation): - m.callInvitations[invitation.contact.id] = invitation - activateCall(invitation) - case let .callOffer(_, contact, callType, offer, sharedKey, _): - withCall(contact) { call in - call.callState = .offerReceived - call.peerMedia = callType.media - call.sharedKey = sharedKey - let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY) - let iceServers = getIceServers() - logger.debug(".callOffer useRelay \(useRelay)") - logger.debug(".callOffer iceServers \(String(describing: iceServers))") - m.callCommand = .offer( - offer: offer.rtcSession, - iceCandidates: offer.rtcIceCandidates, - media: callType.media, aesKey: sharedKey, - iceServers: iceServers, - relay: useRelay - ) - } - case let .callAnswer(_, contact, answer): - withCall(contact) { call in - call.callState = .answerReceived - m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates) - } - case let .callExtraInfo(_, contact, extraInfo): - withCall(contact) { _ in - m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates) - } - case let .callEnded(_, contact): - if let invitation = m.callInvitations.removeValue(forKey: contact.id) { - CallController.shared.reportCallRemoteEnded(invitation: invitation) - } - withCall(contact) { call in - m.callCommand = .end - CallController.shared.reportCallRemoteEnded(call: call) - } - case .chatSuspended: - chatSuspended() - case let .contactSwitch(_, contact, switchProgress): - m.updateContactConnectionStats(contact, switchProgress.connectionStats) - case let .groupMemberSwitch(_, groupInfo, member, switchProgress): - m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats) - case let .contactRatchetSync(_, contact, ratchetSyncProgress): - m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats) - case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress): - m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats) - default: - logger.debug("unsupported event: \(res.responseType)") } + case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE + await chatItemSimpleUpdate(user, aChatItem) + case let .rcvFileStart(user, aChatItem): + await chatItemSimpleUpdate(user, aChatItem) + case let .rcvFileComplete(user, aChatItem): + await chatItemSimpleUpdate(user, aChatItem) + case let .rcvFileSndCancelled(user, aChatItem, _): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupFile(aChatItem) } + case let .rcvFileProgressXFTP(user, aChatItem, _, _): + await chatItemSimpleUpdate(user, aChatItem) + case let .rcvFileError(user, aChatItem): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupFile(aChatItem) } + case let .sndFileStart(user, aChatItem, _): + await chatItemSimpleUpdate(user, aChatItem) + case let .sndFileComplete(user, aChatItem, _): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupDirectFile(aChatItem) } + case let .sndFileRcvCancelled(user, aChatItem, _): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupDirectFile(aChatItem) } + case let .sndFileProgressXFTP(user, aChatItem, _, _, _): + await chatItemSimpleUpdate(user, aChatItem) + case let .sndFileCompleteXFTP(user, aChatItem, _): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupFile(aChatItem) } + case let .sndFileError(user, aChatItem): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupFile(aChatItem) } + case let .callInvitation(invitation): + m.callInvitations[invitation.contact.id] = invitation + activateCall(invitation) + case let .callOffer(_, contact, callType, offer, sharedKey, _): + await withCall(contact) { call in + call.callState = .offerReceived + call.peerMedia = callType.media + call.sharedKey = sharedKey + let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY) + let iceServers = getIceServers() + logger.debug(".callOffer useRelay \(useRelay)") + logger.debug(".callOffer iceServers \(String(describing: iceServers))") + m.callCommand = .offer( + offer: offer.rtcSession, + iceCandidates: offer.rtcIceCandidates, + media: callType.media, aesKey: sharedKey, + iceServers: iceServers, + relay: useRelay + ) + } + case let .callAnswer(_, contact, answer): + await withCall(contact) { call in + call.callState = .answerReceived + m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates) + } + case let .callExtraInfo(_, contact, extraInfo): + await withCall(contact) { _ in + m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates) + } + case let .callEnded(_, contact): + if let invitation = await MainActor.run(body: { m.callInvitations.removeValue(forKey: contact.id) }) { + CallController.shared.reportCallRemoteEnded(invitation: invitation) + } + await withCall(contact) { call in + m.callCommand = .end + CallController.shared.reportCallRemoteEnded(call: call) + } + case .chatSuspended: + chatSuspended() + case let .contactSwitch(_, contact, switchProgress): + await MainActor.run { + m.updateContactConnectionStats(contact, switchProgress.connectionStats) + } + case let .groupMemberSwitch(_, groupInfo, member, switchProgress): + await MainActor.run { + m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats) + } + case let .contactRatchetSync(_, contact, ratchetSyncProgress): + await MainActor.run { + m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats) + } + case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress): + await MainActor.run { + m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats) + } + default: + logger.debug("unsupported event: \(res.responseType)") + } - func withCall(_ contact: Contact, _ perform: (Call) -> Void) { - if let call = m.activeCall, call.contact.apiId == contact.apiId { - perform(call) - } else { - logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)") - } + func withCall(_ contact: Contact, _ perform: (Call) -> Void) async { + if let call = m.activeCall, call.contact.apiId == contact.apiId { + await MainActor.run { perform(call) } + } else { + logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)") } } } @@ -1521,19 +1589,23 @@ func active(_ user: User) -> Bool { user.id == ChatModel.shared.currentUser?.id } -func chatItemSimpleUpdate(_ user: User, _ aChatItem: AChatItem) { +func chatItemSimpleUpdate(_ user: User, _ aChatItem: AChatItem) async { let m = ChatModel.shared let cInfo = aChatItem.chatInfo let cItem = aChatItem.chatItem - if active(user) && m.upsertChatItem(cInfo, cItem) { - NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) + if active(user) { + if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) { + NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) + } } } -func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) { +func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async { let m = ChatModel.shared - for c in contactRefs { - m.networkStatuses[c.agentConnId] = status + await MainActor.run { + for c in contactRefs { + m.networkStatuses[c.agentConnId] = status + } } } @@ -1572,7 +1644,9 @@ func activateCall(_ callInvitation: RcvCallInvitation) { let m = ChatModel.shared CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in if let error = error { - m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil + DispatchQueue.main.async { + m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil + } logger.error("reportNewIncomingCall error: \(error.localizedDescription)") } else { logger.debug("reportNewIncomingCall success") diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index be6ccfd3df..94a8937db6 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -22,10 +22,28 @@ struct TerminalView: View { @State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) @State private var terminalItem: TerminalItem? @State private var scrolled = false + @State private var showing = false var body: some View { if authorized { terminalView() + .onAppear { + if showing { return } + showing = true + Task { + let items = await TerminalItems.shared.items() + await MainActor.run { + chatModel.terminalItems = items + chatModel.showingTerminal = true + } + } + } + .onDisappear { + if terminalItem == nil { + chatModel.showingTerminal = false + chatModel.terminalItems = [] + } + } } else { Button(action: runAuth) { Label("Unlock", systemImage: "lock") } .onAppear(perform: runAuth) @@ -118,9 +136,8 @@ struct TerminalView: View { let cmd = ChatCommand.string(composeState.message) if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) { let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty"))) - DispatchQueue.main.async { - ChatModel.shared.addTerminalItem(.cmd(.now, cmd)) - ChatModel.shared.addTerminalItem(.resp(.now, resp)) + Task { + await TerminalItems.shared.addCommand(.now, cmd, resp) } } else { DispatchQueue.global().async { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index d23380d6b0..d6681a51cd 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -299,6 +299,10 @@ struct SettingsView: View { } .navigationTitle("Your settings") } + .onDisappear { + chatModel.showingTerminal = false + chatModel.terminalItems = [] + } } private func chatDatabaseRow() -> some View { From b829bd0c060ffdeeeaa9854367474e9a2d743e10 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 17 Aug 2023 23:05:19 +0400 Subject: [PATCH 13/14] ios: fix Speak Screen repeating messages (#2897) * ios: fix Speak Screen repeating messages * fix * different modifier --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift | 1 + apps/ios/Shared/Views/Chat/ChatView.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 2dfd0f15f6..ceaf175f9b 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -67,6 +67,7 @@ struct FramedItemView: View { .padding(.horizontal, 12) .padding(.bottom, 6) .overlay(DetermineWidth()) + .accessibilityLabel("") } } .background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme)) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index a73bb766e9..4f1b4fe728 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -515,6 +515,7 @@ struct ChatView: View { VStack(alignment: alignment.horizontal, spacing: 3) { ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) .uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu) + .accessibilityLabel("") if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 { chatItemReactions() .padding(.bottom, 4) From 8bb19db1ff7a306237361f6316760e9e6ba4b5c9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Aug 2023 23:16:23 +0100 Subject: [PATCH 14/14] core: 5.3.0.5 --- package.yaml | 2 +- simplex-chat.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 94a16f1001..6d3975aa4c 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.3.0.4 +version: 5.3.0.5 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/simplex-chat.cabal b/simplex-chat.cabal index ea1bc2e71c..e53f047a24 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 5.3.0.4 +version: 5.3.0.5 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat